admin stuff

This commit is contained in:
Evert Prants 2024-06-01 14:42:08 +03:00
parent 3bf4c7ce04
commit 1f5de32f61
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
34 changed files with 1803 additions and 71 deletions

View File

@ -19,3 +19,4 @@ EMAIL_SMTP_PASS=
REGISTRATIONS=true
ADDRESS_HEADER=X-Forwarded-For
XFF_DEPTH=1
AUTO_MIGRATE=true

View File

@ -1,9 +1,9 @@
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'mysql',
schema: './src/lib/server/drizzle/schema.ts',
out: './src/lib/server/drizzle/migrations',
dialect: 'mysql',
schema: './src/lib/server/drizzle/schema.ts',
out: './migrations',
dbCredentials: {
host: process.env.DATABASE_HOST as string,
port: Number(process.env.DATABASE_PORT) || 3306,
@ -13,4 +13,4 @@ export default defineConfig({
},
verbose: true,
strict: true
})
});

View File

@ -1,5 +1,3 @@
-- Current sql file was generated after introspecting the database
-- If you want to run this migration please uncomment this code before executing migrations
/*
CREATE TABLE `audit_log` (
`id` int(11) AUTO_INCREMENT NOT NULL,
@ -11,7 +9,7 @@ CREATE TABLE `audit_log` (
`created_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)',
`actorId` int(11) DEFAULT 'NULL'
);
--> statement-breakpoint
CREATE TABLE `document` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`title` text NOT NULL,
@ -21,7 +19,7 @@ CREATE TABLE `document` (
`created_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)',
`updated_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)'
);
--> statement-breakpoint
CREATE TABLE `o_auth2_client` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`client_id` varchar(36) NOT NULL,
@ -38,7 +36,7 @@ CREATE TABLE `o_auth2_client` (
`updated_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)',
CONSTRAINT `IDX_e9d16c213910ad57bd05e97b42` UNIQUE(`client_id`)
);
--> statement-breakpoint
CREATE TABLE `o_auth2_client_authorization` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`scope` text DEFAULT 'NULL',
@ -47,7 +45,7 @@ CREATE TABLE `o_auth2_client_authorization` (
`userId` int(11) DEFAULT 'NULL',
`created_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)'
);
--> statement-breakpoint
CREATE TABLE `o_auth2_client_url` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`url` varchar(255) NOT NULL,
@ -56,7 +54,7 @@ CREATE TABLE `o_auth2_client_url` (
`updated_at` timestamp(6) NOT NULL DEFAULT 'current_timestamp(6)',
`clientId` int(11) DEFAULT 'NULL'
);
--> statement-breakpoint
CREATE TABLE `o_auth2_token` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`type` enum('code','access_token','refresh_token') NOT NULL,
@ -70,12 +68,12 @@ CREATE TABLE `o_auth2_token` (
`updated_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)',
`pcke` text DEFAULT 'NULL'
);
--> statement-breakpoint
CREATE TABLE `privilege` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`name` text NOT NULL
);
--> statement-breakpoint
CREATE TABLE `upload` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`original_name` varchar(255) NOT NULL,
@ -85,7 +83,7 @@ CREATE TABLE `upload` (
`created_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)',
`updated_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)'
);
--> statement-breakpoint
CREATE TABLE `user` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`uuid` varchar(36) NOT NULL,
@ -102,12 +100,12 @@ CREATE TABLE `user` (
CONSTRAINT `IDX_78a916df40e02a9deb1c4b75ed` UNIQUE(`username`),
CONSTRAINT `IDX_e12875dfb3b1d92d7d7c5377e2` UNIQUE(`email`)
);
--> statement-breakpoint
CREATE TABLE `user_privileges_privilege` (
`userId` int(11) NOT NULL,
`privilegeId` int(11) NOT NULL
);
--> statement-breakpoint
CREATE TABLE `user_token` (
`id` int(11) AUTO_INCREMENT NOT NULL,
`token` text NOT NULL,
@ -117,21 +115,21 @@ CREATE TABLE `user_token` (
`nonce` text DEFAULT 'NULL',
`created_at` datetime(6) NOT NULL DEFAULT 'current_timestamp(6)'
);
--> statement-breakpoint
ALTER TABLE `audit_log` ADD CONSTRAINT `FK_cb6aa6f6fd56f08eafb60316225` FOREIGN KEY (`actorId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `document` ADD CONSTRAINT `FK_6a2eb13cadfc503989cbe367572` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client` ADD CONSTRAINT `FK_4a6c878506b872e85b3d07f6252` FOREIGN KEY (`ownerId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client` ADD CONSTRAINT `FK_e8d65b1eec13474e493420517d7` FOREIGN KEY (`pictureId`) REFERENCES `upload`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` ADD CONSTRAINT `FK_8227110f58510b7233f3db90cfb` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` ADD CONSTRAINT `FK_9ca9ebb654e7ce71954d5fdb281` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_url` ADD CONSTRAINT `FK_aca59c7bdd65987487eea98d00f` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_token` ADD CONSTRAINT `FK_3ecb760b321ef9bbab635f05b45` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_token` ADD CONSTRAINT `FK_81ffb9b8d672cf3af1af9e789f3` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `upload` ADD CONSTRAINT `FK_7b8d52838a953b188255682597b` FOREIGN KEY (`uploaderId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user` ADD CONSTRAINT `FK_7478a15985dbfa32ed5fc77a7a1` FOREIGN KEY (`pictureId`) REFERENCES `upload`(`id`) ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` ADD CONSTRAINT `FK_0664a7ff494a1859a09014c0f17` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` ADD CONSTRAINT `FK_e71171f4ed20bc8564a1819d0b7` FOREIGN KEY (`privilegeId`) REFERENCES `privilege`(`id`) ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user_token` ADD CONSTRAINT `FK_d37db50eecdf9b8ce4eedd2f918` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX `IDX_0664a7ff494a1859a09014c0f1` ON `user_privileges_privilege` (`userId`);--> statement-breakpoint
ALTER TABLE `audit_log` ADD CONSTRAINT `FK_cb6aa6f6fd56f08eafb60316225` FOREIGN KEY (`actorId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;
ALTER TABLE `document` ADD CONSTRAINT `FK_6a2eb13cadfc503989cbe367572` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE no action ON UPDATE no action;
ALTER TABLE `o_auth2_client` ADD CONSTRAINT `FK_4a6c878506b872e85b3d07f6252` FOREIGN KEY (`ownerId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;
ALTER TABLE `o_auth2_client` ADD CONSTRAINT `FK_e8d65b1eec13474e493420517d7` FOREIGN KEY (`pictureId`) REFERENCES `upload`(`id`) ON DELETE set null ON UPDATE no action;
ALTER TABLE `o_auth2_client_authorization` ADD CONSTRAINT `FK_8227110f58510b7233f3db90cfb` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;
ALTER TABLE `o_auth2_client_authorization` ADD CONSTRAINT `FK_9ca9ebb654e7ce71954d5fdb281` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;
ALTER TABLE `o_auth2_client_url` ADD CONSTRAINT `FK_aca59c7bdd65987487eea98d00f` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;
ALTER TABLE `o_auth2_token` ADD CONSTRAINT `FK_3ecb760b321ef9bbab635f05b45` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;
ALTER TABLE `o_auth2_token` ADD CONSTRAINT `FK_81ffb9b8d672cf3af1af9e789f3` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;
ALTER TABLE `upload` ADD CONSTRAINT `FK_7b8d52838a953b188255682597b` FOREIGN KEY (`uploaderId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE cascade;
ALTER TABLE `user` ADD CONSTRAINT `FK_7478a15985dbfa32ed5fc77a7a1` FOREIGN KEY (`pictureId`) REFERENCES `upload`(`id`) ON DELETE set null ON UPDATE cascade;
ALTER TABLE `user_privileges_privilege` ADD CONSTRAINT `FK_0664a7ff494a1859a09014c0f17` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE cascade;
ALTER TABLE `user_privileges_privilege` ADD CONSTRAINT `FK_e71171f4ed20bc8564a1819d0b7` FOREIGN KEY (`privilegeId`) REFERENCES `privilege`(`id`) ON DELETE cascade ON UPDATE cascade;
ALTER TABLE `user_token` ADD CONSTRAINT `FK_d37db50eecdf9b8ce4eedd2f918` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;
CREATE INDEX `IDX_0664a7ff494a1859a09014c0f1` ON `user_privileges_privilege` (`userId`);
CREATE INDEX `IDX_e71171f4ed20bc8564a1819d0b` ON `user_privileges_privilege` (`privilegeId`);
*/

View File

@ -0,0 +1,67 @@
CREATE TABLE `o_auth2_client_manager` (
`id` int AUTO_INCREMENT PRIMARY KEY NOT NULL,
`clientId` int NOT NULL,
`userId` int NOT NULL,
`issuerId` int,
`created_at` datetime(6) NOT NULL DEFAULT current_timestamp(6),
`updated_at` datetime(6) NOT NULL DEFAULT current_timestamp(6)
);--> statement-breakpoint
DROP TABLE `migrations`;--> statement-breakpoint
ALTER TABLE `audit_log` DROP FOREIGN KEY `FK_cb6aa6f6fd56f08eafb60316225`;--> statement-breakpoint
ALTER TABLE `document` DROP FOREIGN KEY `FK_6a2eb13cadfc503989cbe367572`;--> statement-breakpoint
ALTER TABLE `o_auth2_client` DROP FOREIGN KEY `FK_4a6c878506b872e85b3d07f6252`;--> statement-breakpoint
ALTER TABLE `o_auth2_client` DROP FOREIGN KEY `FK_e8d65b1eec13474e493420517d7`;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` DROP FOREIGN KEY `FK_8227110f58510b7233f3db90cfb`;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` DROP FOREIGN KEY `FK_9ca9ebb654e7ce71954d5fdb281`;--> statement-breakpoint
ALTER TABLE `o_auth2_client_url` DROP FOREIGN KEY `FK_aca59c7bdd65987487eea98d00f`;--> statement-breakpoint
ALTER TABLE `o_auth2_token` DROP FOREIGN KEY `FK_3ecb760b321ef9bbab635f05b45`;--> statement-breakpoint
ALTER TABLE `o_auth2_token` DROP FOREIGN KEY `FK_81ffb9b8d672cf3af1af9e789f3`;--> statement-breakpoint
ALTER TABLE `upload` DROP FOREIGN KEY `FK_7b8d52838a953b188255682597b`;--> statement-breakpoint
ALTER TABLE `user` DROP FOREIGN KEY `FK_7478a15985dbfa32ed5fc77a7a1`;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` DROP FOREIGN KEY `FK_0664a7ff494a1859a09014c0f17`;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` DROP FOREIGN KEY `FK_e71171f4ed20bc8564a1819d0b7`;--> statement-breakpoint
ALTER TABLE `user_token` DROP FOREIGN KEY `FK_d37db50eecdf9b8ce4eedd2f918`;--> statement-breakpoint
ALTER TABLE `audit_log` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `audit_log` MODIFY COLUMN `actorId` int;--> statement-breakpoint
ALTER TABLE `document` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `document` MODIFY COLUMN `authorId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_client` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `o_auth2_client` MODIFY COLUMN `pictureId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_client` MODIFY COLUMN `ownerId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` MODIFY COLUMN `clientId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` MODIFY COLUMN `userId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_client_url` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `o_auth2_client_url` MODIFY COLUMN `clientId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_token` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `o_auth2_token` MODIFY COLUMN `userId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_token` MODIFY COLUMN `clientId` int;--> statement-breakpoint
ALTER TABLE `privilege` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `upload` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `upload` MODIFY COLUMN `uploaderId` int;--> statement-breakpoint
ALTER TABLE `user` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `user` MODIFY COLUMN `pictureId` int;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` MODIFY COLUMN `userId` int NOT NULL;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` MODIFY COLUMN `privilegeId` int NOT NULL;--> statement-breakpoint
ALTER TABLE `user_token` MODIFY COLUMN `id` int AUTO_INCREMENT NOT NULL;--> statement-breakpoint
ALTER TABLE `user_token` MODIFY COLUMN `userId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` ADD `current` tinyint DEFAULT 1 NOT NULL;--> statement-breakpoint
ALTER TABLE `privilege` ADD `clientId` int;--> statement-breakpoint
ALTER TABLE `o_auth2_client_manager` ADD CONSTRAINT `o_auth2_client_manager_clientId_o_auth2_client_id_fk` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_manager` ADD CONSTRAINT `o_auth2_client_manager_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_manager` ADD CONSTRAINT `o_auth2_client_manager_issuerId_user_id_fk` FOREIGN KEY (`issuerId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `audit_log` ADD CONSTRAINT `audit_log_actorId_user_id_fk` FOREIGN KEY (`actorId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `document` ADD CONSTRAINT `document_authorId_user_id_fk` FOREIGN KEY (`authorId`) REFERENCES `user`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client` ADD CONSTRAINT `o_auth2_client_pictureId_upload_id_fk` FOREIGN KEY (`pictureId`) REFERENCES `upload`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client` ADD CONSTRAINT `o_auth2_client_ownerId_user_id_fk` FOREIGN KEY (`ownerId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` ADD CONSTRAINT `o_auth2_client_authorization_clientId_o_auth2_client_id_fk` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_authorization` ADD CONSTRAINT `o_auth2_client_authorization_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_client_url` ADD CONSTRAINT `o_auth2_client_url_clientId_o_auth2_client_id_fk` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_token` ADD CONSTRAINT `o_auth2_token_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `o_auth2_token` ADD CONSTRAINT `o_auth2_token_clientId_o_auth2_client_id_fk` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `privilege` ADD CONSTRAINT `privilege_clientId_o_auth2_client_id_fk` FOREIGN KEY (`clientId`) REFERENCES `o_auth2_client`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE `upload` ADD CONSTRAINT `upload_uploaderId_user_id_fk` FOREIGN KEY (`uploaderId`) REFERENCES `user`(`id`) ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user` ADD CONSTRAINT `user_pictureId_upload_id_fk` FOREIGN KEY (`pictureId`) REFERENCES `upload`(`id`) ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` ADD CONSTRAINT `user_privileges_privilege_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user_privileges_privilege` ADD CONSTRAINT `user_privileges_privilege_privilegeId_privilege_id_fk` FOREIGN KEY (`privilegeId`) REFERENCES `privilege`(`id`) ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
ALTER TABLE `user_token` ADD CONSTRAINT `user_token_userId_user_id_fk` FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON DELETE cascade ON UPDATE no action;

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,13 @@
"when": 1715871691221,
"tag": "0000_initial",
"breakpoints": true
},
{
"idx": 1,
"version": "5",
"when": 1717232859846,
"tag": "0001_redundant_layla_miller",
"breakpoints": true
}
]
}
}

1
src/app.d.ts vendored
View File

@ -15,6 +15,7 @@ declare global {
interface Locals {
session: Session<SessionData>;
user: User;
privileges: string[];
}
interface PageData {

View File

@ -1,7 +1,12 @@
import { SESSION_SECRET } from '$env/static/private';
import '$lib/server/drizzle';
import { AUTO_MIGRATE, SESSION_SECRET } from '$env/static/private';
import { db } from '$lib/server/drizzle';
import { migrate } from 'drizzle-orm/mysql2/migrator';
import { handleSession } from 'svelte-kit-cookie-session';
if (AUTO_MIGRATE === 'true') {
await migrate(db, { migrationsFolder: './migrations' });
}
export const handle = handleSession({
secret: SESSION_SECRET
});

View File

@ -0,0 +1,73 @@
<script lang="ts">
import type { PaginationMeta } from '$lib/types';
import { page } from '$app/stores';
import { t } from '$lib/i18n';
export let meta: PaginationMeta;
$: pageNum = Number($page.url.searchParams.get('page')) || 1;
$: firstPage = pageNum === 1;
$: lastPage = pageNum === meta.pageCount;
$: pageButtons = Array.from({ length: meta.pageCount }, (_, i) => i + 1);
</script>
<nav class="pager">
<a
class="page-button page-prev {firstPage ? 'disabled' : ''}"
href={`?page=${pageNum - 1}`}
tabindex={firstPage ? -1 : 0}
aria-label={$t('common.previous')}
aria-disabled={firstPage}>&lt;&lt;</a
>
{#each pageButtons as buttonNumber}
{@const active = buttonNumber === pageNum}
<a
class="page-button page-link {active ? 'disabled' : ''}"
href={`?page=${buttonNumber}`}
tabindex={active ? -1 : 0}
aria-label={`${$t('common.page')} ${buttonNumber}`}
aria-disabled={active}>{buttonNumber}</a
>
{/each}
<a
class="page-button page-prev {lastPage ? 'disabled' : ''}"
tabindex={lastPage ? -1 : 0}
href={`?page=${pageNum + 1}`}
aria-label={$t('common.next')}
aria-disabled={lastPage}>&gt;&gt;</a
>
</nav>
<style>
.pager {
display: flex;
align-items: center;
gap: 8px;
}
.page-button {
--in-page-button-size: 28px;
display: block;
background-color: #fff;
color: #000;
min-width: var(--in-page-button-size);
height: var(--in-page-button-size);
line-height: var(--in-page-button-size);
text-align: center;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
border-radius: 4px;
text-decoration: none;
&:hover {
background-color: #ececec;
}
&.disabled {
user-select: none;
pointer-events: none;
background-color: #ececec;
color: #616161;
}
}
</style>

View File

@ -0,0 +1,44 @@
<script lang="ts">
import { PUBLIC_SITE_NAME } from '$env/static/public';
import type { UserSession } from '$lib/types';
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>
</div>
</header>
<style>
.admin-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: #00aaff;
background: linear-gradient(180deg, #00aaff 0%, #005bff 100%);
}
.admin-user {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px 4px 4px;
background-color: #004edf;
border-radius: 40px;
& .admin-user-avatar {
width: 32px;
height: 32px;
border-radius: 100%;
object-fit: contain;
}
}
a {
text-decoration: none;
}
</style>

View File

@ -0,0 +1,79 @@
<script lang="ts">
import type { UserSession } from '$lib/types';
import { hasPrivileges } from '$lib/utils';
import { page } from '$app/stores';
import { t } from '$lib/i18n';
export let user: UserSession;
const links = [
{
href: '/ssoadmin/users',
title: $t('admin.menu.users'),
privileges: ['admin:user']
},
{
href: '/ssoadmin/oauth2',
title: $t('admin.menu.oauth2'),
privileges: [['admin:oauth2', 'self:oauth2']]
},
{
href: '/ssoadmin/audit',
title: $t('admin.menu.audit'),
privileges: ['admin:audit']
}
];
$: entries = links.filter((link) => hasPrivileges(user.privileges || [], link.privileges));
</script>
<aside class="admin-sidebar">
<nav>
<ul>
{#each entries as link}
<li>
<a
href={link.href}
class="sidebar-link{$page.url.pathname.startsWith(link.href) ? ' active' : ''}"
>{link.title}</a
>
</li>
{/each}
</ul>
</nav>
</aside>
<style>
.admin-sidebar {
display: flex;
flex-direction: column;
background-color: #dddddd;
height: 100%;
max-width: 240px;
width: 100%;
border-right: 2px solid #b4b4b4;
& > nav {
margin-top: 16px;
& > ul {
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
list-style-type: none;
& > li > a {
display: block;
padding: 8px 16px;
text-decoration: none;
}
}
}
}
.admin-sidebar,
.admin-sidebar :global(a) {
color: #000;
}
</style>

View File

@ -1,8 +1,10 @@
<section class="page-wrapper">
<div class="page-inner">
<slot />
<main>
<div class="page-wrapper">
<div class="page-inner">
<slot />
</div>
</div>
</section>
</main>
<style>
.page-wrapper {
@ -20,4 +22,11 @@
max-width: 1080px;
width: 100%;
}
main {
min-height: 100vh;
height: 100%;
display: flex;
flex-direction: column;
}
</style>

View File

@ -1,8 +1,8 @@
<div class="aside-wrapper">
<main class="aside-wrapper">
<div class="aside-inner">
<slot />
</div>
</div>
</main>
<style>
.aside-wrapper {

View File

@ -0,0 +1,16 @@
{
"title": "Admin",
"menu": {
"users": "Users",
"oauth2": "OAuth2 clients",
"audit": "Audit logs"
},
"users": {
"title": "Users",
"uuid": "UUID",
"email": "Email",
"privileges": "Privileges",
"activated": "Activated",
"registered": "Registered"
}
}

View File

@ -6,5 +6,12 @@
"manage": "Manage",
"back": "Go back",
"home": "Home page",
"required": "Required fields"
"required": "Required fields",
"page": "Page",
"previous": "Previous",
"next": "Next",
"bool": {
"true": "Yes",
"false": "No"
}
}

View File

@ -20,6 +20,12 @@ const config: Config<Params> = {
locale: 'en',
key: 'oauth2',
loader: async () => await import('./en/oauth2.json')
},
{
locale: 'en',
key: 'admin',
routes: [/\/ssoadmin/],
loader: async () => await import('./en/admin.json')
}
]
};

View File

@ -0,0 +1,11 @@
import { error } from '@sveltejs/kit';
import type { UserSession } from './users';
import { hasPrivileges, type RequiredPrivileges } from '$lib/utils';
export class AdminUtils {
static checkPrivileges(session: UserSession, privileges: RequiredPrivileges): void {
if (!session.privileges?.length || !hasPrivileges(session.privileges, privileges)) {
error(403, 'Forbidden resource');
}
}
}

View File

@ -71,17 +71,33 @@ export const oauth2Client = mysqlTable(
export type OAuth2Client = typeof oauth2Client.$inferSelect;
export type NewOAuth2Client = typeof oauth2Client.$inferInsert;
export const oauth2ClientManager = mysqlTable('o_auth2_client_manager', {
id: int('id').autoincrement().notNull(),
clientId: int('clientId')
.references(() => oauth2Client.id, { onDelete: 'cascade' })
.notNull(),
userId: int('userId')
.references(() => user.id, { onDelete: 'cascade' })
.notNull(),
issuerId: int('issuerId').references(() => user.id, { onDelete: 'set null' }),
created_at: datetime('created_at', { mode: 'date', fsp: 6 })
.default(sql`current_timestamp(6)`)
.notNull(),
updated_at: datetime('updated_at', { mode: 'date', fsp: 6 })
.default(sql`current_timestamp(6)`)
.notNull()
});
export const oauth2ClientAuthorization = mysqlTable('o_auth2_client_authorization', {
id: int('id').autoincrement().notNull(),
scope: text('scope'),
expires_at: timestamp('expires_at', { mode: 'date' })
.default(sql`current_timestamp()`)
.notNull(),
expires_at: timestamp('expires_at', { mode: 'date' }),
clientId: int('clientId').references(() => oauth2Client.id, { onDelete: 'cascade' }),
userId: int('userId').references(() => user.id, { onDelete: 'cascade' }),
created_at: datetime('created_at', { mode: 'date', fsp: 6 })
.default(sql`current_timestamp(6)`)
.notNull()
.notNull(),
current: tinyint('current').default(1).notNull()
});
export type OAuth2ClientAuthorization = typeof oauth2ClientAuthorization.$inferSelect;
@ -128,9 +144,12 @@ export type NewOAuth2Token = typeof oauth2Token.$inferInsert;
export const privilege = mysqlTable('privilege', {
id: int('id').autoincrement().notNull(),
name: text('name').notNull()
name: text('name').notNull(),
clientId: int('clientId').references(() => oauth2Client.id, { onDelete: 'cascade' })
});
export type Privilege = typeof privilege.$inferSelect;
export const upload = mysqlTable('upload', {
id: int('id').autoincrement().notNull(),
original_name: varchar('original_name', { length: 255 }).notNull(),
@ -271,7 +290,23 @@ export const oauth2ClientRelations = relations(oauth2Client, ({ one, many }) =>
}),
o_auth2_client_authorizations: many(oauth2ClientAuthorization),
o_auth2_client_urls: many(oauth2ClientUrl),
o_auth2_tokens: many(oauth2Token)
o_auth2_tokens: many(oauth2Token),
o_auth2_managers: many(oauth2ClientManager)
}));
export const oauth2ClientManagerRelations = relations(oauth2ClientManager, ({ one }) => ({
user: one(user, {
fields: [oauth2ClientManager.userId],
references: [user.id]
}),
issuer: one(user, {
fields: [oauth2ClientManager.issuerId],
references: [user.id]
}),
o_auth2_client: one(oauth2Client, {
fields: [oauth2ClientManager.clientId],
references: [oauth2Client.id]
})
}));
export const uploadRelations = relations(upload, ({ one, many }) => ({
@ -329,8 +364,12 @@ export const userPrivilegesPrivilegeRelations = relations(userPrivilegesPrivileg
})
}));
export const privilegeRelations = relations(privilege, ({ many }) => ({
user_privileges_privileges: many(userPrivilegesPrivilege)
export const privilegeRelations = relations(privilege, ({ one, many }) => ({
user_privileges_privileges: many(userPrivilegesPrivilege),
o_auth2_client: one(oauth2Client, {
fields: [privilege.clientId],
references: [oauth2Client.id]
})
}));
export const userTokenRelations = relations(userToken, ({ one }) => ({

View File

@ -29,7 +29,11 @@ export class OAuth2Users {
.from(oauth2ClientAuthorization)
.innerJoin(oauth2Client, eq(oauth2ClientAuthorization.clientId, oauth2Client.id))
.where(
and(eq(oauth2Client.client_id, clientId), eq(oauth2ClientAuthorization.userId, userId))
and(
eq(oauth2Client.client_id, clientId),
eq(oauth2ClientAuthorization.userId, userId),
eq(oauth2ClientAuthorization.current, 1)
)
)
).filter(({ scope }) => {
const splitScope = OAuth2Clients.splitScope(scope || '');
@ -60,7 +64,7 @@ export class OAuth2Users {
await db
.update(oauth2ClientAuthorization)
.set({ scope: OAuth2Clients.joinScope(splitScope) })
.set({ scope: OAuth2Clients.joinScope(splitScope), current: 1, expires_at: null })
.where(eq(oauth2ClientAuthorization.id, existing.id));
return;
}
@ -78,11 +82,13 @@ export class OAuth2Users {
await OAuth2Tokens.wipeClientTokens(client, subject);
await db
.delete(oauth2ClientAuthorization)
.update(oauth2ClientAuthorization)
.set({ current: 0, expires_at: new Date() })
.where(
and(
eq(oauth2ClientAuthorization.userId, subject.id),
eq(oauth2ClientAuthorization.clientId, client.id)
eq(oauth2ClientAuthorization.clientId, client.id),
eq(oauth2ClientAuthorization.current, 1)
)
);
@ -95,7 +101,12 @@ export class OAuth2Users {
.from(oauth2Client)
.innerJoin(oauth2ClientAuthorization, eq(oauth2ClientAuthorization.clientId, oauth2Client.id))
.leftJoin(oauth2ClientUrl, eq(oauth2ClientUrl.clientId, oauth2Client.id))
.where(and(eq(oauth2ClientAuthorization.userId, subject.id)));
.where(
and(
eq(oauth2ClientAuthorization.userId, subject.id),
eq(oauth2ClientAuthorization.current, 1)
)
);
}
static async issueIdToken(subject: User, client: OAuth2Client, scope: string[], nonce?: string) {

View File

@ -62,6 +62,6 @@ export class Uploads {
file: newName,
uploaderId: subject.id
});
await db.update(user).set({ pictureId: retval.insertId });
await db.update(user).set({ pictureId: retval.insertId }).where(eq(user.id, subject.id));
}
}

View File

@ -0,0 +1,79 @@
import { count, eq, ilike, like, or } from 'drizzle-orm';
import {
db,
privilege,
user,
userPrivilegesPrivilege,
type Privilege,
type User
} from '../drizzle';
import type { Paginated, PaginationMeta } from '$lib/types';
export interface AdminUserListItem extends Omit<User, 'password'> {
privileges: Privilege[];
}
export class UsersAdmin {
static async getAllUsers({
filter,
offset = 0,
limit = 20
}: {
filter?: string;
offset?: number;
limit?: number;
}) {
const subfilter = `%${filter}%`;
const searchExpression = filter
? or(
like(user.uuid, subfilter),
ilike(user.username, subfilter),
ilike(user.display_name, subfilter),
ilike(user.email, subfilter)
)
: undefined;
const [{ rowCount }] = await db
.select({ rowCount: count(user.id).mapWith(Number) })
.from(user)
.where(searchExpression);
const junkList = await db
.select()
.from(user)
.leftJoin(userPrivilegesPrivilege, eq(userPrivilegesPrivilege.userId, user.id))
.leftJoin(privilege, eq(userPrivilegesPrivilege.privilegeId, privilege.id))
.where(searchExpression)
.limit(limit)
.offset(offset);
const meta: PaginationMeta = {
rowCount,
pageSize: limit,
pageCount: Math.ceil(rowCount / limit)
};
const list = junkList.reduce<AdminUserListItem[]>((accum, dbe) => {
let user = accum.find((entry) => entry.id === dbe.user.id);
if (!user) {
user = { ...dbe.user, password: undefined, privileges: [] } as AdminUserListItem;
accum.push(user);
}
if (dbe.user_privileges_privilege && dbe.privilege) {
if (
!user.privileges.some((priv) => priv.id === dbe.user_privileges_privilege?.privilegeId)
) {
user.privileges.push(dbe.privilege);
}
}
return accum;
}, []);
return <Paginated<AdminUserListItem>>{
list,
meta
};
}
}

View File

@ -1,6 +1,6 @@
import bcrypt from 'bcryptjs';
import { and, eq, or, sql } from 'drizzle-orm';
import { db, user, type User } from '../drizzle';
import { db, privilege, user, userPrivilegesPrivilege, type User } from '../drizzle';
import type { UserSession } from './types';
import { redirect } from '@sveltejs/kit';
import { CryptoUtils } from '../crypto-utils';
@ -214,6 +214,21 @@ export class Users {
}
}
static async getUserPrivileges(subject: User) {
const list = await db
.select({
privilege: privilege.name
})
.from(privilege)
.innerJoin(userPrivilegesPrivilege, eq(privilege.id, userPrivilegesPrivilege.privilegeId))
.where(eq(userPrivilegesPrivilege.userId, subject.id));
return list.reduce<string[]>(
(accum, { privilege }) => (!accum.includes(privilege) ? [...accum, privilege] : accum),
[]
);
}
static anonymizeEmail(email: string) {
const [name, domain] = email.split('@');
const namePart = `${name.charAt(0)}${''.padStart(name.length - 2, '*')}${name.charAt(name.length - 1)}`;

View File

@ -3,4 +3,5 @@ export interface UserSession {
uuid: string;
name: string;
username: string;
privileges?: string[];
}

18
src/lib/types.ts Normal file
View File

@ -0,0 +1,18 @@
export interface UserSession {
uid: number;
uuid: string;
name: string;
username: string;
privileges?: string[];
}
export interface PaginationMeta {
rowCount: number;
pageSize: number;
pageCount: number;
}
export interface Paginated<T> {
list: T[];
meta: PaginationMeta;
}

9
src/lib/utils.ts Normal file
View File

@ -0,0 +1,9 @@
export type RequiredPrivileges = (string | string[])[];
export const hasPrivileges = (list: string[], privileges: RequiredPrivileges) =>
privileges.every((item) => {
if (Array.isArray(item)) {
return item.some((sub) => list.includes(sub));
}
return list.includes(item);
});

View File

@ -1,16 +1,5 @@
<script lang="ts">
import '../app.css'
import '../app.css';
</script>
<main>
<slot></slot>
</main>
<style>
main {
min-height: 100vh;
height: 100%;
display: flex;
flex-direction: column;
}
</style>
<slot></slot>

View File

@ -1 +0,0 @@
<slot />

View File

@ -0,0 +1,24 @@
import { Users } from '$lib/server/users/index.js';
import { error, redirect } from '@sveltejs/kit';
export const load = async ({ url, locals }) => {
const userInfo = locals.session.data?.user;
const currentUser = await Users.getBySession(userInfo);
if (!userInfo || !currentUser) {
await locals.session.destroy();
return redirect(301, `/login?redirectTo=${encodeURIComponent(url.pathname)}`);
}
// Only users with 'admin' privilege can access
const privileges = await Users.getUserPrivileges(currentUser);
if (!privileges.includes('admin')) {
return error(404, 'Not Found');
}
return {
user: {
...userInfo,
privileges
}
};
};

View File

@ -0,0 +1,42 @@
<script lang="ts">
import AdminHeader from '$lib/components/admin/AdminHeader.svelte';
import AdminSidebar from '$lib/components/admin/AdminSidebar.svelte';
import type { PageData } from './$types';
export let data: PageData;
</script>
<div class="admin-wrapper">
<AdminHeader user={data.user} />
<div class="sidebar-wrapper">
<AdminSidebar user={data.user} />
<main>
<slot />
</main>
</div>
</div>
<style>
.admin-wrapper {
display: flex;
flex-direction: column;
height: 100%;
flex-grow: 1;
overflow: hidden;
}
.sidebar-wrapper {
display: flex;
flex-grow: 1;
overflow: hidden;
& > main {
overflow: auto;
flex-grow: 1;
background-color: #ececec;
color: #000;
padding: 16px;
}
}
</style>

View File

@ -0,0 +1,3 @@
export const load = async () => {
return {};
};

View File

View File

@ -0,0 +1,28 @@
import { AdminUtils } from '$lib/server/admin-utils.js';
import { UsersAdmin } from '$lib/server/users/admin.js';
const PAGE_SIZE = 20;
export const load = async ({ parent, url }) => {
const { user } = await parent();
AdminUtils.checkPrivileges(user, ['admin:user']);
let limit = PAGE_SIZE;
let page = 1;
let filter: string | undefined = undefined;
if (url.searchParams.has('page')) {
page = Number(url.searchParams.get('page')) || 1;
}
if (url.searchParams.has('pageSize')) {
limit = Number(url.searchParams.get('pageSize')) || PAGE_SIZE;
}
if (url.searchParams.has('filter')) {
filter = url.searchParams.get('filter') as string;
}
const offset = (page - 1) * limit;
return await UsersAdmin.getAllUsers({ filter, limit, offset });
};

View File

@ -0,0 +1,102 @@
<script lang="ts">
import Paginator from '$lib/components/Paginator.svelte';
import { t } from '$lib/i18n';
import type { PageData } from './$types';
export let data: PageData;
const dateFormat = new Intl.DateTimeFormat('en-GB', { dateStyle: 'short', timeStyle: 'medium' });
const formatDate = dateFormat.format.bind(null);
</script>
<h1>{$t('admin.users.title')}</h1>
<div class="user-list">
<Paginator meta={data.meta} />
{#each data.list as user}
<a href={`users/${user.uuid}`} class="user-wrapper">
<div class="user">
<div class="user-avatar-wrapper">
<img class="user-avatar" src={`/api/avatar/${user.uuid}`} alt={user.display_name} />
</div>
<div class="user-info">
<h2 class="user-name">{user.display_name}</h2>
<span class="user-username">@{user.username}</span>
<dl>
<dt>{$t('admin.users.uuid')}</dt>
<dd>{user.uuid}</dd>
<dt>{$t('admin.users.email')}</dt>
<dd>{user.email}</dd>
{#if user.privileges.length}
<dt>{$t('admin.users.privileges')}</dt>
<dd>{user.privileges.map(({ name }) => name).join(', ')}</dd>
{/if}
<dt>{$t('admin.users.activated')}</dt>
<dd>{$t(`common.bool.${Boolean(user.activated)}`)}</dd>
<dt>{$t('admin.users.registered')}</dt>
<dd>{formatDate(user.created_at)}</dd>
</dl>
</div>
</div>
</a>
{/each}
<Paginator meta={data.meta} />
</div>
<style>
.user-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.user-wrapper {
color: #000;
text-decoration: none;
display: flex;
transition: transform 100ms linear;
&:hover {
transform: scale(1.01);
}
}
.user {
display: flex;
gap: 1rem;
padding: 8px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.25);
flex-grow: 1;
& .user-avatar {
width: 128px;
height: 128px;
object-fit: contain;
}
& .user-info {
display: flex;
flex-direction: column;
& > dl {
margin: 0;
& > dt {
font-weight: 600;
margin-top: 0.5rem;
}
}
}
& .user-name {
margin: 0;
}
}
</style>