Convert static environment to dynamic, theme toggle

This commit is contained in:
Evert Prants 2024-06-07 18:46:49 +03:00
parent 6febe18daa
commit adbc143926
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
43 changed files with 257 additions and 126 deletions

View File

@ -3,10 +3,7 @@ FROM node:20 AS builder
WORKDIR /usr/src/app WORKDIR /usr/src/app
ARG envFile=.env
COPY . . COPY . .
COPY ./${envFile} ./.env
RUN npm ci RUN npm ci
RUN npm run build RUN npm run build

View File

@ -1,4 +1,4 @@
import { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE } from '$env/static/private'; import { env } from '$env/dynamic/private';
import { csrf } from '$lib/server/csrf'; import { csrf } from '$lib/server/csrf';
import { DB } from '$lib/server/drizzle'; import { DB } from '$lib/server/drizzle';
import { runSeeds } from '$lib/server/drizzle/seeds'; import { runSeeds } from '$lib/server/drizzle/seeds';
@ -7,6 +7,8 @@ import { sequence } from '@sveltejs/kit/hooks';
import { migrate } from 'drizzle-orm/mysql2/migrator'; import { migrate } from 'drizzle-orm/mysql2/migrator';
import { handleSession } from 'svelte-kit-cookie-session'; import { handleSession } from 'svelte-kit-cookie-session';
const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE } = env;
await DB.init(); await DB.init();
await JWT.init(); await JWT.init();

View File

@ -0,0 +1,12 @@
<script lang="ts">
import { setThemeMode, themeMode } from '$lib/theme-mode';
import Button from './Button.svelte';
import Icon from './icons/Icon.svelte';
const toggleMode = () => setThemeMode($themeMode === 'dark' ? 'light' : 'dark');
$: iconName = $themeMode === 'light' ? 'DarkMode' : 'LightMode';
</script>
<Button variant="link" on:click={toggleMode}>
<Icon icon={iconName} />
</Button>

View File

@ -1,14 +1,18 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import type { UserSession } from '$lib/types'; import type { UserSession } from '$lib/types';
import ThemeButton from '../ThemeButton.svelte';
export let user: UserSession; export let user: UserSession;
</script> </script>
<header class="admin-header"> <header class="admin-header">
<a class="site-name" href="/">{PUBLIC_SITE_NAME}</a> <a class="site-name" href="/">{env.PUBLIC_SITE_NAME}</a>
<div class="admin-user"> <div class="aside">
<img class="admin-user-avatar" src={`/api/avatar/${user.uuid}`} alt={user.name} /> <div class="admin-user">
<span class="admin-user-name">{user.name}</span> <img class="admin-user-avatar" src={`/api/avatar/${user.uuid}`} alt={user.name} />
<span class="admin-user-name">{user.name}</span>
</div>
<ThemeButton />
</div> </div>
</header> </header>
@ -19,6 +23,15 @@
align-items: center; align-items: center;
padding: 8px 16px; padding: 8px 16px;
background: var(--ina-header-color); background: var(--ina-header-color);
& .aside {
display: flex;
gap: 1rem;
}
}
.admin-header .aside :global(button) {
color: var(--ina-header-link-color);
} }
.admin-user { .admin-user {

View File

@ -9,8 +9,15 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 1.3rem; margin-bottom: 1.3rem;
@media screen and (max-width: 768px) {
flex-direction: column;
gap: 1rem;
}
} }
.title-row > :global(*):first-child { .title-row > :global(*):first-child {
align-self: flex-start;
margin: 0; margin: 0;
} }
</style> </style>

View File

@ -0,0 +1,24 @@
<script lang="ts">
export let icon: string;
$: iconComponent = import(`./svg/${icon}.svelte`);
</script>
<span class="icon">
{#await iconComponent then { default: component }}
<svelte:component this={component} />
{/await}
</span>
<style>
.icon {
width: 1rem;
height: 1rem;
display: inline-block;
vertical-align: middle;
}
.icon :global(svg) {
fill: currentColor;
}
</style>

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"
><path
d="M480-120q-150 0-255-105T120-480q0-150 105-255t255-105q14 0 27.5 1t26.5 3q-41 29-65.5 75.5T444-660q0 90 63 153t153 63q55 0 101-24.5t75-65.5q2 13 3 26.5t1 27.5q0 150-105 255T480-120Zm0-80q88 0 158-48.5T740-375q-20 5-40 8t-40 3q-123 0-209.5-86.5T364-660q0-20 3-40t8-40q-78 32-126.5 102T200-480q0 116 82 198t198 82Zm-10-270Z"
/></svg
>

After

Width:  |  Height:  |  Size: 411 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"
><path
d="M480-360q50 0 85-35t35-85q0-50-35-85t-85-35q-50 0-85 35t-35 85q0 50 35 85t85 35Zm0 80q-83 0-141.5-58.5T280-480q0-83 58.5-141.5T480-680q83 0 141.5 58.5T680-480q0 83-58.5 141.5T480-280ZM200-440H40v-80h160v80Zm720 0H760v-80h160v80ZM440-760v-160h80v160h-80Zm0 720v-160h80v160h-80ZM256-650l-101-97 57-59 96 100-52 56Zm492 496-97-101 53-55 101 97-57 59Zm-98-550 97-101 59 57-100 96-56-52ZM154-212l101-97 55 53-97 101-59-57Zm326-268Z"
/></svg
>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -26,6 +26,7 @@
"passwordSetSuccess": "Your new password has been set successfully! You may now log in.", "passwordSetSuccess": "Your new password has been set successfully! You may now log in.",
"passwordResetSucces": "If there is an account with that email address, we have sent a password reset email to it.", "passwordResetSucces": "If there is an account with that email address, we have sent a password reset email to it.",
"logout": "Log out", "logout": "Log out",
"admin": "Admin",
"avatar": { "avatar": {
"title": "Profile avatar", "title": "Profile avatar",
"change": "Change avatar", "change": "Change avatar",

View File

@ -1,4 +1,4 @@
import { CHALLENGE_SECRET } from '$env/static/private'; import { env } from '$env/dynamic/private';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
@ -54,11 +54,11 @@ export class CryptoUtils {
} }
public static async encryptChallenge<T>(challenge: T): Promise<string> { public static async encryptChallenge<T>(challenge: T): Promise<string> {
return this.encrypt(JSON.stringify(challenge), CHALLENGE_SECRET); return this.encrypt(JSON.stringify(challenge), env.CHALLENGE_SECRET);
} }
public static async decryptChallenge<T>(challenge: string): Promise<T> { public static async decryptChallenge<T>(challenge: string): Promise<T> {
return JSON.parse(this.decrypt(challenge, CHALLENGE_SECRET)); return JSON.parse(this.decrypt(challenge, env.CHALLENGE_SECRET));
} }
static safeCompare(token: string, token2: string) { static safeCompare(token: string, token2: string) {

View File

@ -1,8 +1,10 @@
import { DATABASE_DB, DATABASE_HOST, DATABASE_PASS } from '$env/static/private'; import { env } from '$env/dynamic/private';
import { drizzle } from 'drizzle-orm/mysql2'; import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise'; import mysql from 'mysql2/promise';
import * as schema from './schema'; import * as schema from './schema';
const { DATABASE_DB, DATABASE_HOST, DATABASE_PASS } = env;
export class DB { export class DB {
static mysqlConnection: mysql.Connection; static mysqlConnection: mysql.Connection;
static drizzle: ReturnType<typeof drizzle<typeof schema>>; static drizzle: ReturnType<typeof drizzle<typeof schema>>;

View File

@ -1,4 +1,8 @@
import { import { env } from '$env/dynamic/private';
import nodemailer from 'nodemailer';
import type { EmailTemplate } from './template.interface';
const {
EMAIL_ENABLED, EMAIL_ENABLED,
EMAIL_FROM, EMAIL_FROM,
EMAIL_SMTP_HOST, EMAIL_SMTP_HOST,
@ -6,9 +10,7 @@ import {
EMAIL_SMTP_PORT, EMAIL_SMTP_PORT,
EMAIL_SMTP_SECURE, EMAIL_SMTP_SECURE,
EMAIL_SMTP_USER EMAIL_SMTP_USER
} from '$env/static/private'; } = env;
import nodemailer from 'nodemailer';
import type { EmailTemplate } from './template.interface';
export class Emails { export class Emails {
public transport?: nodemailer.Transporter; public transport?: nodemailer.Transporter;

View File

@ -1,25 +1,25 @@
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import type { EmailTemplate } from '../template.interface'; import type { EmailTemplate } from '../template.interface';
export const ForgotPasswordEmail = (username: string, url: string): EmailTemplate => ({ export const ForgotPasswordEmail = (username: string, url: string): EmailTemplate => ({
text: ` text: `
${PUBLIC_SITE_NAME} ${env.PUBLIC_SITE_NAME}
Hello, ${username}! You have requested a password reset on ${PUBLIC_SITE_NAME}. Hello, ${username}! You have requested a password reset on ${env.PUBLIC_SITE_NAME}.
In order to change your password, please click on the following link. In order to change your password, please click on the following link.
Change your password: ${url} Change your password: ${url}
If you did not request a password change on ${PUBLIC_SITE_NAME}, you can safely ignore this email.`, If you did not request a password change on ${env.PUBLIC_SITE_NAME}, you can safely ignore this email.`,
html: /* html */ ` html: /* html */ `
<h1>${PUBLIC_SITE_NAME}</h1> <h1>${env.PUBLIC_SITE_NAME}</h1>
<p><strong>Hello, ${username}! You have requested a password reset on ${PUBLIC_SITE_NAME}.</strong></p> <p><strong>Hello, ${username}! You have requested a password reset on ${env.PUBLIC_SITE_NAME}.</strong></p>
<p>In order to change your password, please click on the following link.</p> <p>In order to change your password, please click on the following link.</p>
<p>Change your password: <a href="${url}" target="_blank">${url}</a></p> <p>Change your password: <a href="${url}" target="_blank">${url}</a></p>
<p>If you did not request a password change on ${PUBLIC_SITE_NAME}, you can safely ignore this email.</p>` <p>If you did not request a password change on ${env.PUBLIC_SITE_NAME}, you can safely ignore this email.</p>`
}); });

View File

@ -1,21 +1,21 @@
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import type { EmailTemplate } from '../template.interface'; import type { EmailTemplate } from '../template.interface';
export const InvitationEmail = (url: string): EmailTemplate => ({ export const InvitationEmail = (url: string): EmailTemplate => ({
text: ` text: `
${PUBLIC_SITE_NAME} ${env.PUBLIC_SITE_NAME}
Please click on the following link to create an account on ${PUBLIC_SITE_NAME}. Please click on the following link to create an account on ${env.PUBLIC_SITE_NAME}.
Create your account here: ${url} Create your account here: ${url}
This email was sent to you because you have requested an account on ${PUBLIC_SITE_NAME}. If you did not request this, you may safely ignore this email.`, This email was sent to you because you have requested an account on ${env.PUBLIC_SITE_NAME}. If you did not request this, you may safely ignore this email.`,
html: /* html */ ` html: /* html */ `
<h1>${PUBLIC_SITE_NAME}</h1> <h1>${env.PUBLIC_SITE_NAME}</h1>
<p><b>Please click on the following link to create an account on ${PUBLIC_SITE_NAME}.</b></p> <p><b>Please click on the following link to create an account on ${env.PUBLIC_SITE_NAME}.</b></p>
<p>Create your account here: <a href="${url}" target="_blank">${url}</a></p> <p>Create your account here: <a href="${url}" target="_blank">${url}</a></p>
<p>This email was sent to you because you have requested an account on ${PUBLIC_SITE_NAME}. If you did not request this, you may safely ignore this email.</p>` <p>This email was sent to you because you have requested an account on ${env.PUBLIC_SITE_NAME}. If you did not request this, you may safely ignore this email.</p>`
}); });

View File

@ -1,4 +1,4 @@
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import type { EmailTemplate } from '../template.interface'; import type { EmailTemplate } from '../template.interface';
export const OAuth2InvitationEmail = ( export const OAuth2InvitationEmail = (
@ -7,23 +7,23 @@ export const OAuth2InvitationEmail = (
url: string url: string
): EmailTemplate => ({ ): EmailTemplate => ({
text: ` text: `
${PUBLIC_SITE_NAME} ${env.PUBLIC_SITE_NAME}
${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}. ${inviter} has invited you to edit the "${clientName}" application on ${env.PUBLIC_SITE_NAME}.
Please use the following link to accept the invitation. Please use the following link to accept the invitation.
Accept invitation: ${url} Accept invitation: ${url}
This email was sent to you because someone invited you to contribute to an application on ${PUBLIC_SITE_NAME}. If you believe that this was sent in error, you may safely ignore this email.`, This email was sent to you because someone invited you to contribute to an application on ${env.PUBLIC_SITE_NAME}. If you believe that this was sent in error, you may safely ignore this email.`,
html: /* html */ ` html: /* html */ `
<h1>${PUBLIC_SITE_NAME}</h1> <h1>${env.PUBLIC_SITE_NAME}</h1>
<p>${inviter} has invited you to edit the "${clientName}" application on ${PUBLIC_SITE_NAME}. <p>${inviter} has invited you to edit the "${clientName}" application on ${env.PUBLIC_SITE_NAME}.
<p><b>Please use the following link to accept the invitation:</b></p> <p><b>Please use the following link to accept the invitation:</b></p>
<p>Accept invitation: <a href="${url}" target="_blank">${url}</a></p> <p>Accept invitation: <a href="${url}" target="_blank">${url}</a></p>
<p>This email was sent to you because someone invited you to contribute to an application on ${PUBLIC_SITE_NAME}. If you believe that this was sent in error, you may safely ignore this email.</p>` <p>This email was sent to you because someone invited you to contribute to an application on ${env.PUBLIC_SITE_NAME}. If you believe that this was sent in error, you may safely ignore this email.</p>`
}); });

View File

@ -1,25 +1,25 @@
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import type { EmailTemplate } from '../template.interface'; import type { EmailTemplate } from '../template.interface';
export const RegistrationEmail = (username: string, url: string): EmailTemplate => ({ export const RegistrationEmail = (username: string, url: string): EmailTemplate => ({
text: ` text: `
${PUBLIC_SITE_NAME} ${env.PUBLIC_SITE_NAME}
Welcome to ${PUBLIC_SITE_NAME}, ${username}! Welcome to ${env.PUBLIC_SITE_NAME}, ${username}!
In order to proceed with logging in, please click on the following link to activate your account. In order to proceed with logging in, please click on the following link to activate your account.
Activate your account: ${url} Activate your account: ${url}
This email was sent to you because you have created an account on ${PUBLIC_SITE_NAME}. If you did not create an account, you may contact us or just let the account expire.`, This email was sent to you because you have created an account on ${env.PUBLIC_SITE_NAME}. If you did not create an account, you may contact us or just let the account expire.`,
html: /* html */ ` html: /* html */ `
<h1>${PUBLIC_SITE_NAME}</h1> <h1>${env.PUBLIC_SITE_NAME}</h1>
<p><strong>Welcome to ${PUBLIC_SITE_NAME}, ${username}!</strong></p> <p><strong>Welcome to ${env.PUBLIC_SITE_NAME}, ${username}!</strong></p>
<p>In order to proceed with logging in, please click on the following link to activate your account.</p> <p>In order to proceed with logging in, please click on the following link to activate your account.</p>
<p>Activate your account: <a href="${url}" target="_blank">${url}</a></p> <p>Activate your account: <a href="${url}" target="_blank">${url}</a></p>
<p>This email was sent to you because you have created an account on ${PUBLIC_SITE_NAME}. If you did not create an account, you may contact us or just let the account expire.</p>` <p>This email was sent to you because you have created an account on ${env.PUBLIC_SITE_NAME}. If you did not create an account, you may contact us or just let the account expire.</p>`
}); });

View File

@ -1,4 +1,4 @@
import { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } from '$env/static/private'; import { env } from '$env/dynamic/private';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import { import {
SignJWT, SignJWT,
@ -12,6 +12,8 @@ import {
import { join } from 'path'; import { join } from 'path';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } = env;
/** /**
* Generate JWT keys using the following commands: * Generate JWT keys using the following commands:
* Private: openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048 * Private: openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048

View File

@ -1,4 +1,4 @@
import { PUBLIC_URL, PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import { CryptoUtils } from '$lib/server/crypto-utils'; import { CryptoUtils } from '$lib/server/crypto-utils';
import { import {
DB, DB,
@ -418,14 +418,14 @@ export class OAuth2Clients {
const content = OAuth2InvitationEmail( const content = OAuth2InvitationEmail(
actor.display_name, actor.display_name,
client.title, client.title,
`${PUBLIC_URL}/account/accept-invite?${params.toString()}` `${env.PUBLIC_URL}/account/accept-invite?${params.toString()}`
); );
// TODO: logging // TODO: logging
try { try {
await Emails.getSender().sendTemplate( await Emails.getSender().sendTemplate(
email, email,
`You have been invited to manage "${client.title}" on ${PUBLIC_SITE_NAME}`, `You have been invited to manage "${client.title}" on ${env.PUBLIC_SITE_NAME}`,
content content
); );
} catch { } catch {

View File

@ -10,7 +10,7 @@ import { Users } from '$lib/server/users';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import { OAuth2Clients } from './client'; import { OAuth2Clients } from './client';
import { OAuth2Tokens } from './tokens'; import { OAuth2Tokens } from './tokens';
import { PUBLIC_URL } from '$env/static/public'; import { env } from '$env/dynamic/public';
import { JWT } from '$lib/server/jwt'; import { JWT } from '$lib/server/jwt';
export class OAuth2Users { export class OAuth2Users {
@ -124,7 +124,7 @@ export class OAuth2Users {
} }
if (scope.includes('picture') && subject.pictureId) { if (scope.includes('picture') && subject.pictureId) {
userData.picture = `${PUBLIC_URL}/api/avatar/${subject.uuid}`; userData.picture = `${env.PUBLIC_URL}/api/avatar/${subject.uuid}`;
} }
return JWT.issue(userData, subject.uuid, client.client_id); return JWT.issue(userData, subject.uuid, client.client_id);

View File

@ -73,7 +73,7 @@ export class OAuth2Response {
return obj; return obj;
} }
private static createResponse(code: number, data: unknown) { static createResponse(code: number, data: unknown) {
const isJson = typeof data === 'object'; const isJson = typeof data === 'object';
const body = isJson ? JSON.stringify(data) : (data as string); const body = isJson ? JSON.stringify(data) : (data as string);
return new Response(body, { return new Response(body, {
@ -84,7 +84,7 @@ export class OAuth2Response {
}); });
} }
private static createErrorResponse(err: OAuth2Error) { static createErrorResponse(err: OAuth2Error) {
return OAuth2Response.createResponse(err.status, { return OAuth2Response.createResponse(err.status, {
error: err.code, error: err.code,
error_description: err.message error_description: err.message

View File

@ -4,9 +4,9 @@ import { DB, privilege, user, userPrivilegesPrivilege, type User } from '../driz
import type { UserSession } from './types'; import type { UserSession } from './types';
import { error, redirect } from '@sveltejs/kit'; import { error, redirect } from '@sveltejs/kit';
import { CryptoUtils } from '../crypto-utils'; import { CryptoUtils } from '../crypto-utils';
import { EMAIL_ENABLED } from '$env/static/private'; import { env as privateEnv } from '$env/dynamic/private';
import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email'; import { Emails, ForgotPasswordEmail, InvitationEmail, RegistrationEmail } from '../email';
import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public'; import { env as publicEnv } from '$env/dynamic/public';
import { UserTokens } from './tokens'; import { UserTokens } from './tokens';
export class Users { export class Users {
@ -207,13 +207,13 @@ export class Users {
username, username,
password: passwordHash, password: passwordHash,
display_name: displayName, display_name: displayName,
activated: EMAIL_ENABLED === 'false' ? 1 : Number(activate), activated: privateEnv.EMAIL_ENABLED === 'false' ? 1 : Number(activate),
activity_at: new Date() activity_at: new Date()
}); });
const [newUser] = await DB.drizzle.select().from(user).where(eq(user.id, retval.insertId)); const [newUser] = await DB.drizzle.select().from(user).where(eq(user.id, retval.insertId));
if (EMAIL_ENABLED !== 'false' && !activate) { if (privateEnv.EMAIL_ENABLED !== 'false' && !activate) {
await Users.sendRegistrationEmail(newUser); await Users.sendRegistrationEmail(newUser);
} }
@ -234,13 +234,16 @@ export class Users {
); );
const params = new URLSearchParams({ activate: token.token }); const params = new URLSearchParams({ activate: token.token });
const content = RegistrationEmail(user.username, `${PUBLIC_URL}/login?${params.toString()}`); const content = RegistrationEmail(
user.username,
`${publicEnv.PUBLIC_URL}/login?${params.toString()}`
);
// TODO: logging // TODO: logging
try { try {
await Emails.getSender().sendTemplate( await Emails.getSender().sendTemplate(
user.email, user.email,
`Activate your account on ${PUBLIC_SITE_NAME}`, `Activate your account on ${publicEnv.PUBLIC_SITE_NAME}`,
content content
); );
} catch (error) { } catch (error) {
@ -262,14 +265,14 @@ export class Users {
const params = new URLSearchParams({ token: token.token }); const params = new URLSearchParams({ token: token.token });
const content = ForgotPasswordEmail( const content = ForgotPasswordEmail(
user.username, user.username,
`${PUBLIC_URL}/login/password?${params.toString()}` `${publicEnv.PUBLIC_URL}/login/password?${params.toString()}`
); );
// TODO: logging // TODO: logging
try { try {
await Emails.getSender().sendTemplate( await Emails.getSender().sendTemplate(
user.email, user.email,
`Reset your password on ${PUBLIC_SITE_NAME}`, `Reset your password on ${publicEnv.PUBLIC_SITE_NAME}`,
content content
); );
} catch { } catch {
@ -290,13 +293,13 @@ export class Users {
`register=${email}` `register=${email}`
); );
const params = new URLSearchParams({ token: token.token }); const params = new URLSearchParams({ token: token.token });
const content = InvitationEmail(`${PUBLIC_URL}/register?${params.toString()}`); const content = InvitationEmail(`${publicEnv.PUBLIC_URL}/register?${params.toString()}`);
// TODO: logging // TODO: logging
try { try {
await Emails.getSender().sendTemplate( await Emails.getSender().sendTemplate(
email, email,
`You have been invited to create an account on ${PUBLIC_SITE_NAME}`, `You have been invited to create an account on ${publicEnv.PUBLIC_SITE_NAME}`,
content content
); );
} catch { } catch {

View File

@ -1,7 +1,7 @@
import { authenticator as totp } from 'otplib'; import { authenticator as totp } from 'otplib';
import { DB, userToken, type User } from '../drizzle'; import { DB, userToken, type User } from '../drizzle';
import { and, eq, gt, isNull, or } from 'drizzle-orm'; import { and, eq, gt, isNull, or } from 'drizzle-orm';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
totp.options = { totp.options = {
window: 2 window: 2

30
src/lib/theme-mode.ts Normal file
View File

@ -0,0 +1,30 @@
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import { writable } from 'svelte/store';
export type ThemeModeType = 'light' | 'dark';
export const themeMode = writable<ThemeModeType>('light', (set) => {
if (!browser) return;
const storageMode = window?.localStorage.getItem('inThemeMode') as ThemeModeType;
const uaTheme = window?.matchMedia?.('(prefers-color-scheme: dark)');
const uaMode: ThemeModeType = uaTheme?.matches ? 'dark' : 'light';
set(storageMode || uaMode || 'light');
const uaThemeCallback = () => set(uaTheme?.matches ? 'dark' : 'light');
uaTheme?.addEventListener('change', uaThemeCallback);
return () => uaTheme?.removeEventListener('change', uaThemeCallback);
});
export const useThemeMode = () => {
onMount(() =>
themeMode.subscribe((value) => document.documentElement.setAttribute('theme-base', value))
);
};
export const setThemeMode = (mode: ThemeModeType) => {
if (!browser) return;
themeMode.set(mode);
window.localStorage.setItem('inThemeMode', mode);
};

View File

@ -1,5 +1,8 @@
<script lang="ts"> <script lang="ts">
import { useThemeMode } from '$lib/theme-mode';
import '../app.css'; import '../app.css';
useThemeMode();
</script> </script>
<slot></slot> <slot></slot>

View File

@ -1,8 +1,8 @@
import { JWT_ALGORITHM } from '$env/static/private'; import { env } from '$env/dynamic/private';
import { JWT } from '$lib/server/jwt'; import { JWT } from '$lib/server/jwt';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
export const GET = async () => export const GET = async () =>
json({ json({
keys: [{ alg: JWT_ALGORITHM, kid: JWT.jwksKid, ...JWT.jwks, use: 'sig' }] keys: [{ alg: env.JWT_ALGORITHM, kid: JWT.jwksKid, ...JWT.jwks, use: 'sig' }]
}); });

View File

@ -1,17 +1,17 @@
import { JWT_ALGORITHM, JWT_ISSUER } from '$env/static/private'; import { env as privateEnv } from '$env/dynamic/private';
import { PUBLIC_URL } from '$env/static/public'; import { env as publicEnv } from '$env/dynamic/public';
import { json } from '@sveltejs/kit'; import { json } from '@sveltejs/kit';
export const GET = async () => export const GET = async () =>
json({ json({
issuer: JWT_ISSUER, issuer: privateEnv.JWT_ISSUER,
authorization_endpoint: `${PUBLIC_URL}/oauth2/authorize`, authorization_endpoint: `${publicEnv.PUBLIC_URL}/oauth2/authorize`,
token_endpoint: `${PUBLIC_URL}/oauth2/token`, token_endpoint: `${publicEnv.PUBLIC_URL}/oauth2/token`,
jwks_uri: `${PUBLIC_URL}/.well-known/jwks.json`, jwks_uri: `${publicEnv.PUBLIC_URL}/.well-known/jwks.json`,
userinfo_endpoint: `${PUBLIC_URL}/api/user`, userinfo_endpoint: `${publicEnv.PUBLIC_URL}/api/user`,
introspection_endpoint: `${PUBLIC_URL}/oauth2/introspect`, introspection_endpoint: `${publicEnv.PUBLIC_URL}/oauth2/introspect`,
response_types_supported: ['code', 'id_token'], response_types_supported: ['code', 'id_token'],
id_token_signing_alg_values_supported: [JWT_ALGORITHM], id_token_signing_alg_values_supported: [privateEnv.JWT_ALGORITHM],
subject_types_supported: ['public'], subject_types_supported: ['public'],
scopes_supported: ['openid', 'profile', 'picture', 'email'], scopes_supported: ['openid', 'profile', 'picture', 'email'],
claims_supported: [ claims_supported: [

View File

@ -189,10 +189,12 @@ export async function load({ locals, url }) {
return redirect(301, `/login?redirectTo=${encodeURIComponent(url.pathname)}`); return redirect(301, `/login?redirectTo=${encodeURIComponent(url.pathname)}`);
} }
const privileges = await Users.getUserPrivileges(currentUser);
const otpEnabled = await TimeOTP.isUserOtp(currentUser); const otpEnabled = await TimeOTP.isUserOtp(currentUser);
const updateRef = Date.now(); const updateRef = Date.now();
return { return {
privileges,
user: userInfo, user: userInfo,
email: Users.anonymizeEmail(currentUser.email), email: Users.anonymizeEmail(currentUser.email),
otpEnabled, otpEnabled,

View File

@ -15,16 +15,20 @@
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte'; import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte'; import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import FormErrors from '$lib/components/form/FormErrors.svelte'; import FormErrors from '$lib/components/form/FormErrors.svelte';
import TitleRow from '$lib/components/container/TitleRow.svelte'; import TitleRow from '$lib/components/container/TitleRow.svelte';
import ActionButton from '$lib/components/ActionButton.svelte'; import ActionButton from '$lib/components/ActionButton.svelte';
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
import ThemeButton from '$lib/components/ThemeButton.svelte';
import { hasPrivileges } from '$lib/utils';
export let data: PageData; export let data: PageData;
export let form: ActionData; export let form: ActionData;
let internalErrors: string[] = []; let internalErrors: string[] = [];
$: errors = [...internalErrors, ...(form?.errors?.length ? form.errors : [])]; $: errors = [...internalErrors, ...(form?.errors?.length ? form.errors : [])];
$: adminButton = hasPrivileges(data.privileges, [['admin', 'self:oauth2']]);
let usernameRef: HTMLInputElement; let usernameRef: HTMLInputElement;
let displayRef: HTMLInputElement; let displayRef: HTMLInputElement;
@ -56,14 +60,20 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('account.title')} - {PUBLIC_SITE_NAME}</title> <title>{$t('account.title')} - {env.PUBLIC_SITE_NAME}</title>
</svelte:head> </svelte:head>
<MainContainer> <MainContainer>
<TitleRow> <TitleRow>
<h1>{PUBLIC_SITE_NAME}</h1> <h1>{env.PUBLIC_SITE_NAME}</h1>
<LogoutButton /> <ButtonRow>
<LogoutButton />
{#if adminButton}
<a href="/ssoadmin">{$t('account.admin')}</a>
{/if}
<ThemeButton />
</ButtonRow>
</TitleRow> </TitleRow>
<SplitView> <SplitView>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { assets } from '$app/paths'; import { assets } from '$app/paths';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte'; import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
import ColumnView from '$lib/components/container/ColumnView.svelte'; import ColumnView from '$lib/components/container/ColumnView.svelte';
@ -14,12 +14,12 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('account.authorizations.title')} - {PUBLIC_SITE_NAME}</title> <title>{$t('account.authorizations.title')} - {env.PUBLIC_SITE_NAME}</title>
</svelte:head> </svelte:head>
<MainContainer> <MainContainer>
<TitleRow> <TitleRow>
<h1>{PUBLIC_SITE_NAME}</h1> <h1>{env.PUBLIC_SITE_NAME}</h1>
<a href="/account">{$t('account.altTitle')}</a> <a href="/account">{$t('account.altTitle')}</a>
</TitleRow> </TitleRow>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import Alert from '$lib/components/Alert.svelte'; import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import ColumnView from '$lib/components/container/ColumnView.svelte'; import ColumnView from '$lib/components/container/ColumnView.svelte';
@ -16,12 +16,12 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('account.otp.title')} - {PUBLIC_SITE_NAME}</title> <title>{$t('account.otp.title')} - {env.PUBLIC_SITE_NAME}</title>
</svelte:head> </svelte:head>
<MainContainer> <MainContainer>
<TitleRow> <TitleRow>
<h1>{PUBLIC_SITE_NAME}</h1> <h1>{env.PUBLIC_SITE_NAME}</h1>
<a href="/account">{$t('account.altTitle')}</a> <a href="/account">{$t('account.altTitle')}</a>
</TitleRow> </TitleRow>

View File

@ -1,4 +1,4 @@
import { PUBLIC_URL } from '$env/static/public'; import { env } from '$env/dynamic/public';
import type { User } from '$lib/server/drizzle/schema.js'; import type { User } from '$lib/server/drizzle/schema.js';
import { OAuth2BearerController } from '$lib/server/oauth2/controller/bearer.js'; import { OAuth2BearerController } from '$lib/server/oauth2/controller/bearer.js';
import { AccessDenied, OAuth2Error } from '$lib/server/oauth2/error.js'; import { AccessDenied, OAuth2Error } from '$lib/server/oauth2/error.js';
@ -57,7 +57,7 @@ export const GET = async ({ request, url, locals }) => {
} }
if ((scopelessAccess || tokenScopes?.includes('picture')) && user.pictureId) { if ((scopelessAccess || tokenScopes?.includes('picture')) && user.pictureId) {
userData.picture = `${PUBLIC_URL}/api/avatar/${user.uuid}`; userData.picture = `${env.PUBLIC_URL}/api/avatar/${user.uuid}`;
} }
if (scopelessAccess || tokenScopes?.includes('privileges')) { if (scopelessAccess || tokenScopes?.includes('privileges')) {

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import type { ActionData, PageData } from './$types'; import type { ActionData, PageData } from './$types';
import Alert from '$lib/components/Alert.svelte'; import Alert from '$lib/components/Alert.svelte';
@ -16,11 +16,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('account.login.title')} - {PUBLIC_SITE_NAME}</title> <title>{$t('account.login.title')} - {env.PUBLIC_SITE_NAME}</title>
</svelte:head> </svelte:head>
<SideContainer> <SideContainer>
<h1>{PUBLIC_SITE_NAME}</h1> <h1>{env.PUBLIC_SITE_NAME}</h1>
<h2>{$t('account.login.title')}</h2> <h2>{$t('account.login.title')}</h2>
@ -71,7 +71,7 @@
</form> </form>
<div class="welcome"> <div class="welcome">
<p class="text-bold">{$t('common.description', { siteName: PUBLIC_SITE_NAME })}</p> <p class="text-bold">{$t('common.description', { siteName: env.PUBLIC_SITE_NAME })}</p>
<p>{@html $t('common.cookieDisclaimer')}</p> <p>{@html $t('common.cookieDisclaimer')}</p>
</div> </div>
</SideContainer> </SideContainer>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import Alert from '$lib/components/Alert.svelte'; import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import ButtonRow from '$lib/components/container/ButtonRow.svelte'; import ButtonRow from '$lib/components/container/ButtonRow.svelte';
@ -44,11 +44,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t(`account.${pageTitle}`)} - {PUBLIC_SITE_NAME}</title> <title>{$t(`account.${pageTitle}`)} - {env.PUBLIC_SITE_NAME}</title>
</svelte:head> </svelte:head>
<SideContainer> <SideContainer>
<h1>{PUBLIC_SITE_NAME}</h1> <h1>{env.PUBLIC_SITE_NAME}</h1>
<h2>{$t(`account.${pageTitle}`)}</h2> <h2>{$t(`account.${pageTitle}`)}</h2>
<ColumnView> <ColumnView>
{#if form?.success} {#if form?.success}

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { assets } from '$app/paths'; import { assets } from '$app/paths';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import Alert from '$lib/components/Alert.svelte'; import Alert from '$lib/components/Alert.svelte';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import ColumnView from '$lib/components/container/ColumnView.svelte'; import ColumnView from '$lib/components/container/ColumnView.svelte';
@ -13,12 +13,13 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('oauth2.authorize.title')} "{data.client?.title || ''}" - {PUBLIC_SITE_NAME}</title> <title>{$t('oauth2.authorize.title')} "{data.client?.title || ''}" - {env.PUBLIC_SITE_NAME}</title
>
</svelte:head> </svelte:head>
<MainContainer> <MainContainer>
{#if data.error} {#if data.error}
<h1>{PUBLIC_SITE_NAME}</h1> <h1>{env.PUBLIC_SITE_NAME}</h1>
<ColumnView> <ColumnView>
<Alert type="error" <Alert type="error"
>{$t('oauth2.authorize.errorPage')}<br /><br /><code >{$t('oauth2.authorize.errorPage')}<br /><br /><code
@ -30,7 +31,7 @@
{/if} {/if}
{#if data.client} {#if data.client}
<h1 class="title">{PUBLIC_SITE_NAME}</h1> <h1 class="title">{env.PUBLIC_SITE_NAME}</h1>
<h2 class="title">{$t('oauth2.authorize.title')}</h2> <h2 class="title">{$t('oauth2.authorize.title')}</h2>
<div class="user-client-wrapper"> <div class="user-client-wrapper">

View File

@ -1,4 +1,4 @@
import { REGISTRATIONS } from '$env/static/private'; import { env } from '$env/dynamic/private';
import { Changesets } from '$lib/server/changesets.js'; import { Changesets } from '$lib/server/changesets.js';
import { Users } from '$lib/server/users/index.js'; import { Users } from '$lib/server/users/index.js';
import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js'; import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js';
@ -24,7 +24,7 @@ export const actions = {
if (await limiter.isLimited(event)) throw error(429); if (await limiter.isLimited(event)) throw error(429);
// Logged in users cannot make more accounts // Logged in users cannot make more accounts
if (locals.session.data?.user || REGISTRATIONS === 'false') { if (locals.session.data?.user || env.REGISTRATIONS === 'false') {
return redirect(303, '/'); return redirect(303, '/');
} }
@ -110,6 +110,6 @@ export const load = ({ locals }) => {
} }
return { return {
enabled: REGISTRATIONS === 'true' enabled: env.REGISTRATIONS === 'true'
}; };
}; };

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import type { SubmitFunction } from '@sveltejs/kit'; import type { SubmitFunction } from '@sveltejs/kit';
import type { PageData, ActionData } from './$types'; import type { PageData, ActionData } from './$types';
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
@ -41,11 +41,11 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('account.register.title')} - {PUBLIC_SITE_NAME}</title> <title>{$t('account.register.title')} - {env.PUBLIC_SITE_NAME}</title>
</svelte:head> </svelte:head>
<SideContainer> <SideContainer>
<h1>{PUBLIC_SITE_NAME}</h1> <h1>{env.PUBLIC_SITE_NAME}</h1>
<h2>{$t('account.register.title')}</h2> <h2>{$t('account.register.title')}</h2>
<ColumnView> <ColumnView>

View File

@ -5,7 +5,7 @@
import ClientCard from '$lib/components/admin/AdminClientCard.svelte'; import ClientCard from '$lib/components/admin/AdminClientCard.svelte';
import TitleRow from '$lib/components/container/TitleRow.svelte'; import TitleRow from '$lib/components/container/TitleRow.svelte';
import ColumnView from '$lib/components/container/ColumnView.svelte'; import ColumnView from '$lib/components/container/ColumnView.svelte';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import FormControl from '$lib/components/form/FormControl.svelte'; import FormControl from '$lib/components/form/FormControl.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -13,7 +13,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('admin.oauth2.title')} - {PUBLIC_SITE_NAME} {$t('admin.title')}</title> <title>{$t('admin.oauth2.title')} - {env.PUBLIC_SITE_NAME} {$t('admin.title')}</title>
</svelte:head> </svelte:head>
<TitleRow> <TitleRow>

View File

@ -15,7 +15,7 @@
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public'; import { env } from '$env/dynamic/public';
import { OAUTH2_MAX_REDIRECTS, OAUTH2_MAX_URLS } from '$lib/constants'; import { OAUTH2_MAX_REDIRECTS, OAUTH2_MAX_URLS } from '$lib/constants';
export let data: PageData; export let data: PageData;
@ -43,7 +43,7 @@
<svelte:head> <svelte:head>
<title <title
>{$t('admin.oauth2.title')} / {data.details.title} - {PUBLIC_SITE_NAME} >{$t('admin.oauth2.title')} / {data.details.title} - {env.PUBLIC_SITE_NAME}
{$t('admin.title')}</title {$t('admin.title')}</title
> >
</svelte:head> </svelte:head>
@ -293,7 +293,7 @@
{#if data.fullPrivileges || data.details.isOwner} {#if data.fullPrivileges || data.details.isOwner}
<ColumnView> <ColumnView>
<h2>{$t('admin.oauth2.managers.title')}</h2> <h2>{$t('admin.oauth2.managers.title')}</h2>
<p>{$t('admin.oauth2.managers.hint', { siteName: PUBLIC_SITE_NAME })}</p> <p>{$t('admin.oauth2.managers.hint', { siteName: env.PUBLIC_SITE_NAME })}</p>
<div class="addremove"> <div class="addremove">
{#each data.managers as user} {#each data.managers as user}
@ -343,7 +343,7 @@
{$t('admin.oauth2.apis.authorize')} - {$t('admin.oauth2.apis.authorize')} -
<code <code
><a href={`/oauth2/authorize`} data-sveltekit-preload-data="off" ><a href={`/oauth2/authorize`} data-sveltekit-preload-data="off"
>{PUBLIC_URL}/oauth2/authorize</a >{env.PUBLIC_URL}/oauth2/authorize</a
></code ></code
> >
</li> </li>
@ -351,7 +351,7 @@
{$t('admin.oauth2.apis.token')} - {$t('admin.oauth2.apis.token')} -
<code <code
><a href={`/oauth2/token`} data-sveltekit-preload-data="off" ><a href={`/oauth2/token`} data-sveltekit-preload-data="off"
>{PUBLIC_URL}/oauth2/token</a >{env.PUBLIC_URL}/oauth2/token</a
></code ></code
> >
</li> </li>
@ -359,21 +359,22 @@
{$t('admin.oauth2.apis.introspect')} - {$t('admin.oauth2.apis.introspect')} -
<code <code
><a href={`/oauth2/introspect`} data-sveltekit-preload-data="off" ><a href={`/oauth2/introspect`} data-sveltekit-preload-data="off"
>{PUBLIC_URL}/oauth2/introspect</a >{env.PUBLIC_URL}/oauth2/introspect</a
></code ></code
> >
</li> </li>
<li> <li>
{$t('admin.oauth2.apis.userinfo')} - {$t('admin.oauth2.apis.userinfo')} -
<code <code
><a href={`/api/user`} data-sveltekit-preload-data="off">{PUBLIC_URL}/api/user</a></code ><a href={`/api/user`} data-sveltekit-preload-data="off">{env.PUBLIC_URL}/api/user</a
></code
> >
</li> </li>
<li> <li>
{$t('admin.oauth2.apis.openid')} - {$t('admin.oauth2.apis.openid')} -
<code <code
><a href={`/.well-known/openid-configuration`} data-sveltekit-preload-data="off" ><a href={`/.well-known/openid-configuration`} data-sveltekit-preload-data="off"
>{PUBLIC_URL}/.well-known/openid-configuration</a >{env.PUBLIC_URL}/.well-known/openid-configuration</a
></code ></code
> >
</li> </li>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte'; import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
import ButtonRow from '$lib/components/container/ButtonRow.svelte'; import ButtonRow from '$lib/components/container/ButtonRow.svelte';
@ -13,7 +13,7 @@
<svelte:head> <svelte:head>
<title <title
>{$t('admin.oauth2.privileges.edit')} / {data.details.title} - {PUBLIC_SITE_NAME} >{$t('admin.oauth2.privileges.edit')} / {data.details.title} - {env.PUBLIC_SITE_NAME}
{$t('admin.title')}</title {$t('admin.title')}</title
> >
</svelte:head> </svelte:head>

View File

@ -6,13 +6,13 @@
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
import type { ActionData } from './$types'; import type { ActionData } from './$types';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
export let form: ActionData; export let form: ActionData;
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('admin.oauth2.new')} - {PUBLIC_SITE_NAME} {$t('admin.title')}</title> <title>{$t('admin.oauth2.new')} - {env.PUBLIC_SITE_NAME} {$t('admin.title')}</title>
</svelte:head> </svelte:head>
<h1>{$t('admin.oauth2.new')}</h1> <h1>{$t('admin.oauth2.new')}</h1>

View File

@ -3,7 +3,7 @@
import { t } from '$lib/i18n'; import { t } from '$lib/i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
import UserCard from '$lib/components/admin/AdminUserCard.svelte'; import UserCard from '$lib/components/admin/AdminUserCard.svelte';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import FormControl from '$lib/components/form/FormControl.svelte'; import FormControl from '$lib/components/form/FormControl.svelte';
import ColumnView from '$lib/components/container/ColumnView.svelte'; import ColumnView from '$lib/components/container/ColumnView.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
@ -12,7 +12,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$t('admin.users.title')} - {PUBLIC_SITE_NAME} {$t('admin.title')}</title> <title>{$t('admin.users.title')} - {env.PUBLIC_SITE_NAME} {$t('admin.title')}</title>
</svelte:head> </svelte:head>
<h1>{$t('admin.users.title')}</h1> <h1>{$t('admin.users.title')}</h1>

View File

@ -96,7 +96,10 @@ export const actions = {
body body
); );
if (!!privileges && !hasPrivileges(userSession.privileges || [], ['admin:user:privilege'])) { if (
privileges !== undefined &&
!hasPrivileges(userSession.privileges || [], ['admin:user:privilege'])
) {
return fail(403, { errors: ['unauthorized'] }); return fail(403, { errors: ['unauthorized'] });
} }
@ -109,9 +112,15 @@ export const actions = {
return fail(400, { errors: ['lockout'] }); return fail(400, { errors: ['lockout'] });
} }
if (privileges) { if (privileges !== undefined) {
// TODO: check NaNs const newPrivilegeIds =
const newPrivilegeIds = privileges?.split(',').map(Number) || []; privileges?.split(',').reduce<number[]>((final, entry) => {
if (!entry) return final;
const parsed = Number(entry);
if (isNaN(parsed) || final.includes(parsed)) return final;
return [...final, parsed];
}, []) || [];
await Users.setUserPrivileges(targetUser, newPrivilegeIds); await Users.setUserPrivileges(targetUser, newPrivilegeIds);
} }

View File

@ -10,7 +10,7 @@
import type { ActionData, PageData } from './$types'; import type { ActionData, PageData } from './$types';
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte'; import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
import FormErrors from '$lib/components/form/FormErrors.svelte'; import FormErrors from '$lib/components/form/FormErrors.svelte';
import { PUBLIC_SITE_NAME } from '$env/static/public'; import { env } from '$env/dynamic/public';
import ActionButton from '$lib/components/ActionButton.svelte'; import ActionButton from '$lib/components/ActionButton.svelte';
import Alert from '$lib/components/Alert.svelte'; import Alert from '$lib/components/Alert.svelte';
@ -20,7 +20,7 @@
<svelte:head> <svelte:head>
<title <title
>{$t('admin.users.title')} / {data.details.display_name} - {PUBLIC_SITE_NAME} >{$t('admin.users.title')} / {data.details.display_name} - {env.PUBLIC_SITE_NAME}
{$t('admin.title')}</title {$t('admin.title')}</title
> >
</svelte:head> </svelte:head>