Compare commits
4 Commits
284d805590
...
2435e25aee
| Author | SHA1 | Date | |
|---|---|---|---|
| 2435e25aee | |||
| d74531e2e1 | |||
| 8bf6de1682 | |||
| 0f57799aaf |
11
backend/migrations/20250829140914_create_admin_user.sql
Normal file
11
backend/migrations/20250829140914_create_admin_user.sql
Normal 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
|
||||||
|
);
|
||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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'),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user