filters, page titles
This commit is contained in:
parent
c82ed0e9aa
commit
243fe982b1
@ -86,7 +86,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.transfer-box > :global(.column):nth-child(2) {
|
.transfer-box > :global(.column):nth-child(2) {
|
||||||
margin-top: 1.35rem;
|
margin-top: 1.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transfer-box :global(.form-control) {
|
.transfer-box :global(.form-control) {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 1.25rem;
|
margin-bottom: 1.3rem;
|
||||||
}
|
}
|
||||||
.title-row > :global(*):first-child {
|
.title-row > :global(*):first-child {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
},
|
},
|
||||||
"oauth2": {
|
"oauth2": {
|
||||||
"title": "OAuth2 applications",
|
"title": "OAuth2 applications",
|
||||||
|
"new": "Create a new application",
|
||||||
"clientTitle": "Application name",
|
"clientTitle": "Application name",
|
||||||
"clientId": "Client ID",
|
"clientId": "Client ID",
|
||||||
"clientSecret": "Client secret",
|
"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!",
|
"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",
|
"remove": "Remove",
|
||||||
"manage": "Manage user privileges",
|
"manage": "Manage user privileges",
|
||||||
"new": "Add a new privilege"
|
"new": "Add a new privilege",
|
||||||
|
"edit": "Edit privileges"
|
||||||
},
|
},
|
||||||
"urls": {
|
"urls": {
|
||||||
"title": "Application URLs",
|
"title": "Application URLs",
|
||||||
@ -79,6 +81,14 @@
|
|||||||
},
|
},
|
||||||
"add": "Add URL"
|
"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": {
|
"grantTexts": {
|
||||||
"authorization_code": "Authorization code",
|
"authorization_code": "Authorization code",
|
||||||
"client_credentials": "Client credentials",
|
"client_credentials": "Client credentials",
|
||||||
|
@ -16,5 +16,6 @@
|
|||||||
},
|
},
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
"current": "Current",
|
"current": "Current",
|
||||||
"remove": "Remove"
|
"remove": "Remove",
|
||||||
|
"filter": "Filter"
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
|
import { CryptoUtils } from '$lib/server/crypto-utils';
|
||||||
import {
|
import {
|
||||||
db,
|
db,
|
||||||
oauth2Client,
|
oauth2Client,
|
||||||
|
oauth2ClientAuthorization,
|
||||||
oauth2ClientManager,
|
oauth2ClientManager,
|
||||||
oauth2ClientUrl,
|
oauth2ClientUrl,
|
||||||
privilege,
|
privilege,
|
||||||
user,
|
user,
|
||||||
|
userPrivilegesPrivilege,
|
||||||
type OAuth2Client,
|
type OAuth2Client,
|
||||||
type OAuth2ClientUrl,
|
type OAuth2ClientUrl,
|
||||||
type User
|
type User
|
||||||
} from '$lib/server/drizzle';
|
} from '$lib/server/drizzle';
|
||||||
import { Uploads } from '$lib/server/upload';
|
import { Uploads } from '$lib/server/upload';
|
||||||
import type { PaginationMeta } from '$lib/types';
|
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 {
|
export enum OAuth2ClientURLType {
|
||||||
REDIRECT_URI = 'redirect_uri',
|
REDIRECT_URI = 'redirect_uri',
|
||||||
@ -27,6 +30,17 @@ export interface OAuth2ClientAdminListItem
|
|||||||
urls: Omit<OAuth2ClientUrl, 'created_at' | 'updated_at' | 'clientId'>[];
|
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 {
|
export class OAuth2Clients {
|
||||||
public static availableGrantTypes = [
|
public static availableGrantTypes = [
|
||||||
'authorization_code',
|
'authorization_code',
|
||||||
@ -109,6 +123,52 @@ export class OAuth2Clients {
|
|||||||
)?.length;
|
)?.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) {
|
static checkSecret(client: OAuth2Client, secret: string) {
|
||||||
return client.client_secret === secret;
|
return client.client_secret === secret;
|
||||||
}
|
}
|
||||||
@ -152,7 +212,7 @@ export class OAuth2Clients {
|
|||||||
listAll?: boolean;
|
listAll?: boolean;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const filterText = `%${filters?.filter}%`;
|
const filterText = `%${filters?.filter?.toLowerCase()}%`;
|
||||||
const limit = filters?.limit || 20;
|
const limit = filters?.limit || 20;
|
||||||
const allowedClients = db
|
const allowedClients = db
|
||||||
.select({ id: oauth2Client.id })
|
.select({ id: oauth2Client.id })
|
||||||
@ -165,7 +225,10 @@ export class OAuth2Clients {
|
|||||||
: undefined,
|
: undefined,
|
||||||
filters?.clientId ? eq(oauth2Client.client_id, filters.clientId) : undefined,
|
filters?.clientId ? eq(oauth2Client.client_id, filters.clientId) : undefined,
|
||||||
filters?.filter
|
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
|
: 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) {
|
static async deleteClient(client: OAuth2Client) {
|
||||||
if (client.pictureId) {
|
if (client.pictureId) {
|
||||||
await Uploads.removeClientAvatar(client);
|
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 {
|
import {
|
||||||
db,
|
db,
|
||||||
privilege,
|
privilege,
|
||||||
@ -58,9 +58,9 @@ export class UsersAdmin {
|
|||||||
const searchExpression = filter
|
const searchExpression = filter
|
||||||
? or(
|
? or(
|
||||||
like(user.uuid, subfilter),
|
like(user.uuid, subfilter),
|
||||||
ilike(user.username, subfilter),
|
sql`lower(${user.username}) like ${subfilter}`,
|
||||||
ilike(user.display_name, subfilter),
|
sql`lower(${user.display_name}) like ${subfilter}`,
|
||||||
ilike(user.email, subfilter)
|
sql`lower(${user.email}) like ${subfilter}`
|
||||||
)
|
)
|
||||||
: undefined;
|
: 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
|
const current = await db
|
||||||
.select({
|
.select({
|
||||||
privilegeId: userPrivilegesPrivilege.privilegeId
|
privilegeId: userPrivilegesPrivilege.privilegeId
|
||||||
})
|
})
|
||||||
.from(userPrivilegesPrivilege)
|
.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[]>(
|
const toRemoveIds = current.reduce<number[]>(
|
||||||
(list, { privilegeId }) =>
|
(list, { privilegeId }) =>
|
||||||
|
@ -3,13 +3,33 @@
|
|||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import ClientCard from '$lib/components/admin/AdminClientCard.svelte';
|
import ClientCard from '$lib/components/admin/AdminClientCard.svelte';
|
||||||
|
import TitleRow from '$lib/components/container/TitleRow.svelte';
|
||||||
|
import 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;
|
export let data: PageData;
|
||||||
</script>
|
</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} />
|
<Paginator meta={data.meta} />
|
||||||
{#each data.list as client}
|
{#each data.list as client}
|
||||||
<a href={`oauth2/${client.client_id}`} class="client-link">
|
<a href={`oauth2/${client.client_id}`} class="client-link">
|
||||||
@ -17,7 +37,8 @@
|
|||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
<Paginator meta={data.meta} />
|
<Paginator meta={data.meta} />
|
||||||
</div>
|
</div>
|
||||||
|
</ColumnView>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.client-link {
|
.client-link {
|
||||||
|
@ -32,11 +32,13 @@ export const actions = {
|
|||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -44,8 +46,6 @@ export const actions = {
|
|||||||
return error(404, 'Client not found');
|
return error(404, 'Client not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
|
|
||||||
const body = await request.formData();
|
const body = await request.formData();
|
||||||
const { title, description, activated, verified } = Changesets.take<UpdateRequest>(
|
const { title, description, activated, verified } = Changesets.take<UpdateRequest>(
|
||||||
['title', 'description', 'activated', 'verified'],
|
['title', 'description', 'activated', 'verified'],
|
||||||
@ -81,11 +81,13 @@ export const actions = {
|
|||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -97,7 +99,6 @@ export const actions = {
|
|||||||
return fail(400, { errors: ['deleteActivated'] });
|
return fail(400, { errors: ['deleteActivated'] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
if (details.ownerId !== currentUser.id && !fullPrivileges) {
|
if (details.ownerId !== currentUser.id && !fullPrivileges) {
|
||||||
return fail(403, { errors: ['forbidden'] });
|
return fail(403, { errors: ['forbidden'] });
|
||||||
}
|
}
|
||||||
@ -111,11 +112,13 @@ export const actions = {
|
|||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -123,7 +126,6 @@ export const actions = {
|
|||||||
return error(404, 'Client not found');
|
return error(404, 'Client not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
if (!fullPrivileges && !details.isOwner) {
|
if (!fullPrivileges && !details.isOwner) {
|
||||||
return fail(403, { errors: ['forbidden'] });
|
return fail(403, { errors: ['forbidden'] });
|
||||||
}
|
}
|
||||||
@ -135,15 +137,17 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
removeUrl: async ({ locals, url, params: { uuid } }) => {
|
removeUrl: async ({ locals, url, params: { uuid } }) => {
|
||||||
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -161,15 +165,17 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
addUrl: async ({ locals, request, params: { uuid } }) => {
|
addUrl: async ({ locals, request, params: { uuid } }) => {
|
||||||
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -192,15 +198,17 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
removePrivilege: async ({ locals, url, params: { uuid } }) => {
|
removePrivilege: async ({ locals, url, params: { uuid } }) => {
|
||||||
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -218,15 +226,17 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
addPrivilege: async ({ locals, request, params: { uuid } }) => {
|
addPrivilege: async ({ locals, request, params: { uuid } }) => {
|
||||||
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -250,11 +260,13 @@ export const actions = {
|
|||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -262,7 +274,6 @@ export const actions = {
|
|||||||
return error(404, 'Client not found');
|
return error(404, 'Client not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
const allowedGrants = fullPrivileges
|
const allowedGrants = fullPrivileges
|
||||||
? OAuth2Clients.availableGrantTypes
|
? OAuth2Clients.availableGrantTypes
|
||||||
: OAuth2Clients.userSetGrants;
|
: OAuth2Clients.userSetGrants;
|
||||||
@ -285,11 +296,13 @@ export const actions = {
|
|||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -297,7 +310,6 @@ export const actions = {
|
|||||||
return error(404, 'Client not found');
|
return error(404, 'Client not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
|
||||||
const allowedScopes = fullPrivileges
|
const allowedScopes = fullPrivileges
|
||||||
? OAuth2Clients.availableScopes
|
? OAuth2Clients.availableScopes
|
||||||
: OAuth2Clients.userSetScopes;
|
: OAuth2Clients.userSetScopes;
|
||||||
@ -316,15 +328,17 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
avatar: async ({ request, locals, params: { uuid } }) => {
|
avatar: async ({ request, locals, params: { uuid } }) => {
|
||||||
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -346,15 +360,17 @@ export const actions = {
|
|||||||
return { errors: [] };
|
return { errors: [] };
|
||||||
},
|
},
|
||||||
removeAvatar: async ({ locals, params: { uuid } }) => {
|
removeAvatar: async ({ locals, params: { uuid } }) => {
|
||||||
const { currentUser } = await UsersAdmin.getActionUser(locals, [
|
const { currentUser, userSession } = await UsersAdmin.getActionUser(locals, [
|
||||||
['admin:oauth2', 'self:oauth2']
|
['admin:oauth2', 'self:oauth2']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const fullPrivileges = hasPrivileges(userSession.privileges || [], ['admin:oauth2']);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -379,7 +395,7 @@ export const load = async ({ params: { uuid }, parent }) => {
|
|||||||
list: [details]
|
list: [details]
|
||||||
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
} = await OAuth2Clients.getClientByAdminUser(currentUser as User, {
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
listAll: false,
|
listAll: fullPrivileges,
|
||||||
omitSecret: false
|
omitSecret: false
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -388,8 +404,10 @@ export const load = async ({ params: { uuid }, parent }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const privileges = await Users.getAvailablePrivileges(details.id);
|
const privileges = await Users.getAvailablePrivileges(details.id);
|
||||||
|
const users = await OAuth2Clients.getAuthorizedUsers(details as OAuth2Client);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
users,
|
||||||
availableUrls: OAuth2Clients.availableUrlTypes,
|
availableUrls: OAuth2Clients.availableUrlTypes,
|
||||||
availablePrivileges: privileges,
|
availablePrivileges: privileges,
|
||||||
availableGrants: fullPrivileges
|
availableGrants: fullPrivileges
|
||||||
|
@ -14,6 +14,7 @@
|
|||||||
import type { ActionData, PageData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
|
import { PUBLIC_SITE_NAME, PUBLIC_URL } from '$env/static/public';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
@ -37,6 +38,13 @@
|
|||||||
$: uuidPrefix = data.details.client_id.split('-')[0] + ':';
|
$: uuidPrefix = data.details.client_id.split('-')[0] + ':';
|
||||||
</script>
|
</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>
|
<h1>{$t('admin.oauth2.title')} / {data.details.title}</h1>
|
||||||
|
|
||||||
<ColumnView>
|
<ColumnView>
|
||||||
@ -282,16 +290,74 @@
|
|||||||
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
|
<Button type="submit" variant="primary">{$t('common.submit')}</Button>
|
||||||
</form>
|
</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>
|
<h2>{$t('admin.oauth2.authorizations')}</h2>
|
||||||
<p>{$t('admin.oauth2.authorizationsHint')}</p>
|
<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>
|
<b>{$t('admin.oauth2.noAuthorizations')}</b>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</ColumnView>
|
</ColumnView>
|
||||||
|
|
||||||
<AvatarModal show={showAvatarModal} url={$page.url.pathname} />
|
<AvatarModal show={showAvatarModal} url={$page.url.pathname} />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
h2,
|
h2,
|
||||||
|
ul,
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
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 { t } from '$lib/i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import UserCard from '$lib/components/admin/AdminUserCard.svelte';
|
import UserCard from '$lib/components/admin/AdminUserCard.svelte';
|
||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
import FormControl from '$lib/components/form/FormControl.svelte';
|
||||||
|
import ColumnView from '$lib/components/container/ColumnView.svelte';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{$t('admin.users.title')} - {PUBLIC_SITE_NAME} {$t('admin.title')}</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<h1>{$t('admin.users.title')}</h1>
|
<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} />
|
<Paginator meta={data.meta} />
|
||||||
{#each data.list as user}
|
{#each data.list as user}
|
||||||
<a href={`users/${user.uuid}`} class="user-link">
|
<a href={`users/${user.uuid}`} class="user-link">
|
||||||
@ -17,7 +33,8 @@
|
|||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
<Paginator meta={data.meta} />
|
<Paginator meta={data.meta} />
|
||||||
</div>
|
</div>
|
||||||
|
</ColumnView>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.user-link {
|
.user-link {
|
||||||
|
@ -10,11 +10,19 @@
|
|||||||
import type { ActionData, PageData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
|
import AdminPrivilegesSelect from '$lib/components/admin/AdminPrivilegesSelect.svelte';
|
||||||
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
import FormErrors from '$lib/components/form/FormErrors.svelte';
|
||||||
|
import { PUBLIC_SITE_NAME } from '$env/static/public';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
export let form: ActionData;
|
export let form: ActionData;
|
||||||
</script>
|
</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>
|
<h1>{$t('admin.users.title')} / {data.details.display_name}</h1>
|
||||||
|
|
||||||
<SplitView>
|
<SplitView>
|
||||||
|
Loading…
Reference in New Issue
Block a user