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 grant_type: GrantType,
pub code: Option<&'r str>, pub code: Option<&'r str>,
pub redirect_uri: 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 client_secret: Option<&'r str>,
pub scope: Option<&'r str>, pub scope: Option<&'r str>,
pub refresh_token: Option<&'r str>, pub refresh_token: Option<&'r str>,

View file

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

View file

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

View file

@ -12,13 +12,11 @@ pub async fn reset_password_page(
return Err(Error::bad_request("Reset password token has expired")); 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? .await?
.ok_or_else(|| Error::not_found("Invalid or expired token"))?; .ok_or_else(|| Error::not_found("Invalid or expired token"))?;
let page = Page::ResetPassword(super::content::ResetPassword { let page = Page::ResetPassword(super::content::ResetPassword {});
username: user.username().into(),
});
Ok(flash Ok(flash
.map(|flash| Page::with_flash(page.clone(), 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> <!doctype html>
<html lang="en"> <html>
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"/> <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.min.css" rel="stylesheet"/>
<link href="/css/tabler-vendors.min.css" rel="stylesheet"/> <link href="/css/tabler-vendors.min.css" rel="stylesheet"/>
<link href="/css/demo.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 --> <!-- Favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
@ -27,5 +32,34 @@
} }
</style> </style>
</head> </head>
{% block page %}{% endblock page %} {% 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> </html>

View file

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

View file

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

View file

@ -2,3 +2,7 @@
{% block content %} {% block content %}
{% endblock 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> <script src="/js/demo-theme.min.js"></script>
<div> <div>
<div class="min-vh-100 d-flex flex-column justify-content-between"> <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"> <div class="text-center mb-4">
{% include "utils/logo" %} {% include "utils/logo" %}
</div> </div>
@ -22,8 +22,8 @@
<div class="card card-md"> <div class="card card-md">
<div class="card-body"> <div class="card-body">
<div class="text-center mb-2"> <div class="text-center mb-2">
<h2 class="h2">Access {{ app_name }}</h2> <h2 class="h2" data-i18n="authorize.access_app"></h2>
<p class="text-muted">With your {{ business_name }} account</p> <p class="text-muted" data-i18n="authorize.with_account"></p>
</div> </div>
<form id="authorize_form" action="" method="post" autocomplete="off" novalidate class="mt-4"> <form id="authorize_form" action="" method="post" autocomplete="off" novalidate class="mt-4">
{% if user %} {% if user %}
@ -39,23 +39,19 @@
</div> </div>
{% else %} {% else %}
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="login">Login</label> <label class="form-label" for="login" data-i18n="authorize.login"></label>
<input id="login" name="login" type="text" class="form-control" <input id="login" name="login" type="text" class="form-control" autocomplete="off">
placeholder="Email or username"
autocomplete="off">
</div> </div>
<div class="mb-2"> <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"> <div class="input-group input-group-flat">
<input id="password" name="password" type="password" class="form-control" <input id="password" name="password" type="password" class="form-control" autocomplete="off">
placeholder="Your password"
autocomplete="off">
</div> </div>
</div> </div>
{% endif %} {% endif %}
<div class="form-footer"> <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> </div>
</form> </form>
</div> </div>
@ -63,7 +59,7 @@
{% if user %} {% if user %}
{% else %} {% else %}
<div class="text-center text-muted mt-3"> <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> </div>
{% endif %} {% endif %}
</div> </div>
@ -79,3 +75,10 @@
</body> </body>
{% endblock page %} {% 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>
<div class="min-vh-100 d-flex flex-column justify-content-between"> <div class="min-vh-100 d-flex flex-column justify-content-between">
<div class="page page-center"> <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"> <div class="text-center mb-4">
{% include "utils/logo" %} {% include "utils/logo" %}
</div> </div>
<div class="card card-md"> <div class="card card-md">
<div class="card-body text-center"> <div class="card-body text-center">
<div class="mb-4"> <div class="mb-4">
<h2 class="card-title">Hello!</h2> <h2 class="card-title" data-i18n="redirect.hello"></h2>
<p class="text-muted">Preparing application</p> <p class="text-muted" data-i18n="redirect.preparing_app"></p>
</div> </div>
<div class="mb-4"> <div class="mb-4">
{{ user::avatar(username=username, name=name, size="xl", css="mb-3") }} {{ user::avatar(username=username, name=name, size="xl", css="mb-3") }}
@ -34,7 +34,7 @@
</div> </div>
</div> </div>
<div class="text-center text-muted mt-3"> <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 }}"> <meta http-equiv="refresh" content="2; url={{ home_page }}">
</div> </div>
</div> </div>
@ -48,3 +48,7 @@
<script src="/js/demo.min.js" defer></script> <script src="/js/demo.min.js" defer></script>
</body> </body>
{% endblock page %} {% endblock page %}
{% block i18n %}
localize("#redirect_page");
{% endblock i18n %}

View file

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

View file

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

View file

@ -2,13 +2,11 @@
{% block content %} {% block content %}
<!-- Page header --> <!-- 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="container-xl">
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col"> <div class="col">
<h2 class="page-title"> <h2 class="page-title" data-i18n="user_settings.settings"></h2>
Settings
</h2>
</div> </div>
</div> </div>
</div> </div>
@ -23,41 +21,41 @@
</div> </div>
{% endif %} {% endif %}
<div class="card"> <div class="card" id="settings_personal_page">
<div class="row g-0"> <div class="row g-0">
<div class="col-3 d-none d-md-block border-end"> <div class="col-3 d-none d-md-block border-end">
<div class="card-body"> <div class="card-body">
<div class="list-group list-group-transparent"> <div class="list-group list-group-transparent">
<a href="./personal" <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" <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" <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>
</div> </div>
<div class="col d-flex flex-column"> <div class="col d-flex flex-column">
<form action="" method="post"> <form action="" method="post">
<div class="card-header"> <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>
<div class="card-body"> <div class="card-body">
<!-- Username --> <!-- 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"> <div class="input-icon mb-3">
<span class="input-icon-addon"> <span class="input-icon-addon">
{% include "icons/id-badge-2" %} {% include "icons/id-badge-2" %}
</span> </span>
<input name="username" id="username" value="{{ username }}" type="text" <input name="username" id="username" value="{{ username }}" type="text" class="form-control" required>
placeholder="Enter a username"
class="form-control"
required>
</div> </div>
<!-- Name --> <!-- 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"> <div class="input-icon mb-3">
<span class="input-icon-addon"> <span class="input-icon-addon">
{% include "icons/user" %} {% include "icons/user" %}
@ -68,7 +66,7 @@
</div> </div>
<!-- Email --> <!-- 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"> <div class="input-icon mb-3">
<span class="input-icon-addon"> <span class="input-icon-addon">
{% include "icons/at" %} {% include "icons/at" %}
@ -79,12 +77,11 @@
</div> </div>
<!-- Timezone --> <!-- 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"> <div class="mb-3">
<select type="text" class="form-select" placeholder="Search your nearest city..." <select type="text" class="form-select" id="select-timezone" name="timezone" autocomplete="off">
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 %} {% for tz in list_timezones %}
{% if timezone == tz %} {% if timezone == tz %}
@ -98,11 +95,11 @@
</select> </select>
</div> </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>
<div class="card-footer text-end"> <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> </div>
</form> </form>
@ -146,3 +143,9 @@
}); });
</script> </script>
{% endblock additional_js %} {% 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 %} {% block content %}
<!-- Page header --> <!-- 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="container-xl">
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col"> <div class="col">
<h2 class="page-title"> <h2 class="page-title" data-i18n="user_settings.settings"></h2>
Settings
</h2>
</div> </div>
</div> </div>
</div> </div>
@ -23,17 +21,20 @@
</div> </div>
{% endif %} {% endif %}
<div class="card"> <div class="card" id="settings_security_page">
<div class="row g-0"> <div class="row g-0">
<div class="col-3 d-none d-md-block border-end"> <div class="col-3 d-none d-md-block border-end">
<div class="card-body"> <div class="card-body">
<div class="list-group list-group-transparent"> <div class="list-group list-group-transparent">
<a href="./personal" <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" <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" <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>
</div> </div>
@ -329,3 +330,9 @@
</div> </div>
{% endblock content %} {% 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 %} {% block content %}
<!-- Page header --> <!-- 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="container-xl">
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col"> <div class="col">
<h2 class="page-title"> <h2 class="page-title" data-i18n="user_settings.settings"></h2>
Settings
</h2>
</div> </div>
</div> </div>
</div> </div>
@ -23,32 +21,35 @@
</div> </div>
{% endif %} {% endif %}
<div class="card"> <div class="card" id="settings_visual_page">
<div class="row g-0"> <div class="row g-0">
<div class="col-3 d-none d-md-block border-end"> <div class="col-3 d-none d-md-block border-end">
<div class="card-body"> <div class="card-body">
<div class="list-group list-group-transparent"> <div class="list-group list-group-transparent">
<a href="./personal" <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" <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" <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>
</div> </div>
<div class="col d-flex flex-column"> <div class="col d-flex flex-column">
<div class="card-body"> <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"> <a href="?theme=dark" class="btn hide-theme-dark">
{% include "icons/moon" %} {% include "icons/moon" %}
Enable dark mode <span data-i18n="user_settings_visual.enable_dark"></span>
</a> </a>
<a href="?theme=light" class="btn hide-theme-light"> <a href="?theme=light" class="btn hide-theme-light">
{% include "icons/sun" %} {% include "icons/sun" %}
Enable light mode <span data-i18n="user_settings_visual.enable_light"></span>
</a> </a>
</div> </div>
@ -59,3 +60,9 @@
</div> </div>
</div> </div>
{% endblock content %} {% endblock content %}
{% block i18n %}
localize("#header_user_nav");
localize("#settings_header");
localize("#settings_visual_page");
{% endblock i18n %}

View file

@ -15,35 +15,33 @@
</div> </div>
{% endif %} {% endif %}
<form action="/setup" method="post"> <form action="/setup" method="post" id="setup_form">
<div class="card card-md"> <div class="card card-md">
<div class="card-body text-center py-4 p-sm-5"> <div class="card-body text-center py-4 p-sm-5">
<h1 class="">Welcome to Ezidam!</h1> <h1 data-i18n="setup.welcome"></h1>
<p class="text-muted">Initial setup</p> <p class="text-muted" data-i18n="setup.initial_setup"></p>
</div> </div>
<!-- First admin account --> <!-- 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="card-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label required" for="username">Username</label> <label class="form-label required" for="username" data-i18n="setup.username"></label>
<input name="username" id="username" type="text" placeholder="Enter a username" <input name="username" id="username" type="text" class="form-control" required>
class="form-control" required>
</div> </div>
<div class="mb-3"> <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"> <div class="input-group input-group-flat">
<input name="password" id="password" type="password" placeholder="Enter password" <input name="password" id="password" type="password" class="form-control" autocomplete="off" required>
class="form-control" autocomplete="off" required>
</div> </div>
</div> </div>
</div> </div>
<!-- Settings --> <!-- 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="card-body">
<div class="mb-3"> <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" <input name="url" id="url" type="url" placeholder="https://example.com" class="form-control"
required> required>
</div> </div>
@ -52,7 +50,7 @@
<div class="row align-items-center mt-3"> <div class="row align-items-center mt-3">
<div class="col"> <div class="col">
<div class="btn-list justify-content-end"> <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> </div>
</div> </div>
@ -66,3 +64,7 @@
<script src="/js/demo.min.js" defer></script> <script src="/js/demo.min.js" defer></script>
</body> </body>
{% endblock page %} {% 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) }} {{ user::user_info(name=user.name, username=user.username, email=user.email) }}
</div> </div>
</a> </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="d-xl-none">
<div class="dropdown-item-text"> <div class="dropdown-item-text">
{{ user::user_info(name=user.name, username=user.username, email=user.email) }} {{ user::user_info(name=user.name, username=user.username, email=user.email) }}
@ -24,13 +24,13 @@
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
</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 %} {% 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 %} {% endif %}
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<form action="/logout" method="post"> <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> </form>
</div> </div>
</div> </div>