mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-10-14 13:49:03 +02:00
feat: add OAuth APIs with scoped permissions (#1542)
Co-authored-by: Alessandro (Ale) Segala <43508+ItalyPaleAle@users.noreply.github.com>
This commit is contained in:
co-authored by
Alessandro Segala
parent
0b2706a488
commit
09d196f7c5
@@ -32,6 +32,7 @@
|
||||
children,
|
||||
onInput,
|
||||
labelFor,
|
||||
readonly = false,
|
||||
inputClass,
|
||||
...restProps
|
||||
}: HTMLAttributes<HTMLDivElement> &
|
||||
@@ -43,6 +44,7 @@
|
||||
disabled?: boolean;
|
||||
inputClass?: string;
|
||||
type?: 'text' | 'password' | 'email' | 'number' | 'checkbox' | 'date' | 'url';
|
||||
readonly?: boolean;
|
||||
onInput?: (e: FormInputEvent) => void;
|
||||
} = $props();
|
||||
|
||||
@@ -64,7 +66,7 @@
|
||||
<FormattedMessage m={description} />
|
||||
{#if docsLink}
|
||||
<a
|
||||
class="relative text-black after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:translate-y-[-1px] after:bg-white dark:text-white"
|
||||
class="relative text-black after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:-translate-y-px after:bg-white dark:text-white"
|
||||
href={docsLink}
|
||||
target="_blank"
|
||||
>
|
||||
@@ -91,6 +93,7 @@
|
||||
bind:value={input.value}
|
||||
{disabled}
|
||||
oninput={(e) => onInput?.(e)}
|
||||
{readonly}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<script lang="ts">
|
||||
import * as Item from '$lib/components/ui/item/index.js';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import { LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
|
||||
import type { InteractionScopeInfo } from '$lib/types/oidc.type';
|
||||
import { LucideKeyRound, LucideMail, LucideUser, LucideUsers } from '@lucide/svelte';
|
||||
import ScopeItem from './scope-item.svelte';
|
||||
|
||||
let { scopes }: { scopes: string[] } = $props();
|
||||
let { scopes, scopeInfo = [] }: { scopes: string[]; scopeInfo?: InteractionScopeInfo[] } =
|
||||
$props();
|
||||
|
||||
const standardScopes = ['openid', 'profile', 'email', 'groups', 'offline_access'];
|
||||
const infoByKey = $derived(new Map(scopeInfo.map((info) => [info.key, info])));
|
||||
const customScopes = $derived(scopes.filter((scope) => !standardScopes.includes(scope)));
|
||||
</script>
|
||||
|
||||
<Item.Group data-testid="scopes">
|
||||
<Item.Group data-testid="scopes" class="gap-1">
|
||||
{#if scopes.includes('email')}
|
||||
<ScopeItem icon={LucideMail} name={m.email()} description={m.view_your_email_address()} />
|
||||
{/if}
|
||||
@@ -25,4 +31,11 @@
|
||||
description={m.view_the_groups_you_are_a_member_of()}
|
||||
/>
|
||||
{/if}
|
||||
{#each customScopes as scope}
|
||||
<ScopeItem
|
||||
icon={LucideKeyRound}
|
||||
name={infoByKey.get(scope)?.name ?? scope}
|
||||
description={infoByKey.get(scope)?.description || m.access_an_api_on_your_behalf()}
|
||||
/>
|
||||
{/each}
|
||||
</Item.Group>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<p
|
||||
bind:this={ref}
|
||||
data-slot="card-description"
|
||||
class={cn('text-muted-foreground text-sm', className)}
|
||||
class={cn('text-muted-foreground text-sm mt-3', className)}
|
||||
{...restProps}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type {
|
||||
Api,
|
||||
ApiCreate,
|
||||
ApiListItem,
|
||||
ApiPermissionInput,
|
||||
ApiUpdate,
|
||||
ClientApiAccess
|
||||
} from '$lib/types/api.type';
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import APIService from './api-service';
|
||||
|
||||
export default class ApisService extends APIService {
|
||||
list = async (options?: ListRequestOptions) => {
|
||||
const res = await this.api.get('/apis', { params: options });
|
||||
return res.data as Paginated<ApiListItem>;
|
||||
};
|
||||
|
||||
listAll = async () => {
|
||||
const res = await this.api.get('/apis', { params: { pagination: { page: 1, limit: 1000 } } });
|
||||
return (res.data as Paginated<ApiListItem>).data;
|
||||
};
|
||||
|
||||
get = async (id: string) => {
|
||||
const res = await this.api.get(`/apis/${id}`);
|
||||
return res.data as Api;
|
||||
};
|
||||
|
||||
create = async (api: ApiCreate) => {
|
||||
const res = await this.api.post('/apis', api);
|
||||
return res.data as Api;
|
||||
};
|
||||
|
||||
update = async (id: string, api: ApiUpdate) => {
|
||||
const res = await this.api.put(`/apis/${id}`, api);
|
||||
return res.data as Api;
|
||||
};
|
||||
|
||||
remove = async (id: string) => {
|
||||
await this.api.delete(`/apis/${id}`);
|
||||
};
|
||||
|
||||
updatePermissions = async (id: string, permissions: ApiPermissionInput[]) => {
|
||||
const res = await this.api.put(`/apis/${id}/permissions`, { permissions });
|
||||
return res.data as Api;
|
||||
};
|
||||
|
||||
getClientAccess = async (clientId: string) => {
|
||||
const res = await this.api.get(`/api-access/${clientId}`);
|
||||
return res.data as ClientApiAccess;
|
||||
};
|
||||
|
||||
updateClientAccess = async (clientId: string, access: ClientApiAccess) => {
|
||||
const res = await this.api.put(`/api-access/${clientId}`, access);
|
||||
return res.data as ClientApiAccess;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
export type ApiPermission = {
|
||||
id: string;
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type Api = {
|
||||
id: string;
|
||||
name: string;
|
||||
resource: string;
|
||||
createdAt: string;
|
||||
permissions: ApiPermission[];
|
||||
};
|
||||
|
||||
export type ApiListItem = Omit<Api, 'permissions'> & {
|
||||
permissionCount: number;
|
||||
};
|
||||
|
||||
export type ApiCreate = {
|
||||
name: string;
|
||||
resource: string;
|
||||
};
|
||||
|
||||
export type ApiUpdate = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ApiPermissionInput = {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ClientApiAccess = {
|
||||
userDelegatedPermissionIds: string[];
|
||||
clientPermissionIds: string[];
|
||||
};
|
||||
@@ -43,7 +43,10 @@ export type OidcClientWithAllowedUserGroupsCount = OidcClient & {
|
||||
allowedUserGroupsCount: number;
|
||||
};
|
||||
|
||||
export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo' | 'hasDarkLogo' | 'pkceSupported'>;
|
||||
export type OidcClientUpdate = Omit<
|
||||
OidcClient,
|
||||
'id' | 'logoURL' | 'hasLogo' | 'hasDarkLogo' | 'pkceSupported'
|
||||
>;
|
||||
export type OidcClientCreate = OidcClientUpdate & {
|
||||
id?: string;
|
||||
};
|
||||
@@ -61,6 +64,7 @@ export type OidcClientCreateWithLogo = OidcClientCreate & {
|
||||
|
||||
export type OidcDeviceCodeInfo = {
|
||||
scope: string[];
|
||||
scopeInfo: InteractionScopeInfo[];
|
||||
authorizationRequired: boolean;
|
||||
reauthenticationRequired: boolean;
|
||||
client: OidcClientMetaData;
|
||||
@@ -72,9 +76,16 @@ export type AccessibleOidcClient = OidcClientMetaData & {
|
||||
|
||||
export type InteractionStep = 'authenticate' | 'select_account' | 'reauthenticate' | 'consent';
|
||||
|
||||
export type InteractionScopeInfo = {
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type InteractionSession = {
|
||||
id: string;
|
||||
scopes: string[];
|
||||
scopeInfo: InteractionScopeInfo[];
|
||||
client: OidcClientMetaData;
|
||||
currentStep?: InteractionStep;
|
||||
requiredSteps: InteractionStep[];
|
||||
|
||||
Reference in New Issue
Block a user