Convert static environment to dynamic, theme toggle
This commit is contained in:
parent
6febe18daa
commit
adbc143926
@ -3,10 +3,7 @@ FROM node:20 AS builder
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
ARG envFile=.env
|
||||
|
||||
COPY . .
|
||||
COPY ./${envFile} ./.env
|
||||
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
|
@ -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 { DB } from '$lib/server/drizzle';
|
||||
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 { handleSession } from 'svelte-kit-cookie-session';
|
||||
|
||||
const { AUTO_MIGRATE, SESSION_SECRET, SESSION_SECURE } = env;
|
||||
|
||||
await DB.init();
|
||||
await JWT.init();
|
||||
|
||||
|
12
src/lib/components/ThemeButton.svelte
Normal file
12
src/lib/components/ThemeButton.svelte
Normal 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>
|
@ -1,14 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import type { UserSession } from '$lib/types';
|
||||
import ThemeButton from '../ThemeButton.svelte';
|
||||
export let user: UserSession;
|
||||
</script>
|
||||
|
||||
<header class="admin-header">
|
||||
<a class="site-name" href="/">{PUBLIC_SITE_NAME}</a>
|
||||
<div class="admin-user">
|
||||
<img class="admin-user-avatar" src={`/api/avatar/${user.uuid}`} alt={user.name} />
|
||||
<span class="admin-user-name">{user.name}</span>
|
||||
<a class="site-name" href="/">{env.PUBLIC_SITE_NAME}</a>
|
||||
<div class="aside">
|
||||
<div class="admin-user">
|
||||
<img class="admin-user-avatar" src={`/api/avatar/${user.uuid}`} alt={user.name} />
|
||||
<span class="admin-user-name">{user.name}</span>
|
||||
</div>
|
||||
<ThemeButton />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@ -19,6 +23,15 @@
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
background: var(--ina-header-color);
|
||||
|
||||
& .aside {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-header .aside :global(button) {
|
||||
color: var(--ina-header-link-color);
|
||||
}
|
||||
|
||||
.admin-user {
|
||||
|
@ -9,8 +9,15 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1.3rem;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.title-row > :global(*):first-child {
|
||||
align-self: flex-start;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
24
src/lib/components/icons/Icon.svelte
Normal file
24
src/lib/components/icons/Icon.svelte
Normal 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>
|
5
src/lib/components/icons/svg/DarkMode.svelte
Normal file
5
src/lib/components/icons/svg/DarkMode.svelte
Normal 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 |
5
src/lib/components/icons/svg/LightMode.svelte
Normal file
5
src/lib/components/icons/svg/LightMode.svelte
Normal 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 |
@ -26,6 +26,7 @@
|
||||
"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.",
|
||||
"logout": "Log out",
|
||||
"admin": "Admin",
|
||||
"avatar": {
|
||||
"title": "Profile avatar",
|
||||
"change": "Change avatar",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { CHALLENGE_SECRET } from '$env/static/private';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import * as crypto from 'crypto';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
@ -54,11 +54,11 @@ export class CryptoUtils {
|
||||
}
|
||||
|
||||
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> {
|
||||
return JSON.parse(this.decrypt(challenge, CHALLENGE_SECRET));
|
||||
return JSON.parse(this.decrypt(challenge, env.CHALLENGE_SECRET));
|
||||
}
|
||||
|
||||
static safeCompare(token: string, token2: string) {
|
||||
|
@ -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 mysql from 'mysql2/promise';
|
||||
import * as schema from './schema';
|
||||
|
||||
const { DATABASE_DB, DATABASE_HOST, DATABASE_PASS } = env;
|
||||
|
||||
export class DB {
|
||||
static mysqlConnection: mysql.Connection;
|
||||
static drizzle: ReturnType<typeof drizzle<typeof schema>>;
|
||||
|
@ -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_FROM,
|
||||
EMAIL_SMTP_HOST,
|
||||
@ -6,9 +10,7 @@ import {
|
||||
EMAIL_SMTP_PORT,
|
||||
EMAIL_SMTP_SECURE,
|
||||
EMAIL_SMTP_USER
|
||||
} from '$env/static/private';
|
||||
import nodemailer from 'nodemailer';
|
||||
import type { EmailTemplate } from './template.interface';
|
||||
} = env;
|
||||
|
||||
export class Emails {
|
||||
public transport?: nodemailer.Transporter;
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import type { EmailTemplate } from '../template.interface';
|
||||
|
||||
export const ForgotPasswordEmail = (username: string, url: string): EmailTemplate => ({
|
||||
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.
|
||||
|
||||
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 */ `
|
||||
<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>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>`
|
||||
});
|
||||
|
@ -1,21 +1,21 @@
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import type { EmailTemplate } from '../template.interface';
|
||||
|
||||
export const InvitationEmail = (url: string): EmailTemplate => ({
|
||||
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}
|
||||
|
||||
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 */ `
|
||||
<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>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>`
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import type { EmailTemplate } from '../template.interface';
|
||||
|
||||
export const OAuth2InvitationEmail = (
|
||||
@ -7,23 +7,23 @@ export const OAuth2InvitationEmail = (
|
||||
url: string
|
||||
): EmailTemplate => ({
|
||||
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.
|
||||
|
||||
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 */ `
|
||||
<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>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>`
|
||||
});
|
||||
|
@ -1,25 +1,25 @@
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import type { EmailTemplate } from '../template.interface';
|
||||
|
||||
export const RegistrationEmail = (username: string, url: string): EmailTemplate => ({
|
||||
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.
|
||||
|
||||
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 */ `
|
||||
<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>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>`
|
||||
});
|
||||
|
@ -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 {
|
||||
SignJWT,
|
||||
@ -12,6 +12,8 @@ import {
|
||||
import { join } from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const { JWT_ALGORITHM, JWT_EXPIRATION, JWT_ISSUER } = env;
|
||||
|
||||
/**
|
||||
* Generate JWT keys using the following commands:
|
||||
* Private: openssl genpkey -out jwt.private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:2048
|
||||
|
@ -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 {
|
||||
DB,
|
||||
@ -418,14 +418,14 @@ export class OAuth2Clients {
|
||||
const content = OAuth2InvitationEmail(
|
||||
actor.display_name,
|
||||
client.title,
|
||||
`${PUBLIC_URL}/account/accept-invite?${params.toString()}`
|
||||
`${env.PUBLIC_URL}/account/accept-invite?${params.toString()}`
|
||||
);
|
||||
|
||||
// TODO: logging
|
||||
try {
|
||||
await Emails.getSender().sendTemplate(
|
||||
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
|
||||
);
|
||||
} catch {
|
||||
|
@ -10,7 +10,7 @@ import { Users } from '$lib/server/users';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { OAuth2Clients } from './client';
|
||||
import { OAuth2Tokens } from './tokens';
|
||||
import { PUBLIC_URL } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { JWT } from '$lib/server/jwt';
|
||||
|
||||
export class OAuth2Users {
|
||||
@ -124,7 +124,7 @@ export class OAuth2Users {
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -73,7 +73,7 @@ export class OAuth2Response {
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static createResponse(code: number, data: unknown) {
|
||||
static createResponse(code: number, data: unknown) {
|
||||
const isJson = typeof data === 'object';
|
||||
const body = isJson ? JSON.stringify(data) : (data as string);
|
||||
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, {
|
||||
error: err.code,
|
||||
error_description: err.message
|
||||
|
@ -4,9 +4,9 @@ import { DB, privilege, user, userPrivilegesPrivilege, type User } from '../driz
|
||||
import type { UserSession } from './types';
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
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 { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public';
|
||||
import { env as publicEnv } from '$env/dynamic/public';
|
||||
import { UserTokens } from './tokens';
|
||||
|
||||
export class Users {
|
||||
@ -207,13 +207,13 @@ export class Users {
|
||||
username,
|
||||
password: passwordHash,
|
||||
display_name: displayName,
|
||||
activated: EMAIL_ENABLED === 'false' ? 1 : Number(activate),
|
||||
activated: privateEnv.EMAIL_ENABLED === 'false' ? 1 : Number(activate),
|
||||
activity_at: new Date()
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -234,13 +234,16 @@ export class Users {
|
||||
);
|
||||
|
||||
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
|
||||
try {
|
||||
await Emails.getSender().sendTemplate(
|
||||
user.email,
|
||||
`Activate your account on ${PUBLIC_SITE_NAME}`,
|
||||
`Activate your account on ${publicEnv.PUBLIC_SITE_NAME}`,
|
||||
content
|
||||
);
|
||||
} catch (error) {
|
||||
@ -262,14 +265,14 @@ export class Users {
|
||||
const params = new URLSearchParams({ token: token.token });
|
||||
const content = ForgotPasswordEmail(
|
||||
user.username,
|
||||
`${PUBLIC_URL}/login/password?${params.toString()}`
|
||||
`${publicEnv.PUBLIC_URL}/login/password?${params.toString()}`
|
||||
);
|
||||
|
||||
// TODO: logging
|
||||
try {
|
||||
await Emails.getSender().sendTemplate(
|
||||
user.email,
|
||||
`Reset your password on ${PUBLIC_SITE_NAME}`,
|
||||
`Reset your password on ${publicEnv.PUBLIC_SITE_NAME}`,
|
||||
content
|
||||
);
|
||||
} catch {
|
||||
@ -290,13 +293,13 @@ export class Users {
|
||||
`register=${email}`
|
||||
);
|
||||
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
|
||||
try {
|
||||
await Emails.getSender().sendTemplate(
|
||||
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
|
||||
);
|
||||
} catch {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { authenticator as totp } from 'otplib';
|
||||
import { DB, userToken, type User } from '../drizzle';
|
||||
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 = {
|
||||
window: 2
|
||||
|
30
src/lib/theme-mode.ts
Normal file
30
src/lib/theme-mode.ts
Normal 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);
|
||||
};
|
@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { useThemeMode } from '$lib/theme-mode';
|
||||
import '../app.css';
|
||||
|
||||
useThemeMode();
|
||||
</script>
|
||||
|
||||
<slot></slot>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { JWT_ALGORITHM } from '$env/static/private';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { JWT } from '$lib/server/jwt';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async () =>
|
||||
json({
|
||||
keys: [{ alg: JWT_ALGORITHM, kid: JWT.jwksKid, ...JWT.jwks, use: 'sig' }]
|
||||
keys: [{ alg: env.JWT_ALGORITHM, kid: JWT.jwksKid, ...JWT.jwks, use: 'sig' }]
|
||||
});
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { JWT_ALGORITHM, JWT_ISSUER } from '$env/static/private';
|
||||
import { PUBLIC_URL } from '$env/static/public';
|
||||
import { env as privateEnv } from '$env/dynamic/private';
|
||||
import { env as publicEnv } from '$env/dynamic/public';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const GET = async () =>
|
||||
json({
|
||||
issuer: JWT_ISSUER,
|
||||
authorization_endpoint: `${PUBLIC_URL}/oauth2/authorize`,
|
||||
token_endpoint: `${PUBLIC_URL}/oauth2/token`,
|
||||
jwks_uri: `${PUBLIC_URL}/.well-known/jwks.json`,
|
||||
userinfo_endpoint: `${PUBLIC_URL}/api/user`,
|
||||
introspection_endpoint: `${PUBLIC_URL}/oauth2/introspect`,
|
||||
issuer: privateEnv.JWT_ISSUER,
|
||||
authorization_endpoint: `${publicEnv.PUBLIC_URL}/oauth2/authorize`,
|
||||
token_endpoint: `${publicEnv.PUBLIC_URL}/oauth2/token`,
|
||||
jwks_uri: `${publicEnv.PUBLIC_URL}/.well-known/jwks.json`,
|
||||
userinfo_endpoint: `${publicEnv.PUBLIC_URL}/api/user`,
|
||||
introspection_endpoint: `${publicEnv.PUBLIC_URL}/oauth2/introspect`,
|
||||
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'],
|
||||
scopes_supported: ['openid', 'profile', 'picture', 'email'],
|
||||
claims_supported: [
|
||||
|
@ -189,10 +189,12 @@ export async function load({ locals, url }) {
|
||||
return redirect(301, `/login?redirectTo=${encodeURIComponent(url.pathname)}`);
|
||||
}
|
||||
|
||||
const privileges = await Users.getUserPrivileges(currentUser);
|
||||
const otpEnabled = await TimeOTP.isUserOtp(currentUser);
|
||||
const updateRef = Date.now();
|
||||
|
||||
return {
|
||||
privileges,
|
||||
user: userInfo,
|
||||
email: Users.anonymizeEmail(currentUser.email),
|
||||
otpEnabled,
|
||||
|
@ -15,16 +15,20 @@
|
||||
import AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
||||
import AvatarModal from '$lib/components/avatar/AvatarModal.svelte';
|
||||
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 TitleRow from '$lib/components/container/TitleRow.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 form: ActionData;
|
||||
|
||||
let internalErrors: string[] = [];
|
||||
$: errors = [...internalErrors, ...(form?.errors?.length ? form.errors : [])];
|
||||
$: adminButton = hasPrivileges(data.privileges, [['admin', 'self:oauth2']]);
|
||||
|
||||
let usernameRef: HTMLInputElement;
|
||||
let displayRef: HTMLInputElement;
|
||||
@ -56,14 +60,20 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('account.title')} - {PUBLIC_SITE_NAME}</title>
|
||||
<title>{$t('account.title')} - {env.PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<MainContainer>
|
||||
<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>
|
||||
|
||||
<SplitView>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
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 AvatarCard from '$lib/components/avatar/AvatarCard.svelte';
|
||||
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||
@ -14,12 +14,12 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('account.authorizations.title')} - {PUBLIC_SITE_NAME}</title>
|
||||
<title>{$t('account.authorizations.title')} - {env.PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<MainContainer>
|
||||
<TitleRow>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
||||
<a href="/account">{$t('account.altTitle')}</a>
|
||||
</TitleRow>
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 Button from '$lib/components/Button.svelte';
|
||||
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||
@ -16,12 +16,12 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('account.otp.title')} - {PUBLIC_SITE_NAME}</title>
|
||||
<title>{$t('account.otp.title')} - {env.PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<MainContainer>
|
||||
<TitleRow>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
||||
<a href="/account">{$t('account.altTitle')}</a>
|
||||
</TitleRow>
|
||||
|
||||
|
@ -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 { OAuth2BearerController } from '$lib/server/oauth2/controller/bearer.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) {
|
||||
userData.picture = `${PUBLIC_URL}/api/avatar/${user.uuid}`;
|
||||
userData.picture = `${env.PUBLIC_URL}/api/avatar/${user.uuid}`;
|
||||
}
|
||||
|
||||
if (scopelessAccess || tokenScopes?.includes('privileges')) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { enhance } from '$app/forms';
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import Alert from '$lib/components/Alert.svelte';
|
||||
@ -16,11 +16,11 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('account.login.title')} - {PUBLIC_SITE_NAME}</title>
|
||||
<title>{$t('account.login.title')} - {env.PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SideContainer>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
||||
|
||||
<h2>{$t('account.login.title')}</h2>
|
||||
|
||||
@ -71,7 +71,7 @@
|
||||
</form>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</SideContainer>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
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 Button from '$lib/components/Button.svelte';
|
||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||
@ -44,11 +44,11 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t(`account.${pageTitle}`)} - {PUBLIC_SITE_NAME}</title>
|
||||
<title>{$t(`account.${pageTitle}`)} - {env.PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SideContainer>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
||||
<h2>{$t(`account.${pageTitle}`)}</h2>
|
||||
<ColumnView>
|
||||
{#if form?.success}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 Button from '$lib/components/Button.svelte';
|
||||
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||
@ -13,12 +13,13 @@
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<MainContainer>
|
||||
{#if data.error}
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
||||
<ColumnView>
|
||||
<Alert type="error"
|
||||
>{$t('oauth2.authorize.errorPage')}<br /><br /><code
|
||||
@ -30,7 +31,7 @@
|
||||
{/if}
|
||||
|
||||
{#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>
|
||||
|
||||
<div class="user-client-wrapper">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { REGISTRATIONS } from '$env/static/private';
|
||||
import { env } from '$env/dynamic/private';
|
||||
import { Changesets } from '$lib/server/changesets.js';
|
||||
import { Users } from '$lib/server/users/index.js';
|
||||
import { emailRegex, passwordRegex, usernameRegex } from '$lib/validators.js';
|
||||
@ -24,7 +24,7 @@ export const actions = {
|
||||
if (await limiter.isLimited(event)) throw error(429);
|
||||
|
||||
// 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, '/');
|
||||
}
|
||||
|
||||
@ -110,6 +110,6 @@ export const load = ({ locals }) => {
|
||||
}
|
||||
|
||||
return {
|
||||
enabled: REGISTRATIONS === 'true'
|
||||
enabled: env.REGISTRATIONS === 'true'
|
||||
};
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 { PageData, ActionData } from './$types';
|
||||
import { t } from '$lib/i18n';
|
||||
@ -41,11 +41,11 @@
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('account.register.title')} - {PUBLIC_SITE_NAME}</title>
|
||||
<title>{$t('account.register.title')} - {env.PUBLIC_SITE_NAME}</title>
|
||||
</svelte:head>
|
||||
|
||||
<SideContainer>
|
||||
<h1>{PUBLIC_SITE_NAME}</h1>
|
||||
<h1>{env.PUBLIC_SITE_NAME}</h1>
|
||||
<h2>{$t('account.register.title')}</h2>
|
||||
|
||||
<ColumnView>
|
||||
|
@ -5,7 +5,7 @@
|
||||
import ClientCard from '$lib/components/admin/AdminClientCard.svelte';
|
||||
import TitleRow from '$lib/components/container/TitleRow.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 { page } from '$app/stores';
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<TitleRow>
|
||||
|
@ -15,7 +15,7 @@
|
||||
import { t } from '$lib/i18n';
|
||||
import { page } from '$app/stores';
|
||||
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';
|
||||
|
||||
export let data: PageData;
|
||||
@ -43,7 +43,7 @@
|
||||
|
||||
<svelte:head>
|
||||
<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
|
||||
>
|
||||
</svelte:head>
|
||||
@ -293,7 +293,7 @@
|
||||
{#if data.fullPrivileges || data.details.isOwner}
|
||||
<ColumnView>
|
||||
<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">
|
||||
{#each data.managers as user}
|
||||
@ -343,7 +343,7 @@
|
||||
{$t('admin.oauth2.apis.authorize')} -
|
||||
<code
|
||||
><a href={`/oauth2/authorize`} data-sveltekit-preload-data="off"
|
||||
>{PUBLIC_URL}/oauth2/authorize</a
|
||||
>{env.PUBLIC_URL}/oauth2/authorize</a
|
||||
></code
|
||||
>
|
||||
</li>
|
||||
@ -351,7 +351,7 @@
|
||||
{$t('admin.oauth2.apis.token')} -
|
||||
<code
|
||||
><a href={`/oauth2/token`} data-sveltekit-preload-data="off"
|
||||
>{PUBLIC_URL}/oauth2/token</a
|
||||
>{env.PUBLIC_URL}/oauth2/token</a
|
||||
></code
|
||||
>
|
||||
</li>
|
||||
@ -359,21 +359,22 @@
|
||||
{$t('admin.oauth2.apis.introspect')} -
|
||||
<code
|
||||
><a href={`/oauth2/introspect`} data-sveltekit-preload-data="off"
|
||||
>{PUBLIC_URL}/oauth2/introspect</a
|
||||
>{env.PUBLIC_URL}/oauth2/introspect</a
|
||||
></code
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
{$t('admin.oauth2.apis.userinfo')} -
|
||||
<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>
|
||||
{$t('admin.oauth2.apis.openid')} -
|
||||
<code
|
||||
><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
|
||||
>
|
||||
</li>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<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 AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
|
||||
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
<svelte:head>
|
||||
<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
|
||||
>
|
||||
</svelte:head>
|
||||
|
@ -6,13 +6,13 @@
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import type { ActionData } from './$types';
|
||||
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||
import { env } from '$env/dynamic/public';
|
||||
|
||||
export let form: ActionData;
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<h1>{$t('admin.oauth2.new')}</h1>
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { t } from '$lib/i18n';
|
||||
import type { PageData } from './$types';
|
||||
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 ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||
import { page } from '$app/stores';
|
||||
@ -12,7 +12,7 @@
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
<h1>{$t('admin.users.title')}</h1>
|
||||
|
@ -96,7 +96,10 @@ export const actions = {
|
||||
body
|
||||
);
|
||||
|
||||
if (!!privileges && !hasPrivileges(userSession.privileges || [], ['admin:user:privilege'])) {
|
||||
if (
|
||||
privileges !== undefined &&
|
||||
!hasPrivileges(userSession.privileges || [], ['admin:user:privilege'])
|
||||
) {
|
||||
return fail(403, { errors: ['unauthorized'] });
|
||||
}
|
||||
|
||||
@ -109,9 +112,15 @@ export const actions = {
|
||||
return fail(400, { errors: ['lockout'] });
|
||||
}
|
||||
|
||||
if (privileges) {
|
||||
// TODO: check NaNs
|
||||
const newPrivilegeIds = privileges?.split(',').map(Number) || [];
|
||||
if (privileges !== undefined) {
|
||||
const newPrivilegeIds =
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
import type { ActionData, PageData } from './$types';
|
||||
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.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 Alert from '$lib/components/Alert.svelte';
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
<svelte:head>
|
||||
<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
|
||||
>
|
||||
</svelte:head>
|
||||
|
Loading…
Reference in New Issue
Block a user