filters, page titles
This commit is contained in:
parent
c82ed0e9aa
commit
243fe982b1
@ -86,7 +86,7 @@
|
||||
}
|
||||
|
||||
.transfer-box > :global(.column):nth-child(2) {
|
||||
margin-top: 1.35rem;
|
||||
margin-top: 1.8rem;
|
||||
}
|
||||
|
||||
.transfer-box :global(.form-control) {
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -16,5 +16,6 @@
|
||||
},
|
||||
"available": "Available",
|
||||
"current": "Current",
|
||||
"remove": "Remove"
|
||||
"remove": "Remove",
|
||||
"filter": "Filter"
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 }) =>
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
};
|
38
src/routes/ssoadmin/oauth2/[uuid]/user/[user]/+page.svelte
Normal file
38
src/routes/ssoadmin/oauth2/[uuid]/user/[user]/+page.svelte
Normal 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>
|
48
src/routes/ssoadmin/oauth2/new/+page.server.ts
Normal file
48
src/routes/ssoadmin/oauth2/new/+page.server.ts
Normal 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 {};
|
||||
};
|
43
src/routes/ssoadmin/oauth2/new/+page.svelte
Normal file
43
src/routes/ssoadmin/oauth2/new/+page.svelte
Normal 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>
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user