filters, page titles

This commit is contained in:
Evert Prants 2024-06-02 14:53:31 +03:00
parent c82ed0e9aa
commit 243fe982b1
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
16 changed files with 516 additions and 57 deletions

View File

@ -86,7 +86,7 @@
}
.transfer-box > :global(.column):nth-child(2) {
margin-top: 1.35rem;
margin-top: 1.8rem;
}
.transfer-box :global(.form-control) {

View File

@ -8,7 +8,7 @@
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1.25rem;
margin-bottom: 1.3rem;
}
.title-row > :global(*):first-child {
margin: 0;

View File

@ -30,6 +30,7 @@
},
"oauth2": {
"title": "OAuth2 applications",
"new": "Create a new application",
"clientTitle": "Application name",
"clientId": "Client ID",
"clientSecret": "Client secret",
@ -64,7 +65,8 @@
"addHint": "You may assign application-specific privileges to your authorized users. You may use this system for permissions or for tagging your users, all up to you!",
"remove": "Remove",
"manage": "Manage user privileges",
"new": "Add a new privilege"
"new": "Add a new privilege",
"edit": "Edit privileges"
},
"urls": {
"title": "Application URLs",
@ -79,6 +81,14 @@
},
"add": "Add URL"
},
"apis": {
"title": "OAuth2 APIs",
"authorize": "OAuth2 Authorization endpoint",
"token": "OAuth2 Token endpoint",
"introspect": "OAuth2 Introspection endpoint",
"userinfo": "User information endpoint (Bearer)",
"openid": "OpenID Connect configuration"
},
"grantTexts": {
"authorization_code": "Authorization code",
"client_credentials": "Client credentials",

View File

@ -16,5 +16,6 @@
},
"available": "Available",
"current": "Current",
"remove": "Remove"
"remove": "Remove",
"filter": "Filter"
}

View File

@ -1,17 +1,20 @@
import { CryptoUtils } from '$lib/server/crypto-utils';
import {
db,
oauth2Client,
oauth2ClientAuthorization,
oauth2ClientManager,
oauth2ClientUrl,
privilege,
user,
userPrivilegesPrivilege,
type OAuth2Client,
type OAuth2ClientUrl,
type User
} from '$lib/server/drizzle';
import { Uploads } from '$lib/server/upload';
import type { PaginationMeta } from '$lib/types';
import { and, count, eq, ilike, like, or } from 'drizzle-orm';
import { and, count, eq, like, or, sql } from 'drizzle-orm';
export enum OAuth2ClientURLType {
REDIRECT_URI = 'redirect_uri',
@ -27,6 +30,17 @@ export interface OAuth2ClientAdminListItem
urls: Omit<OAuth2ClientUrl, 'created_at' | 'updated_at' | 'clientId'>[];
}
export interface OAuth2AuthorizedUser {
uuid: string;
name: string;
current: boolean;
privileges: { id: number; name: string }[];
}
export interface OAuth2ManagerUser {
email: string;
}
export class OAuth2Clients {
public static availableGrantTypes = [
'authorization_code',
@ -109,6 +123,52 @@ export class OAuth2Clients {
)?.length;
}
static async getAuthorizedUsers(client: OAuth2Client, userUuid?: string) {
const junkList = await db
.select()
.from(oauth2ClientAuthorization)
.innerJoin(user, eq(user.id, oauth2ClientAuthorization.userId))
.leftJoin(userPrivilegesPrivilege, eq(userPrivilegesPrivilege.userId, user.id))
.leftJoin(
privilege,
and(
eq(userPrivilegesPrivilege.privilegeId, privilege.id),
eq(privilege.clientId, client.id)
)
)
.where(
and(
eq(oauth2ClientAuthorization.clientId, client.id),
userUuid ? eq(user.uuid, userUuid) : undefined
)
);
const list = junkList.reduce<OAuth2AuthorizedUser[]>((accum, dbo) => {
let user = accum.find((entry) => entry.uuid === dbo.user.uuid);
if (!user) {
user = {
uuid: dbo.user.uuid,
name: dbo.user.display_name,
current: false,
privileges: []
};
accum.push(user);
}
if (dbo.privilege && !user.privileges.some(({ id }) => id === dbo.privilege?.id)) {
user.privileges.push({ id: dbo.privilege.id, name: dbo.privilege.name });
}
if (dbo.o_auth2_client_authorization?.current === 1) {
user.current = true;
}
return accum;
}, []);
return list;
}
static checkSecret(client: OAuth2Client, secret: string) {
return client.client_secret === secret;
}
@ -152,7 +212,7 @@ export class OAuth2Clients {
listAll?: boolean;
}
) {
const filterText = `%${filters?.filter}%`;
const filterText = `%${filters?.filter?.toLowerCase()}%`;
const limit = filters?.limit || 20;
const allowedClients = db
.select({ id: oauth2Client.id })
@ -165,7 +225,10 @@ export class OAuth2Clients {
: undefined,
filters?.clientId ? eq(oauth2Client.client_id, filters.clientId) : undefined,
filters?.filter
? or(ilike(oauth2Client.title, filterText), like(oauth2Client.client_id, filterText))
? or(
sql`lower(${oauth2Client.title}) like ${filterText}`,
like(oauth2Client.client_id, filterText)
)
: undefined
)
)
@ -240,6 +303,32 @@ export class OAuth2Clients {
};
}
static async createClient(subject: User, title: string, redirect: string, description?: string) {
const uid = CryptoUtils.createUUID();
const secret = CryptoUtils.generateSecret();
const [retval] = await db.insert(oauth2Client).values({
title,
description,
client_id: uid,
client_secret: secret,
grants: 'authorization_code',
scope: 'profile',
ownerId: subject.id,
created_at: new Date(),
activated: 1,
verified: 0
});
await db.insert(oauth2ClientUrl).values({
type: 'redirect_uri',
url: redirect,
clientId: retval.insertId
});
return uid;
}
static async deleteClient(client: OAuth2Client) {
if (client.pictureId) {
await Uploads.removeClientAvatar(client);

View File

@ -1,4 +1,4 @@
import { count, eq, ilike, like, or } from 'drizzle-orm';
import { count, eq, like, or, sql } from 'drizzle-orm';
import {
db,
privilege,
@ -58,9 +58,9 @@ export class UsersAdmin {
const searchExpression = filter
? or(
like(user.uuid, subfilter),
ilike(user.username, subfilter),
ilike(user.display_name, subfilter),
ilike(user.email, subfilter)
sql`lower(${user.username}) like ${subfilter}`,
sql`lower(${user.display_name}) like ${subfilter}`,
sql`lower(${user.email}) like ${subfilter}`
)
: undefined;

View File

@ -241,13 +241,19 @@ export class Users {
);
}
static async setUserPrivileges(subject: User, privilegeIds: number[]) {
static async setUserPrivileges(subject: User, privilegeIds: number[], clientId?: number) {
const current = await db
.select({
privilegeId: userPrivilegesPrivilege.privilegeId
})
.from(userPrivilegesPrivilege)
.where(eq(userPrivilegesPrivilege.userId, subject.id));
.innerJoin(privilege, eq(privilege.id, userPrivilegesPrivilege.privilegeId))
.where(
and(
eq(userPrivilegesPrivilege.userId, subject.id),
clientId ? eq(privilege.clientId, clientId) : undefined
)
);
const toRemoveIds = current.reduce<number[]>(
(list, { privilegeId }) =>

View File

@ -3,13 +3,33 @@
import { t } from '$lib/i18n';
import type { PageData } from './$types';
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 FormControl from '$lib/components/form/FormControl.svelte';
import { page } from '$app/stores';
export let data: PageData;
</script>
<h1>{$t('admin.oauth2.title')}</h1>
<svelte:head>
<title>{$t('admin.oauth2.title')} - {PUBLIC_SITE_NAME} {$t('admin.title')}</title>
</svelte:head>
<div class="client-list">
<TitleRow>
<h1>{$t('admin.oauth2.title')}</h1>
<a href="oauth2/new">{$t('admin.oauth2.new')}</a>
</TitleRow>
<ColumnView>
<form action="" method="get">
<FormControl>
<label for="filter">{$t('common.filter')}</label>
<input name="filter" value={$page.url.searchParams.get('filter')} />
</FormControl>
</form>
<div class="client-list">
<Paginator meta={data.meta} />
{#each data.list as client}
<a href={`oauth2/${client.client_id}`} class="client-link">
@ -17,7 +37,8 @@
</a>
{/each}
<Paginator meta={data.meta} />
</div>
</div>
</ColumnView>
<style>
.client-link {

View File

@ -32,11 +32,13 @@ export const actions = {
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -44,8 +46,6 @@ export const actions = {
return error(404, 'Client not found');
}
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const body = await request.formData();
const { title, description, activated, verified } = Changesets.take<UpdateRequest>(
['title', 'description', 'activated', 'verified'],
@ -81,11 +81,13 @@ export const actions = {
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -97,7 +99,6 @@ export const actions = {
return fail(400, { errors: ['deleteActivated'] });
}
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
if (details.ownerId !== currentUser.id && !fullPrivileges) {
return fail(403, { errors: ['forbidden'] });
}
@ -111,11 +112,13 @@ export const actions = {
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -123,7 +126,6 @@ export const actions = {
return error(404, 'Client not found');
}
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
if (!fullPrivileges && !details.isOwner) {
return fail(403, { errors: ['forbidden'] });
}
@ -135,15 +137,17 @@ export const actions = {
return { errors: [] };
},
removeUrl: async ({ locals, url, params: { uuid } }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -161,15 +165,17 @@ export const actions = {
return { errors: [] };
},
addUrl: async ({ locals, request, params: { uuid } }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -192,15 +198,17 @@ export const actions = {
return { errors: [] };
},
removePrivilege: async ({ locals, url, params: { uuid } }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -218,15 +226,17 @@ export const actions = {
return { errors: [] };
},
addPrivilege: async ({ locals, request, params: { uuid } }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -250,11 +260,13 @@ export const actions = {
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -262,7 +274,6 @@ export const actions = {
return error(404, 'Client not found');
}
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const allowedGrants = fullPrivileges
? OAuth2Clients.availableGrantTypes
: OAuth2Clients.userSetGrants;
@ -285,11 +296,13 @@ export const actions = {
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -297,7 +310,6 @@ export const actions = {
return error(404, 'Client not found');
}
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const allowedScopes = fullPrivileges
? OAuth2Clients.availableScopes
: OAuth2Clients.userSetScopes;
@ -316,15 +328,17 @@ export const actions = {
return { errors: [] };
},
avatar: async ({ request, locals, params: { uuid } }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -346,15 +360,17 @@ export const actions = {
return { errors: [] };
},
removeAvatar: async ({ locals, params: { uuid } }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -379,7 +395,7 @@ export const load = async ({ params: { uuid }, parent }) => {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
listAll: fullPrivileges,
omitSecret: false
});
@ -388,8 +404,10 @@ export const load = async ({ params: { uuid }, parent }) => {
}
const privileges = await Users.getAvailablePrivileges(details.id);
const users = await OAuth2Clients.getAuthorizedUsers(details as OAuth2Client);
return {
users,
availableUrls: OAuth2Clients.availableUrlTypes,
availablePrivileges: privileges,
availableGrants: fullPrivileges

View File

@ -14,6 +14,7 @@
import type { ActionData, PageData } from './$types';
import { writable } from 'svelte/store';
import FormErrors from '$lib/components/form/FormErrors.svelte';
import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public';
export let data: PageData;
export let form: ActionData;
@ -37,6 +38,13 @@
$: uuidPrefix = data.details.client_id.split('-')[0] + ':';
</script>
<svelte:head>
<title
>{$t('admin.oauth2.title')} / {data.details.title} - {PUBLIC_SITE_NAME}
{$t('admin.title')}</title
>
</svelte:head>
<h1>{$t('admin.oauth2.title')} / {data.details.title}</h1>
<ColumnView>
@ -282,16 +290,74 @@
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
</form>
<h2>{$t('admin.oauth2.apis.title')}</h2>
<ul>
<li>
{$t('admin.oauth2.apis.authorize')} -
<code
><a href={`/oauth2/authorize`} data-sveltekit-preload-data="off"
>{PUBLIC_URL}/oauth2/authorize</a
></code
>
</li>
<li>
{$t('admin.oauth2.apis.token')} -
<code
><a href={`/oauth2/token`} data-sveltekit-preload-data="off">{PUBLIC_URL}/oauth2/token</a
></code
>
</li>
<li>
{$t('admin.oauth2.apis.introspect')} -
<code
><a href={`/oauth2/introspect`} data-sveltekit-preload-data="off"
>{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>
</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
></code
>
</li>
</ul>
<h2>{$t('admin.oauth2.authorizations')}</h2>
<p>{$t('admin.oauth2.authorizationsHint')}</p>
<div class="addremove">
{#each data.users as user}
<div class="auth-user addremove-item">
<div>
<div class="auth-user-name">
<b>{user.uuid}</b> ({user.name})
</div>
<div class="auth-user-privileges">
{user.privileges.map(({ name }) => name).join(', ')}
</div>
{#if !user.current}
<b>{$t('admin.oauth2.revoked')}</b>
{/if}
</div>
<a href={`${$page.url.pathname}/user/${user.uuid}`}>{$t('admin.oauth2.privileges.edit')}</a>
</div>
{:else}
<b>{$t('admin.oauth2.noAuthorizations')}</b>
{/each}
</div>
</ColumnView>
<AvatarModal show={showAvatarModal} url={$page.url.pathname} />
<style>
h2,
ul,
p {
margin: 0;
}

View File

@ -0,0 +1,94 @@
import { AdminUtils } from '$lib/server/admin-utils';
import { Changesets } from '$lib/server/changesets.js';
import type { OAuth2Client, User } from '$lib/server/drizzle';
import { OAuth2Clients } from '$lib/server/oauth2';
import { Users } from '$lib/server/users';
import { UsersAdmin } from '$lib/server/users/admin';
import { error, redirect } from '@sveltejs/kit';
interface PrivilegesRequest {
privileges: string;
}
export const actions = {
privileges: async ({ locals, params: { uuid, user: userId }, request }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
omitSecret: false
});
if (!details) {
return error(404, 'Client not found');
}
const [auth] = await OAuth2Clients.getAuthorizedUsers(details as OAuth2Client, userId);
if (!auth) {
return error(404, 'User not found');
}
const targetUser = await Users.getByUuid(userId);
if (!targetUser) {
return error(404, 'User not found');
}
const availablePrivileges = await Users.getAvailablePrivileges(details.id);
const body = await request.formData();
const { privileges } = Changesets.take<PrivilegesRequest>(['privileges'], body);
const splitFilter = (privileges || '').split(',').reduce<number[]>((final, id) => {
const privId = Number(id);
if (
isNaN(privId) ||
final.includes(privId) ||
!availablePrivileges.some((entry) => entry.id === privId)
) {
return final;
}
return [...final, privId];
}, []);
await Users.setUserPrivileges(targetUser, splitFilter, details.id);
return redirect(303, '..');
}
};
export const load = async ({ params: { uuid, user: userId }, parent }) => {
const { user } = await parent();
const currentUser = await Users.getBySession(user);
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]);
const {
list: [details]
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
clientId: uuid,
listAll: false,
omitSecret: false
});
if (!details) {
return error(404, 'Client not found');
}
const [auth] = await OAuth2Clients.getAuthorizedUsers(details as OAuth2Client, userId);
if (!auth) {
return error(404, 'User not found');
}
const availablePrivileges = await Users.getAvailablePrivileges(details.id);
return {
auth,
details,
availablePrivileges
};
};

View File

@ -0,0 +1,38 @@
<script lang="ts">
import { PUBLIC_SITE_NAME } from '$env/static/public';
import Button from '$lib/components/Button.svelte';
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
import ButtonRow from '$lib/components/container/ButtonRow.svelte';
import FormControl from '$lib/components/form/FormControl.svelte';
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
import { t } from '$lib/i18n';
import type { PageData } from './$types';
export let data: PageData;
</script>
<svelte:head>
<title
>{$t('admin.oauth2.privileges.edit')} / {data.details.title} - {PUBLIC_SITE_NAME}
{$t('admin.title')}</title
>
</svelte:head>
<h1>{$t('admin.oauth2.privileges.edit')} / {data.details.title}</h1>
<h2>{data.auth.name}</h2>
<form action="?/privileges" method="POST">
<FormWrapper>
<FormControl>
<label for="form-uuid">{$t('admin.users.uuid')}</label>
<input id="form-uuid" disabled value={data.auth.uuid} />
</FormControl>
<AdminPrivilegesSelect available={data.availablePrivileges} current={data.auth.privileges} />
<ButtonRow>
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
<a href="../">{$t('common.cancel')}</a>
</ButtonRow>
</FormWrapper>
</form>

View File

@ -0,0 +1,48 @@
import { AdminUtils } from '$lib/server/admin-utils';
import { Changesets } from '$lib/server/changesets.js';
import { OAuth2Clients } from '$lib/server/oauth2/index.js';
import { UsersAdmin } from '$lib/server/users/admin';
import { fail, redirect } from '@sveltejs/kit';
interface CreateClientRequest {
title: string;
description: string;
redirectUri: string;
}
export const actions = {
default: async ({ locals, request }) => {
const { currentUser } = await UsersAdmin.getActionUser(locals, [
['admin:oauth2', 'self:oauth2']
]);
const body = await request.formData();
const { title, description, redirectUri } = Changesets.take<CreateClientRequest>(
['title', 'description', 'redirectUri'],
body
);
if (!title || title.length < 3 || title.length > 32) {
return fail(400, { errors: ['invalidTitle'] });
}
if (description && description.length > 1000) {
return fail(400, { errors: ['invalidDescription'] });
}
if (!redirectUri) {
return fail(400, { errors: ['noRedirect'] });
}
const uuid = await OAuth2Clients.createClient(currentUser, title, redirectUri, description);
return redirect(303, `/ssoadmin/oauth2/${uuid}`);
}
};
export const load = async ({ parent }) => {
const { user } = await parent();
AdminUtils.checkPrivileges(user, [['admin:oauth2', 'self:oauth2']]);
return {};
};

View File

@ -0,0 +1,43 @@
<script lang="ts">
import FormControl from '$lib/components/form/FormControl.svelte';
import FormSection from '$lib/components/form/FormSection.svelte';
import FormWrapper from '$lib/components/form/FormWrapper.svelte';
import FormErrors from '$lib/components/form/FormErrors.svelte';
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';
export let form: ActionData;
</script>
<svelte:head>
<title>{$t('admin.oauth2.new')} - {PUBLIC_SITE_NAME} {$t('admin.title')}</title>
</svelte:head>
<h1>{$t('admin.oauth2.new')}</h1>
<form action="" method="POST">
<FormWrapper>
<FormErrors errors={form?.errors || []} prefix="admin.oauth2.errors" />
<FormSection required>
<FormControl>
<label for="form-title">{$t('admin.oauth2.clientTitle')}</label>
<input name="title" id="form-title" required />
</FormControl>
<FormControl>
<label for="form-redirectUri">{$t('admin.oauth2.urls.types.redirect_uri')}</label>
<input name="redirectUri" type="url" id="form-redirectUri" required />
</FormControl>
<FormControl>
<label for="client-description">{$t('admin.oauth2.description')}</label>
<textarea name="description" id="client-description" rows="3" />
</FormControl>
</FormSection>
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
</FormWrapper>
</form>

View File

@ -3,13 +3,29 @@
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 FormControl from '$lib/components/form/FormControl.svelte';
import ColumnView from '$lib/components/container/ColumnView.svelte';
import { page } from '$app/stores';
export let data: PageData;
</script>
<svelte:head>
<title>{$t('admin.users.title')} - {PUBLIC_SITE_NAME} {$t('admin.title')}</title>
</svelte:head>
<h1>{$t('admin.users.title')}</h1>
<div class="user-list">
<ColumnView>
<form action="" method="get">
<FormControl>
<label for="filter">{$t('common.filter')}</label>
<input name="filter" value={$page.url.searchParams.get('filter')} />
</FormControl>
</form>
<div class="user-list">
<Paginator meta={data.meta} />
{#each data.list as user}
<a href={`users/${user.uuid}`} class="user-link">
@ -17,7 +33,8 @@
</a>
{/each}
<Paginator meta={data.meta} />
</div>
</div>
</ColumnView>
<style>
.user-link {

View File

@ -10,11 +10,19 @@
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';
export let data: PageData;
export let form: ActionData;
</script>
<svelte:head>
<title
>{$t('admin.users.title')} / {data.details.display_name} - {PUBLIC_SITE_NAME}
{$t('admin.title')}</title
>
</svelte:head>
<h1>{$t('admin.users.title')} / {data.details.display_name}</h1>
<SplitView>