Compare commits

..

4 Commits

Author SHA1 Message Date
409
2435e25aee create admin user on first start 2025-08-29 16:17:33 +02:00
409
d74531e2e1 AUTH_ALLOW_REGISTRATION env variable 2025-08-29 16:17:13 +02:00
409
8bf6de1682 reuse register toast id 2025-08-29 16:16:57 +02:00
409
0f57799aaf fix frontend using different email validation 2025-08-29 16:16:45 +02:00
6 changed files with 54 additions and 3 deletions

View File

@@ -0,0 +1,11 @@
INSERT INTO users (
name,
email,
hash,
admin
) VALUES (
'admin',
'admin@example.com',
'$argon2id$v=19$m=19456,t=2,p=1$H1WsElL4921/WD5oPkY7JQ$aHudNG8z0ns3pRULfuDpuEkxPUbGxq9AHC4QGyt5odc',
true
);

View File

@@ -43,6 +43,8 @@ impl From<RegisterUserRequest> for CreateUserRequest {
pub enum RegisterUserError { pub enum RegisterUserError {
#[error(transparent)] #[error(transparent)]
CreateUser(#[from] CreateUserError), CreateUser(#[from] CreateUserError),
#[error("Registration is disabled")]
Disabled,
#[error(transparent)] #[error(transparent)]
Unknown(#[from] anyhow::Error), Unknown(#[from] anyhow::Error),
} }

View File

@@ -52,6 +52,7 @@ use crate::{
}; };
const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION"; const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION";
const ALLOW_REGISTRATION_KEY: &str = "AUTH_ALLOW_REGISTRATION";
/// The authentication service configuration /// The authentication service configuration
/// ///
@@ -59,6 +60,7 @@ const AUTH_SESSION_EXPIRATION_KEY: &str = "AUTH_SESSION_EXPIRATION";
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct AuthConfig { pub struct AuthConfig {
session_lifetime: SessionExpirationTime, session_lifetime: SessionExpirationTime,
allow_registration: bool,
} }
impl AuthConfig { impl AuthConfig {
@@ -71,12 +73,27 @@ impl AuthConfig {
} }
}; };
Ok(Self { session_lifetime }) let allow_registration = match Config::load_env(ALLOW_REGISTRATION_KEY)
.map(|v| v.to_lowercase())
.as_deref()
{
Ok("true") => true,
Ok("false") | Ok(_) | Err(_) => false,
};
Ok(Self {
session_lifetime,
allow_registration,
})
} }
pub fn session_lifetime(&self) -> SessionExpirationTime { pub fn session_lifetime(&self) -> SessionExpirationTime {
self.session_lifetime self.session_lifetime
} }
pub fn allow_registration(&self) -> bool {
self.allow_registration
}
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -232,6 +249,11 @@ where
} }
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> { async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
if !self.config.allow_registration {
self.metrics.record_user_registration_failure().await;
return Err(RegisterUserError::Disabled);
}
let result = self.repository.create_user(request.into()).await; let result = self.repository.create_user(request.into()).await;
if let Ok(user) = result.as_ref() { if let Ok(user) = result.as_ref() {

View File

@@ -112,6 +112,9 @@ impl From<RegisterUserError> for ApiError {
fn from(value: RegisterUserError) -> Self { fn from(value: RegisterUserError) -> Self {
match value { match value {
RegisterUserError::CreateUser(err) => err.into(), RegisterUserError::CreateUser(err) => err.into(),
RegisterUserError::Disabled => {
Self::BadRequest("User registration is disabled".to_string())
}
RegisterUserError::Unknown(error) => Self::InternalServerError(error.to_string()), RegisterUserError::Unknown(error) => Self::InternalServerError(error.to_string()),
} }
} }

View File

@@ -1,6 +1,8 @@
import { toast } from 'vue-sonner'; import { toast } from 'vue-sonner';
import type { ApiResponse } from '#shared/types/api'; import type { ApiResponse } from '#shared/types/api';
const REGISTER_TOAST_ID = 'REGISTER_USER_TOAST' as const;
export async function registerUser( export async function registerUser(
username: string, username: string,
email: string, email: string,
@@ -24,6 +26,7 @@ export async function registerUser(
if (data.value == null) { if (data.value == null) {
toast.error('Register', { toast.error('Register', {
id: REGISTER_TOAST_ID,
description: error.value?.data ?? 'Failed to register', description: error.value?.data ?? 'Failed to register',
}); });
@@ -33,6 +36,7 @@ export async function registerUser(
} }
toast.success('Register', { toast.success('Register', {
id: REGISTER_TOAST_ID,
description: `Successfully registered user ${username}`, description: `Successfully registered user ${username}`,
}); });

View File

@@ -1,13 +1,22 @@
import { object, string } from 'yup'; import { object, string } from 'yup';
const EMAIL_REGEX: RegExp =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
export const registerSchema = object({ export const registerSchema = object({
name: string().trim().min(1).required('required'), name: string().trim().min(1).required('required'),
email: string().trim().email('Expected a valid email').required('required'), email: string()
.trim()
.matches(EMAIL_REGEX, 'Expected a valid email')
.required('required'),
password: string().trim().min(12).max(32).required('required'), password: string().trim().min(12).max(32).required('required'),
}); });
export const loginSchema = object({ export const loginSchema = object({
email: string().trim().email('Expected a valid email').required('required'), email: string()
.trim()
.matches(EMAIL_REGEX, 'Expected a valid email')
.required('required'),
// Don't include the min and max here to let bad actors waste their time // Don't include the min and max here to let bad actors waste their time
password: string().trim().required('required'), password: string().trim().required('required'),
}); });