Compare commits

..

11 commits
master ... i18n

23 changed files with 322 additions and 176 deletions

View file

@ -38,7 +38,7 @@ pub struct TokenRequest<'r> {
pub grant_type: GrantType,
pub code: Option<&'r str>,
pub redirect_uri: Option<&'r str>,
pub client_id: Option<&'r str>,
pub client_id: &'r str,
pub client_secret: Option<&'r str>,
pub scope: Option<&'r str>,
pub refresh_token: Option<&'r str>,

View file

@ -51,14 +51,12 @@ pub enum TokenError {
RefreshTokenExpired,
AuthorizationCodeUsed,
AuthorizationCodeExpired,
HttpAuthDifferentClientId,
AppError(apps::Error),
AppNotFoundFromAuthorizationCode(String),
AppNotFoundFromRefreshToken(String),
AppIdNotProvided,
AppNotFound(String),
AppSecretNotProvided,
Blocking(task::JoinError),
SecretCompare(hash::Error),
AppIdWrong,
AppSecretWrong,
UserError(users::Error),
UserNotFound,
@ -113,19 +111,14 @@ impl<'r> Responder<'r, 'static> for TokenError {
Status::BadRequest,
"Authorization code has expired".to_string(),
),
TokenError::AppError(e) => (Status::InternalServerError, e.to_string()),
TokenError::AppNotFoundFromAuthorizationCode(e) => (
Status::NotFound,
format!("Could not find application from authorization code {e}"),
),
TokenError::AppNotFoundFromRefreshToken(e) => (
Status::NotFound,
format!("Could not find application from refresh token {e}"),
),
TokenError::AppIdNotProvided => (
TokenError::HttpAuthDifferentClientId => (
Status::BadRequest,
"Could not get client_id: not provided in any way".to_string(),
"HTTP Auth differs from provided client_id".to_string(),
),
TokenError::AppError(e) => (Status::InternalServerError, e.to_string()),
TokenError::AppNotFound(e) => {
(Status::NotFound, format!("Could not find application {e}"))
}
TokenError::AppSecretNotProvided => {
(Status::BadRequest, "Secret was not provided".to_string())
}
@ -134,7 +127,6 @@ impl<'r> Responder<'r, 'static> for TokenError {
Status::InternalServerError,
format!("Failed to check app secret: {e}"),
),
TokenError::AppIdWrong => (Status::Forbidden, "Invalid client_id provided".to_string()),
TokenError::AppSecretWrong => {
(Status::Forbidden, "Invalid secret provided".to_string())
}
@ -179,8 +171,8 @@ pub async fn request_token(
) -> std::result::Result<TokenResponse, TokenError> {
let mut transaction = db.begin().await.map_err(TokenError::TransactionStart)?;
// Get user and app depending on grant type
let (user, app) = match token_request.grant_type {
// Get user depending on grant type
let user = match token_request.grant_type {
GrantType::AuthorizationCode => {
let authorization_code = token_request
.code
@ -228,20 +220,12 @@ pub async fn request_token(
return Err(TokenError::UserArchived(user.id().to_string()));
}
// Get app from code
let app = App::get_one_from_authorization_code(&mut transaction, authorization_code)
.await
.map_err(TokenError::AppError)?
.ok_or(TokenError::AppNotFoundFromAuthorizationCode(
authorization_code.into(),
))?;
// Mark code as used
code.use_code(&mut transaction)
.await
.map_err(TokenError::AuthorizationError)?;
(user, app)
user
}
GrantType::RefreshToken => {
let refresh_token = token_request
@ -280,39 +264,28 @@ pub async fn request_token(
return Err(TokenError::RefreshTokenExpired);
}
// Get app
let app = App::get_one_by_id(&mut transaction, refresh_token.app().as_ref())
.await
.map_err(TokenError::AppError)?
.ok_or(TokenError::AppNotFoundFromRefreshToken(format!(
"Refresh token for user {}",
refresh_token.user()
)))?;
refresh_token
.use_token(&mut transaction)
.await
.map_err(TokenError::RefreshTokenError)?;
(user, app)
user
}
};
// Get client id
// https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3
let provided_client_id = match (&app_auth, token_request.client_id) {
(Some(http_auth), _) => http_auth.id.to_string(),
(None, Some(form)) => form.into(),
(None, None) => {
return Err(TokenError::AppIdNotProvided);
// If HTTP Basic Auth is provided, verify provided client id in form
if let Some(app_auth) = &app_auth {
if app_auth.id != token_request.client_id {
return Err(TokenError::HttpAuthDifferentClientId);
}
};
// Verify client id
if app.id().as_ref() != provided_client_id {
return Err(TokenError::AppIdWrong);
}
// Get app
let app = App::get_one_by_id(&mut transaction, token_request.client_id)
.await
.map_err(TokenError::AppError)?
.ok_or(TokenError::AppNotFound(token_request.client_id.into()))?;
if app.is_confidential() {
let provided_secret = match (app_auth, token_request.client_secret) {
(Some(http_auth), _) => http_auth.password,

View file

@ -41,7 +41,5 @@ pub mod content {
#[derive(Serialize)]
#[serde(crate = "rocket::serde")]
#[derive(Clone)]
pub struct ResetPassword {
pub username: String,
}
pub struct ResetPassword {}
}

View file

@ -12,13 +12,11 @@ pub async fn reset_password_page(
return Err(Error::bad_request("Reset password token has expired"));
}
let user = User::get_one_from_password_reset_token(&mut **db, &token.0)
User::get_one_from_password_reset_token(&mut **db, &token.0)
.await?
.ok_or_else(|| Error::not_found("Invalid or expired token"))?;
let page = Page::ResetPassword(super::content::ResetPassword {
username: user.username().into(),
});
let page = Page::ResetPassword(super::content::ResetPassword {});
Ok(flash
.map(|flash| Page::with_flash(page.clone(), flash))

View file

@ -0,0 +1,52 @@
{
"error.go_home": "Go to homepage",
"header.settings": "Settings",
"header.admin_panel": "Admin panel",
"header.logout": "Logout",
"setup.welcome": "Welcome to Ezidam!",
"setup.initial_setup": "Initial setup",
"setup.first_admin_account": "first admin account",
"setup.username": "Username",
"setup.password": "Password",
"setup.settings": "settings",
"setup.base_url": "Base URL",
"setup.finish": "Finish setup",
"forgot_password.title": "Forgot password",
"forgot_password.email": "Email",
"forgot_password.paper_key": "Paper key",
"forgot_password.email_description": "Enter your email address linked to your account. We will email you a link to reset your password.",
"forgot_password.email_address": "Email address",
"forgot_password.request": "Request password reset",
"forgot_password.paper_key_description": "Enter your login linked to your account, with your paper key.",
"forgot_password.login": "Login",
"reset_password.title": "Reset your password",
"reset_password.new_password": "New password",
"reset_password.confirm_password": "Confirm new password",
"reset_password.set_password": "Set new password",
"authorize.access_app": "Access [[app_name]]",
"authorize.with_account": "With your [[business_name]] account",
"authorize.login": "Login",
"authorize.password": "Password",
"authorize.authorize": "Authorize",
"authorize.forgot_password": "Forgot your password?",
"totp.verify_account": "Verify your account",
"totp.enter_code": "Enter the code displayed on your device",
"totp.verify_code": "Verify code",
"redirect.hello": "Hello!",
"redirect.preparing_app": "Preparing application",
"redirect.not_redirected": "Click here if you are not redirected",
"user_settings.settings": "Settings",
"user_settings.personal": "Personal",
"user_settings.security": "Security",
"user_settings.visual": "Visual",
"user_settings_visual.enable_dark": "Enable dark mode",
"user_settings_visual.enable_light": "Enable light mode",
"user_settings_personal.my_profile": "My profile",
"user_settings_personal.username": "Username",
"user_settings_personal.full_name": "Full name",
"user_settings_personal.email": "Email address",
"user_settings_personal.timezone": "Timezone",
"user_settings_personal.utc_default": "UTC (Default)",
"user_settings_personal.last_updated": "Profile last updated on",
"user_settings_personal.save": "Save"
}

View file

@ -0,0 +1,52 @@
{
"error.go_home": "Aller à la page d'accueil",
"header.settings": "Réglages",
"header.admin_panel": "Console d'administration",
"header.logout": "Se déconnecter",
"setup.welcome": "Bienvenue sur Ezidam!",
"setup.initial_setup": "Configuration initiale",
"setup.first_admin_account": "premier compte administrateur",
"setup.username": "Nom d'utilisateur",
"setup.password": "Mot de passe",
"setup.settings": "réglages",
"setup.base_url": "URL de base",
"setup.finish": "Terminer la configuration",
"forgot_password.title": "Mot de passe oublié",
"forgot_password.email": "Email",
"forgot_password.paper_key": "Clé papier",
"forgot_password.email_description": "Entrez l'adresse email associée à votre compte. Nous allons vous envoyer par email un lien pour réinitialiser votre mot de passe.",
"forgot_password.email_address": "Adresse email",
"forgot_password.request": "Demander réinitialisation du mot de passe",
"forgot_password.paper_key_description": "Entrez votre nom d'utilisateur, ainsi que votre clé papier.",
"forgot_password.login": "Nom d'utilisateur",
"reset_password.title": "Réinitialisation du mot de passe",
"reset_password.new_password": "Nouveau mot de passe",
"reset_password.confirm_password": "Confirmer le mot de passe",
"reset_password.set_password": "Enregistrer le mot de passe",
"authorize.access_app": "Accéder à [[app_name]]",
"authorize.with_account": "Avec votre compte [[business_name]]",
"authorize.login": "Nom d'utilisateur",
"authorize.password": "Mot de passe",
"authorize.authorize": "Autoriser",
"authorize.forgot_password": "Mot de passe oublié?",
"totp.verify_account": "Vérification de votre compte",
"totp.enter_code": "Entrez le code de vérification sur votre appareil",
"totp.verify_code": "Verifier le code",
"redirect.hello": "Bonjour!",
"redirect.preparing_app": "Préparation de l'application",
"redirect.not_redirected": "Cliquez ici si vous n'êtes pas redirigé",
"user_settings.settings": "Réglages",
"user_settings.personal": "Personnel",
"user_settings.security": "Sécurité",
"user_settings.visual": "Visuel",
"user_settings_visual.enable_dark": "Activer le mode sombre",
"user_settings_visual.enable_light": "Activer le mode clair",
"user_settings_personal.my_profile": "Mon profil",
"user_settings_personal.username": "Nom d'utilisateur",
"user_settings_personal.full_name": "Nom complet",
"user_settings_personal.email": "Adresse email",
"user_settings_personal.timezone": "Fuseau horaire",
"user_settings_personal.utc_default": "UTC (par défaut)",
"user_settings_personal.last_updated": "Profil mis à jour le",
"user_settings_personal.save": "Enregistrer"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,2 @@
// https://www.unpkg.com/loc-i18next@0.1.5/loc-i18next.min.js
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locI18next=t()}(this,function(){"use strict";function e(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function t(t){for(var n=1;n<arguments.length;n++){var r=null!=arguments[n]?arguments[n]:{},i=Object.keys(r);"function"==typeof Object.getOwnPropertySymbols&&(i=i.concat(Object.getOwnPropertySymbols(r).filter(function(e){return Object.getOwnPropertyDescriptor(r,e).enumerable}))),i.forEach(function(n){e(t,n,r[n])})}return t}function n(e){function n(t,n,r){var i="text";if(0==n.indexOf("[")){var o=n.split("]");n=o[1],i=o[0].substr(1,o[0].length-1)}if(n=n.indexOf(";")==n.length-1?n.substr(0,n.length-2):n,"html"===i)t.innerHTML=e.t(n,c(r,t.innerHTML));else if("text"===i)t.textContent=e.t(n,c(r,t.textContent));else if("prepend"===i){var l=t.innerHTML.indexOf("<loc-i18n>"),u=t.innerHTML.indexOf("</loc-i18n>")+11;l>-1&&u>6&&(t.innerHTML=[t.innerHTML.substring(0,l),t.innerHTML.slice(u)].join("")),t.innerHTML=["<loc-i18n>",e.t(n,c(r,t.innerHTML)),"</loc-i18n>",t.innerHTML].join("")}else if("append"===i){var f=t.innerHTML.indexOf("<loc-i18n>"),s=t.innerHTML.indexOf("</loc-i18n>")+11;f>-1&&s>6&&(t.innerHTML=[t.innerHTML.substring(0,f),t.innerHTML.slice(s)].join("")),t.innerHTML=[t.innerHTML,"<loc-i18n>",e.t(n,c(r,t.innerHTML),"</loc-i18n>")].join("")}else if(0===i.indexOf("data-")){var a=i.substr("data-".length),p=e.t(n,c(r,t.getAttribute(a)));t.setAttribute(a,p),t.setAttribute(i,p)}else t.setAttribute(i,e.t(n,c(r,t.getAttribute(i))))}function i(e){return JSON.parse(e.replace(/:\s*"([^"]*)"/g,function(e,t){return': "'+t.replace(/:/g,"@colon@")+'"'}).replace(/:\s*'([^']*)'/g,function(e,t){return': "'+t.replace(/:/g,"@colon@")+'"'}).replace(/(['"])?([a-z0-9A-Z_]+)(['"])?\s*:/g,'"$2": ').replace(/@colon@/g,":"))}function o(e,r){var o=e.getAttribute(u.selectorAttr);if(o){var l=e,c=e.getAttribute(u.targetAttr);if(null!=c&&(l=e.querySelector(c)||e),r||!0!==u.useOptionsAttr||(r=i(e.getAttribute(u.optionsAttr)||"{}")),r=r||{},o.indexOf(";")>=0)for(var f=o.split(";"),s=0,a=f.length;s<a;s++)""!=f[s]&&n(l,f[s],r);else n(l,o,r);if(!0===u.useOptionsAttr){var p={};p=t({clone:p},r),delete p.lng,e.setAttribute(u.optionsAttr,JSON.stringify(p))}}}function l(e,t){for(var n=(null===t||void 0===t?void 0:t.document)||u.document,r=n.querySelectorAll(e),i=0;i<r.length;i++){for(var l=r[i],c=l.querySelectorAll("["+u.selectorAttr+"]"),f=c.length-1;f>-1;f--)o(c[f],t);o(l,t)}}var u=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};u=t({},r,u);var c=function(e,n){return u.parseDefaultValueFromContent?t({},e,{defaultValue:n}):e};return l}var r={selectorAttr:"data-i18n",targetAttr:"i18n-target",optionsAttr:"i18n-options",useOptionsAttr:!1,parseDefaultValueFromContent:!0,document:document};return{init:n}});

View file

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/>
@ -9,6 +9,11 @@
<link href="/css/tabler.min.css" rel="stylesheet"/>
<link href="/css/tabler-vendors.min.css" rel="stylesheet"/>
<link href="/css/demo.min.css" rel="stylesheet"/>
<!-- i18n -->
<script src="/libs/i18next/i18next.min.js"></script>
<script src="/libs/i18next-http-backend/i18nextHttpBackend.min.js"></script>
<script src="/libs/i18next-browser-language-detector/i18nextBrowserLanguageDetector.min.js"></script>
<script src="/libs/loc-i18next/loc-i18next.min.js"></script>
<!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
@ -27,5 +32,34 @@
}
</style>
</head>
{% block page %}{% endblock page %}
<script>
i18next
.use(i18nextBrowserLanguageDetector)
.use(i18nextHttpBackend)
.init({
detection: {
order: ['localStorage', 'navigator'],
},
fallbackLng: 'en',
debug: false,
interpolation: {
prefix: "[[",
suffix: "]]"
},
backend: {
loadPath: '/i18n/[[lng]].json'
}
}, function (err, _t) {
if (err) {
return console.error(err);
}
localize = locI18next.init(i18next);
{% block i18n %}{% endblock i18n %}
});
</script>
</html>

View file

@ -4,7 +4,7 @@
<body class=" border-top-wide border-primary d-flex flex-column">
<script src="/js/demo-theme.min.js"></script>
<div class="page page-center">
<div class="container-tight py-4">
<div class="container-tight py-4" id="error_page">
<div class="empty">
<div class="empty-header">{{ http_code }}</div>
<p class="empty-title">{{ http_reason }}</p>
@ -14,7 +14,7 @@
<div class="empty-action">
<a href="/" class="btn btn-primary">
{% include "icons/arrow-left" %}
Take me home
<label data-i18n="error.go_home"></label>
</a>
</div>
</div>
@ -26,3 +26,7 @@
<script src="/js/demo.min.js" defer></script>
</body>
{% endblock page %}
{% block i18n %}
localize("#error_page");
{% endblock i18n %}

View file

@ -16,66 +16,55 @@
</div>
{% endif %}
<div class="card">
<h2 class="card-title text-center my-4 h2">Reset your password</h2>
<div class="card" id="forgot_password_card">
<h2 class="card-title text-center my-4 h2" data-i18n="forgot_password.title"></h2>
<div class="card-header">
<ul class="nav nav-tabs card-header-tabs nav-fill" data-bs-toggle="tabs">
<li class="nav-item">
<a href="#tabs-email" class="nav-link active" data-bs-toggle="tab">Email</a>
<a href="#tabs-email" class="nav-link active" data-bs-toggle="tab" data-i18n="forgot_password.email"></a>
</li>
<li class="nav-item">
<a href="#tabs-paper-key" class="nav-link" data-bs-toggle="tab">Paper key</a>
<a href="#tabs-paper-key" class="nav-link" data-bs-toggle="tab" data-i18n="forgot_password.paper_key"></a>
</li>
</ul>
</div>
<div class="card-body">
<div class="tab-content">
<div class="tab-pane active show" id="tabs-email">
<p class="mt-2 mb-4">
Enter your email address linked to your account. We will email you a link to reset your
password.
</p>
<p class="mt-2 mb-4" data-i18n="forgot_password.email_description"></p>
<form class="mb-2" action="/forgot-password/email" method="post" autocomplete="off"
novalidate>
<div class="mb-3">
<label class="form-label required" for="email">Email address</label>
<input id="email" type="email" name="email" class="form-control"
placeholder="Enter email"
required>
<label class="form-label required" for="email" data-i18n="forgot_password.email_address"></label>
<input id="email" type="email" name="email" class="form-control" required>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">
{% include "icons/mail" %}
Request password reset
<label data-i18n="forgot_password.request"></label>
</button>
</div>
</form>
</div>
<div class="tab-pane" id="tabs-paper-key">
<p class="mt-2 mb-4">
Enter your login linked to your account, with your paper key.
</p>
<p class="mt-2 mb-4" data-i18n="forgot_password.paper_key_description"></p>
<form class="mb-2" action="/forgot-password/paper-key" method="post" autocomplete="off"
novalidate>
<div class="mb-3">
<label class="form-label required" for="login">Login</label>
<input id="login" type="text" name="login" class="form-control"
placeholder="Email or username"
required>
<label class="form-label required" for="login" data-i18n="forgot_password.login"></label>
<input id="login" type="text" name="login" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label required" for="paper_key">Paper key</label>
<input id="paper_key" type="text" name="paper_key" class="form-control"
placeholder="Enter your paper key"
required>
<label class="form-label required" for="paper_key" data-i18n="forgot_password.paper_key"></label>
<input id="paper_key" type="text" name="paper_key" class="form-control" required>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">
{% include "icons/paperclip" %}
Request password reset
<label data-i18n="forgot_password.request"></label>
</button>
</div>
</form>
@ -94,3 +83,7 @@
<script src="/js/demo.min.js" defer></script>
</body>
{% endblock page %}
{% block i18n %}
localize("#forgot_password_card");
{% endblock i18n %}

View file

@ -2,3 +2,7 @@
{% block content %}
{% endblock content %}
{% block i18n %}
localize("#header_user_nav");
{% endblock i18n %}

View file

@ -8,7 +8,7 @@
<script src="/js/demo-theme.min.js"></script>
<div>
<div class="min-vh-100 d-flex flex-column justify-content-between">
<div class="container container-tight py-4">
<div class="container container-tight py-4" id="authorize_page">
<div class="text-center mb-4">
{% include "utils/logo" %}
</div>
@ -22,8 +22,8 @@
<div class="card card-md">
<div class="card-body">
<div class="text-center mb-2">
<h2 class="h2">Access {{ app_name }}</h2>
<p class="text-muted">With your {{ business_name }} account</p>
<h2 class="h2" data-i18n="authorize.access_app"></h2>
<p class="text-muted" data-i18n="authorize.with_account"></p>
</div>
<form id="authorize_form" action="" method="post" autocomplete="off" novalidate class="mt-4">
{% if user %}
@ -39,23 +39,19 @@
</div>
{% else %}
<div class="mb-3">
<label class="form-label" for="login">Login</label>
<input id="login" name="login" type="text" class="form-control"
placeholder="Email or username"
autocomplete="off">
<label class="form-label" for="login" data-i18n="authorize.login"></label>
<input id="login" name="login" type="text" class="form-control" autocomplete="off">
</div>
<div class="mb-2">
<label class="form-label" for="password">Password</label>
<label class="form-label" for="password" data-i18n="authorize.password"></label>
<div class="input-group input-group-flat">
<input id="password" name="password" type="password" class="form-control"
placeholder="Your password"
autocomplete="off">
<input id="password" name="password" type="password" class="form-control" autocomplete="off">
</div>
</div>
{% endif %}
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">Authorize</button>
<button type="submit" class="btn btn-primary w-100" data-i18n="authorize.authorize"></button>
</div>
</form>
</div>
@ -63,7 +59,7 @@
{% if user %}
{% else %}
<div class="text-center text-muted mt-3">
<a href="/forgot-password" tabindex="-1">Forgot your password?</a>
<a href="/forgot-password" tabindex="-1" data-i18n="authorize.forgot_password"></a>
</div>
{% endif %}
</div>
@ -79,3 +75,10 @@
</body>
{% endblock page %}
{% block i18n %}
localize("#authorize_page", {
'app_name': '{{app_name}}',
'business_name': '{{business_name}}',
});
{% endblock i18n %}

View file

@ -8,15 +8,15 @@
<div>
<div class="min-vh-100 d-flex flex-column justify-content-between">
<div class="page page-center">
<div class="container container-tight py-4">
<div class="container container-tight py-4" id="redirect_page">
<div class="text-center mb-4">
{% include "utils/logo" %}
</div>
<div class="card card-md">
<div class="card-body text-center">
<div class="mb-4">
<h2 class="card-title">Hello!</h2>
<p class="text-muted">Preparing application</p>
<h2 class="card-title" data-i18n="redirect.hello"></h2>
<p class="text-muted" data-i18n="redirect.preparing_app"></p>
</div>
<div class="mb-4">
{{ user::avatar(username=username, name=name, size="xl", css="mb-3") }}
@ -34,7 +34,7 @@
</div>
</div>
<div class="text-center text-muted mt-3">
<a href="{{ home_page }}" tabindex="-1">Click here if you are not redirected</a>
<a href="{{ home_page }}" tabindex="-1" data-i18n="redirect.not_redirected"></a>
<meta http-equiv="refresh" content="2; url={{ home_page }}">
</div>
</div>
@ -48,3 +48,7 @@
<script src="/js/demo.min.js" defer></script>
</body>
{% endblock page %}
{% block i18n %}
localize("#redirect_page");
{% endblock i18n %}

View file

@ -19,11 +19,11 @@
</div>
{% endif %}
<div class="card card-md">
<div class="card card-md" id="totp_page">
<div class="card-body">
<div class="text-center mb-4">
<div class="mb-4">
<h2 class="card-title">Verify your account</h2>
<h2 class="card-title" data-i18n="totp.verify_account"></h2>
</div>
<div class="mb-4">
{{ user::avatar(username=username, name=name, size="lg", css="mb-3") }}
@ -38,9 +38,7 @@
</div>
<form id="totp_form" action="" method="post" autocomplete="off" novalidate class="mt-4">
<div class="mb-3">
<label class="form-label required" for="code">
Enter the code displayed on your device
</label>
<label class="form-label required" for="code" data-i18n="totp.enter_code"></label>
<input
class="form-control"
type="text"
@ -54,7 +52,7 @@
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">Verify code</button>
<button type="submit" class="btn btn-primary w-100" data-i18n="totp.verify_code"></button>
</div>
</form>
</div>
@ -72,3 +70,7 @@
</body>
{% endblock page %}
{% block i18n %}
localize("#totp_page");
{% endblock i18n %}

View file

@ -16,27 +16,25 @@
</div>
{% endif %}
<div class="card">
<h2 class="card-title text-center my-4 h2">Reset your password</h2>
<div class="card" id="reset_password_card">
<h2 class="card-title text-center my-4 h2" data-i18n="reset_password.title"></h2>
<div class="card-body">
<p class="mb-4 text-center">Resetting password for <code>{{ username }}</code></p>
<form class="mb-2" action="" method="post" autocomplete="off" novalidate>
<div class="mb-3">
<label class="form-label required" for="password">New password</label>
<input name="password" id="password" type="password" class="form-control" placeholder="Enter new password" required>
<label class="form-label required" for="password" data-i18n="reset_password.new_password"></label>
<input name="password" id="password" type="password" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label required" for="confirm_password">Confirm new password</label>
<input name="confirm_password" id="confirm_password" type="password" class="form-control" placeholder="Confirm new password" required>
<label class="form-label required" for="confirm_password" data-i18n="reset_password.confirm_password"></label>
<input name="confirm_password" id="confirm_password" type="password" class="form-control" required>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary w-100">
{% include "icons/password" %}
Set new password
<label data-i18n="reset_password.set_password"></label>
</button>
</div>
</form>
@ -54,3 +52,7 @@
<script src="/js/demo.min.js" defer></script>
</body>
{% endblock page %}
{% block i18n %}
localize("#reset_password_card");
{% endblock i18n %}

View file

@ -2,13 +2,11 @@
{% block content %}
<!-- Page header -->
<div class="page-header d-print-none">
<div class="page-header d-print-none" id="settings_header">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
<h2 class="page-title" data-i18n="user_settings.settings"></h2>
</div>
</div>
</div>
@ -23,41 +21,41 @@
</div>
{% endif %}
<div class="card">
<div class="card" id="settings_personal_page">
<div class="row g-0">
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<div class="list-group list-group-transparent">
<a href="./personal"
class="list-group-item list-group-item-action d-flex align-items-center active">Personal</a>
class="list-group-item list-group-item-action d-flex align-items-center active"
data-i18n="user_settings.personal"></a>
<a href="./security"
class="list-group-item list-group-item-action d-flex align-items-center">Security</a>
class="list-group-item list-group-item-action d-flex align-items-center"
data-i18n="user_settings.security"></a>
<a href="./visual"
class="list-group-item list-group-item-action d-flex align-items-center">Visual</a>
class="list-group-item list-group-item-action d-flex align-items-center"
data-i18n="user_settings.visual"></a>
</div>
</div>
</div>
<div class="col d-flex flex-column">
<form action="" method="post">
<div class="card-header">
<h3 class="card-title">My Profile</h3>
<h3 class="card-title" data-i18n="user_settings_personal.my_profile"></h3>
</div>
<div class="card-body">
<!-- Username -->
<label class="form-label required" for="username">Username</label>
<label class="form-label required" for="username" data-i18n="user_settings_personal.username"></label>
<div class="input-icon mb-3">
<span class="input-icon-addon">
{% include "icons/id-badge-2" %}
</span>
<input name="username" id="username" value="{{ username }}" type="text"
placeholder="Enter a username"
class="form-control"
required>
<input name="username" id="username" value="{{ username }}" type="text" class="form-control" required>
</div>
<!-- Name -->
<label class="form-label" for="name">Full Name</label>
<label class="form-label" for="name" data-i18n="user_settings_personal.full_name"></label>
<div class="input-icon mb-3">
<span class="input-icon-addon">
{% include "icons/user" %}
@ -68,7 +66,7 @@
</div>
<!-- Email -->
<label class="form-label" for="email">Email address</label>
<label class="form-label" for="email" data-i18n="user_settings_personal.email"></label>
<div class="input-icon mb-3">
<span class="input-icon-addon">
{% include "icons/at" %}
@ -79,12 +77,11 @@
</div>
<!-- Timezone -->
<label class="form-label" for="select-timezone">Timezone</label>
<label class="form-label" for="select-timezone" data-i18n="user_settings_personal.timezone"></label>
<div class="mb-3">
<select type="text" class="form-select" placeholder="Search your nearest city..."
id="select-timezone" name="timezone" autocomplete="off">
<select type="text" class="form-select" id="select-timezone" name="timezone" autocomplete="off">
<option value="UTC">UTC (Default)</option>
<option value="UTC" data-i18n="user_settings_personal.utc_default"></option>
{% for tz in list_timezones %}
{% if timezone == tz %}
@ -98,11 +95,11 @@
</select>
</div>
<p class="mt-4 text-muted">Profile last updated on {{ updated_at | date(format="%A %-d %B %Y, %T", timezone=user.zoneinfo | default(value="UTC")) }}</p>
<p class="mt-4 text-muted"><span data-i18n="user_settings_personal.last_updated"></span> {{ updated_at | date(format="%Y-%m-%d, %T", timezone=user.zoneinfo | default(value="UTC")) }}</p>
</div>
<div class="card-footer text-end">
<button type="submit" class="btn btn-primary">Save</button>
<button type="submit" class="btn btn-primary" data-i18n="user_settings_personal.save"></button>
</div>
</form>
@ -146,3 +143,9 @@
});
</script>
{% endblock additional_js %}
{% block i18n %}
localize("#header_user_nav");
localize("#settings_header");
localize("#settings_personal_page");
{% endblock i18n %}

View file

@ -2,13 +2,11 @@
{% block content %}
<!-- Page header -->
<div class="page-header d-print-none">
<div class="page-header d-print-none" id="settings_header">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
<h2 class="page-title" data-i18n="user_settings.settings"></h2>
</div>
</div>
</div>
@ -23,17 +21,20 @@
</div>
{% endif %}
<div class="card">
<div class="card" id="settings_security_page">
<div class="row g-0">
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<div class="list-group list-group-transparent">
<a href="./personal"
class="list-group-item list-group-item-action d-flex align-items-center">Personal</a>
class="list-group-item list-group-item-action d-flex align-items-center"
data-i18n="user_settings.personal"></a>
<a href="./security"
class="list-group-item list-group-item-action d-flex align-items-center active">Security</a>
class="list-group-item list-group-item-action d-flex align-items-center active"
data-i18n="user_settings.security"></a>
<a href="./visual"
class="list-group-item list-group-item-action d-flex align-items-center">Visual</a>
class="list-group-item list-group-item-action d-flex align-items-center"
data-i18n="user_settings.visual"></a>
</div>
</div>
</div>
@ -329,3 +330,9 @@
</div>
{% endblock content %}
{% block i18n %}
localize("#header_user_nav");
localize("#settings_header");
localize("#settings_security_page");
{% endblock i18n %}

View file

@ -2,13 +2,11 @@
{% block content %}
<!-- Page header -->
<div class="page-header d-print-none">
<div class="page-header d-print-none" id="settings_header">
<div class="container-xl">
<div class="row g-2 align-items-center">
<div class="col">
<h2 class="page-title">
Settings
</h2>
<h2 class="page-title" data-i18n="user_settings.settings"></h2>
</div>
</div>
</div>
@ -23,32 +21,35 @@
</div>
{% endif %}
<div class="card">
<div class="card" id="settings_visual_page">
<div class="row g-0">
<div class="col-3 d-none d-md-block border-end">
<div class="card-body">
<div class="list-group list-group-transparent">
<a href="./personal"
class="list-group-item list-group-item-action d-flex align-items-center">Personal</a>
class="list-group-item list-group-item-action d-flex align-items-center"
data-i18n="user_settings.personal"></a>
<a href="./security"
class="list-group-item list-group-item-action d-flex align-items-center">Security</a>
class="list-group-item list-group-item-action d-flex align-items-center"
data-i18n="user_settings.security"></a>
<a href="./visual"
class="list-group-item list-group-item-action d-flex align-items-center active">Visual</a>
class="list-group-item list-group-item-action d-flex align-items-center active"
data-i18n="user_settings.visual"></a>
</div>
</div>
</div>
<div class="col d-flex flex-column">
<div class="card-body">
<h2 class="mb-4">Visual</h2>
<h2 class="mb-4" data-i18n="user_settings.visual"></h2>
<a href="?theme=dark" class="btn hide-theme-dark">
{% include "icons/moon" %}
Enable dark mode
<span data-i18n="user_settings_visual.enable_dark"></span>
</a>
<a href="?theme=light" class="btn hide-theme-light">
{% include "icons/sun" %}
Enable light mode
<span data-i18n="user_settings_visual.enable_light"></span>
</a>
</div>
@ -59,3 +60,9 @@
</div>
</div>
{% endblock content %}
{% block i18n %}
localize("#header_user_nav");
localize("#settings_header");
localize("#settings_visual_page");
{% endblock i18n %}

View file

@ -15,35 +15,33 @@
</div>
{% endif %}
<form action="/setup" method="post">
<form action="/setup" method="post" id="setup_form">
<div class="card card-md">
<div class="card-body text-center py-4 p-sm-5">
<h1 class="">Welcome to Ezidam!</h1>
<p class="text-muted">Initial setup</p>
<h1 data-i18n="setup.welcome"></h1>
<p class="text-muted" data-i18n="setup.initial_setup"></p>
</div>
<!-- First admin account -->
<div class="hr-text hr-text-center hr-text-spaceless">first admin account</div>
<div class="hr-text hr-text-center hr-text-spaceless" data-i18n="setup.first_admin_account"></div>
<div class="card-body">
<div class="mb-3">
<label class="form-label required" for="username">Username</label>
<input name="username" id="username" type="text" placeholder="Enter a username"
class="form-control" required>
<label class="form-label required" for="username" data-i18n="setup.username"></label>
<input name="username" id="username" type="text" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label required" for="password">Password</label>
<label class="form-label required" for="password" data-i18n="setup.password"></label>
<div class="input-group input-group-flat">
<input name="password" id="password" type="password" placeholder="Enter password"
class="form-control" autocomplete="off" required>
<input name="password" id="password" type="password" class="form-control" autocomplete="off" required>
</div>
</div>
</div>
<!-- Settings -->
<div class="hr-text hr-text-center hr-text-spaceless">settings</div>
<div class="hr-text hr-text-center hr-text-spaceless" data-i18n="setup.settings"></div>
<div class="card-body">
<div class="mb-3">
<label class="form-label required" for="url">Base URL</label>
<label class="form-label required" for="url" data-i18n="setup.base_url"></label>
<input name="url" id="url" type="url" placeholder="https://example.com" class="form-control"
required>
</div>
@ -52,7 +50,7 @@
<div class="row align-items-center mt-3">
<div class="col">
<div class="btn-list justify-content-end">
<button type="submit" class="btn btn-primary">Finish setup</button>
<button type="submit" class="btn btn-primary" data-i18n="setup.finish"></button>
</div>
</div>
</div>
@ -66,3 +64,7 @@
<script src="/js/demo.min.js" defer></script>
</body>
{% endblock page %}
{% block i18n %}
localize("#setup_form");
{% endblock i18n %}

View file

@ -16,7 +16,7 @@
{{ user::user_info(name=user.name, username=user.username, email=user.email) }}
</div>
</a>
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow">
<div class="dropdown-menu dropdown-menu-end dropdown-menu-arrow" id="header_user_nav">
<div class="d-xl-none">
<div class="dropdown-item-text">
{{ user::user_info(name=user.name, username=user.username, email=user.email) }}
@ -24,13 +24,13 @@
<div class="dropdown-divider"></div>
</div>
<a href="/settings" class="dropdown-item">Settings</a>
<a href="/settings" class="dropdown-item" data-i18n="header.settings"></a>
{% if user.isAdmin == true %}
<a href="/admin" class="dropdown-item">Admin panel</a>
<a href="/admin" class="dropdown-item" data-i18n="header.admin_panel"></a>
{% endif %}
<div class="dropdown-divider"></div>
<form action="/logout" method="post">
<button type="submit" class="dropdown-item">Logout</button>
<button type="submit" class="dropdown-item" data-i18n="header.logout"></button>
</form>
</div>
</div>