feat: FIRST_INSTALL_TOKEN to help services like Umbrel secure the first_install endpoint (#3751)
Co-authored-by: dni ⚡ <office@dnilabs.com>
This commit is contained in:
@@ -6,6 +6,10 @@
|
|||||||
# The following settings are ONLY set in your .env file.
|
# The following settings are ONLY set in your .env file.
|
||||||
# They are NOT managed by the Admin UI and are not stored in the database.
|
# They are NOT managed by the Admin UI and are not stored in the database.
|
||||||
|
|
||||||
|
# === First Install Token ===
|
||||||
|
# if set the user is required to enter this token on the /first_install page
|
||||||
|
# FIRST_INSTALL_TOKEN="myaccesstoken"
|
||||||
|
|
||||||
# === Logging and Development ===
|
# === Logging and Development ===
|
||||||
|
|
||||||
DEBUG=False
|
DEBUG=False
|
||||||
|
|||||||
@@ -358,6 +358,7 @@ class UpdateSuperuserPassword(BaseModel):
|
|||||||
username: str = Query(default=..., min_length=2, max_length=20)
|
username: str = Query(default=..., min_length=2, max_length=20)
|
||||||
password: str = Query(default=..., min_length=8, max_length=50)
|
password: str = Query(default=..., min_length=8, max_length=50)
|
||||||
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
password_repeat: str = Query(default=..., min_length=8, max_length=50)
|
||||||
|
first_install_token: str | None = Query(None)
|
||||||
|
|
||||||
|
|
||||||
class LoginUsr(BaseModel):
|
class LoginUsr(BaseModel):
|
||||||
|
|||||||
@@ -503,6 +503,12 @@ async def update_ui_customization(
|
|||||||
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
async def first_install(data: UpdateSuperuserPassword) -> JSONResponse:
|
||||||
if not settings.first_install:
|
if not settings.first_install:
|
||||||
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
raise HTTPException(HTTPStatus.FORBIDDEN, "This is not your first install")
|
||||||
|
if settings.first_install_token:
|
||||||
|
if not data.first_install_token:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Missing first_install_token.")
|
||||||
|
if settings.first_install_token != data.first_install_token:
|
||||||
|
raise HTTPException(HTTPStatus.UNAUTHORIZED, "Invalid first_install_token.")
|
||||||
|
|
||||||
account = await get_account(settings.super_user)
|
account = await get_account(settings.super_user)
|
||||||
if not account:
|
if not account:
|
||||||
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
raise HTTPException(HTTPStatus.INTERNAL_SERVER_ERROR, "Superuser not found.")
|
||||||
|
|||||||
@@ -983,6 +983,7 @@ class EnvSettings(LNbitsSettings):
|
|||||||
enable_log_to_file: bool = Field(default=True)
|
enable_log_to_file: bool = Field(default=True)
|
||||||
log_rotation: str = Field(default="100 MB")
|
log_rotation: str = Field(default="100 MB")
|
||||||
log_retention: str = Field(default="3 months")
|
log_retention: str = Field(default="3 months")
|
||||||
|
first_install_token: str | None = Field(default=None)
|
||||||
|
|
||||||
cleanup_wallets_days: int = Field(default=90, ge=0)
|
cleanup_wallets_days: int = Field(default=90, ge=0)
|
||||||
funding_source_max_retries: int = Field(default=4, ge=0)
|
funding_source_max_retries: int = Field(default=4, ge=0)
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -518,6 +518,7 @@ window.localisation.en = {
|
|||||||
hash: 'Hash: ',
|
hash: 'Hash: ',
|
||||||
welcome_lnbits: 'Welcome to LNbits',
|
welcome_lnbits: 'Welcome to LNbits',
|
||||||
setup_su_account: 'Set up the Superuser account below.',
|
setup_su_account: 'Set up the Superuser account below.',
|
||||||
|
first_install_token: 'First Install Token (optional)',
|
||||||
create_ticker_converter: 'Create Currency Ticker Converter',
|
create_ticker_converter: 'Create Currency Ticker Converter',
|
||||||
enable_audit: 'Enable Audit',
|
enable_audit: 'Enable Audit',
|
||||||
recommended: 'Recommended',
|
recommended: 'Recommended',
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ window.PageFirstInstall = {
|
|||||||
isPwdRepeat: true,
|
isPwdRepeat: true,
|
||||||
username: '',
|
username: '',
|
||||||
password: '',
|
password: '',
|
||||||
passwordRepeat: ''
|
passwordRepeat: '',
|
||||||
|
firstInstallToken: ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -17,20 +18,25 @@ window.PageFirstInstall = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async setPassword() {
|
setPassword() {
|
||||||
try {
|
LNbits.api
|
||||||
await LNbits.api.request('PUT', '/api/v1/auth/first_install', null, {
|
.request('PUT', `/api/v1/auth/first_install`, null, {
|
||||||
username: this.loginData.username,
|
username: this.loginData.username,
|
||||||
password: this.loginData.password,
|
password: this.loginData.password,
|
||||||
password_repeat: this.loginData.passwordRepeat
|
password_repeat: this.loginData.passwordRepeat,
|
||||||
|
first_install_token: this.loginData.firstInstallToken
|
||||||
})
|
})
|
||||||
window.location.href = '/admin'
|
.then(async () => {
|
||||||
} catch (e) {
|
const res = await LNbits.api.getAuthUser()
|
||||||
LNbits.utils.notifyApiError(e)
|
this.g.user = LNbits.map.user(res.data)
|
||||||
}
|
this.g.isPublicPage = false
|
||||||
|
this.$router.push('/admin')
|
||||||
|
})
|
||||||
|
.catch(this.utils.notifyApiError)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
document.title = 'First Install - LNbits'
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
this.loginData.firstInstallToken = params.get('token') || ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,18 @@
|
|||||||
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
@click="loginData.isPwdRepeat = !loginData.isPwdRepeat"
|
||||||
/> </template
|
/> </template
|
||||||
></q-input>
|
></q-input>
|
||||||
|
<q-input
|
||||||
|
filled
|
||||||
|
v-model.trim="loginData.firstInstallToken"
|
||||||
|
:type="loginData.isPwd ? 'password' : 'text'"
|
||||||
|
:label="$t('first_install_token')"
|
||||||
|
><template v-slot:append>
|
||||||
|
<q-icon
|
||||||
|
:name="loginData.isPwd ? 'visibility_off' : 'visibility'"
|
||||||
|
class="cursor-pointer"
|
||||||
|
@click="loginData.isPwd = !loginData.isPwd"
|
||||||
|
/> </template
|
||||||
|
></q-input>
|
||||||
<q-btn
|
<q-btn
|
||||||
@click="setPassword()"
|
@click="setPassword()"
|
||||||
unelevated
|
unelevated
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ from lnbits.settings import settings
|
|||||||
|
|
||||||
def log_server_info():
|
def log_server_info():
|
||||||
logger.info("LNbits Info")
|
logger.info("LNbits Info")
|
||||||
|
if settings.first_install:
|
||||||
|
logger.success("This is a fresh install of LNbits.")
|
||||||
|
if settings.first_install_token:
|
||||||
|
logger.success(
|
||||||
|
f"FIRST_INSTALL_TOKEN: `{settings.first_install_token}`. "
|
||||||
|
"Please provide this token on /first_install."
|
||||||
|
)
|
||||||
logger.info(f"Version: {settings.version}")
|
logger.info(f"Version: {settings.version}")
|
||||||
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
logger.info(f"Baseurl: {settings.lnbits_baseurl}")
|
||||||
logger.info(f"Host: {settings.host}")
|
logger.info(f"Host: {settings.host}")
|
||||||
|
|||||||
Reference in New Issue
Block a user