Skip to content

Commit 6306b9a

Browse files
committed
✨ Better apps list, app detail page, including suggesting a more secure allowed redirect URIs list
1 parent 0db5a8f commit 6306b9a

File tree

8 files changed

+247
-38
lines changed

8 files changed

+247
-38
lines changed

src/lib/SensitiveValue.svelte

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
const { children }: { children: Snippet<[]> } = $props();
5+
6+
let shown = $state(false);
7+
</script>
8+
9+
<div class="sensitive-value">
10+
<button
11+
onclick={() => {
12+
shown = !shown;
13+
}}
14+
>{#if shown}cacher{:else}montrer{/if}</button
15+
>
16+
{#if shown}
17+
{@render children?.()}
18+
{:else}
19+
{Array.from({ length: 20 })
20+
.map(() => '*')
21+
.join('')}
22+
{/if}
23+
</div>

src/lib/server/oauth.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,8 @@ import { env } from '$env/dynamic/public';
22
import { env as secrets } from '$env/dynamic/private';
33
import { Authentik } from 'arctic';
44

5-
const domain = 'auth.inpt.fr';
65
export const authentik = new Authentik(
7-
domain,
6+
env.PUBLIC_AUTHENTIK_INSTANCE,
87
env.PUBLIC_OAUTH2_ID,
98
secrets.PRIVATE_OAUTH2_SECRET,
109
'http://localhost:5173/login/callback'

src/routes/+page.gql

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ query PageHome {
22
me {
33
uid
44
}
5-
application(slug: "aedopte") {
6-
... on Application {
7-
name
8-
metaIcon
9-
groupSlug
10-
churrosGroup {
5+
applications {
6+
... on PaginatedApplicationList {
7+
results {
8+
metaIcon
9+
metaPublisher
1110
name
12-
pictureURL
11+
slug
1312
}
1413
}
1514
}

src/routes/+page.svelte

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,18 @@
1111
1212
const { data, form }: Props = $props();
1313
const { PageHome } = $derived(data);
14-
const apps = $derived.by(() => {
15-
if (!$PageHome) return [];
16-
if ($PageHome.data?.application?.__typename === 'Application') {
17-
return [$PageHome.data.application];
18-
}
19-
return [];
20-
});
14+
15+
const apps = $derived(
16+
$PageHome.data?.applications?.__typename === 'PaginatedApplicationList'
17+
? $PageHome.data.applications.results.filter((app) => app !== null)
18+
: []
19+
);
2120
2221
$effect(() => console.log($PageHome));
2322
2423
onMount(async () => {
2524
if (form?.appSlug) {
26-
await goto(`/app/${form.appSlug}`);
25+
await goto(`/apps/${form.appSlug}`);
2726
}
2827
});
2928
</script>
@@ -45,13 +44,28 @@
4544
</section>
4645
</form>
4746

48-
<div>
49-
{$PageHome?.errors?.map((e) => e.message).join('\n') ?? ''}
50-
</div>
5147
<ul>
5248
{#each apps as app}
5349
<li>
54-
{JSON.stringify(app)}
50+
<a href="/apps/{app.slug}">
51+
<img src={app.metaIcon} alt="Icone" />
52+
{app.name} by {app?.metaPublisher}
53+
</a>
5554
</li>
5655
{/each}
5756
</ul>
57+
58+
<style>
59+
ul {
60+
list-style: none;
61+
padding-left: 0;
62+
}
63+
li > a {
64+
display: flex;
65+
align-items: center;
66+
gap: 0 1ch;
67+
}
68+
li img {
69+
height: 1.5rem;
70+
}
71+
</style>

src/routes/app/[slug]/+page.svelte

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/routes/app/[slug]/+page.gql renamed to src/routes/apps/[slug]/+page.gql

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
query PageApplication($slug: String!) {
22
application(slug: $slug) {
33
... on Application {
4+
name
5+
slug
46
metaIcon
7+
metaDescription
8+
launchUrl
59
churrosGroup {
610
uid
711
}
8-
# providerObj {
9-
# application {
10-
# ... on Application {
11-
# pk
12-
# }
13-
# }
14-
# }
1512
oauth2Provider {
1613
... on OAuth2Provider {
14+
pk
1715
clientId
1816
clientSecret
1917
clientType
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { graphql } from '$houdini';
2+
import { error } from '@sveltejs/kit';
3+
4+
export const actions = {
5+
async editAllowedURIs(event) {
6+
const formdata = [...(await event.request.formData()).entries()];
7+
const uris = formdata
8+
.filter(([k]) => k.startsWith('uri:'))
9+
.sort(([a], [b]) => b - a)
10+
.map(([_, v]) => v)
11+
.filter(Boolean)
12+
.join('\n');
13+
const providerId = parseInt(Object.fromEntries(formdata).providerid);
14+
15+
const { data, errors } = await graphql(`
16+
mutation EditAllowedURIs($providerId: Int!, $uris: String!) {
17+
providersOauth2PartialUpdate(id: $providerId, input: { redirectUris: $uris }) {
18+
__typename
19+
}
20+
}
21+
`).mutate(
22+
{
23+
providerId,
24+
uris
25+
},
26+
{ event }
27+
);
28+
29+
if (errors) {
30+
error(400, {
31+
message: errors.map((e) => e.message).join('\n')
32+
});
33+
}
34+
35+
return {};
36+
}
37+
};

src/routes/apps/[slug]/+page.svelte

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
<script lang="ts">
2+
import { env } from '$env/dynamic/public';
3+
import SensitiveValue from '$lib/SensitiveValue.svelte';
4+
import type { PageData } from './$houdini';
5+
interface Props {
6+
data: PageData;
7+
}
8+
const { data }: Props = $props();
9+
const { PageApplication } = $derived(data);
10+
11+
function enumerate<T>(a: T[]): Array<[number, T]> {
12+
return Object.entries(a).map(([k, v]) => [parseInt(k), v]);
13+
}
14+
15+
function uris(allUris?: string | null): string[] {
16+
if (!allUris) return [];
17+
return allUris.split('\n');
18+
}
19+
</script>
20+
21+
{#if $PageApplication?.errors}
22+
<section class="errors">
23+
<h2>Oops!</h2>
24+
<p>Il y a eu des erreurs</p>
25+
<ul>
26+
{#each $PageApplication.errors as { message }}
27+
<li>{message}</li>
28+
{/each}
29+
</ul>
30+
</section>
31+
{/if}
32+
33+
{#if $PageApplication?.data?.application?.__typename === 'GenericError'}
34+
<section class="errors">
35+
<h1>Ooops</h1>
36+
<p>{$PageApplication.data.application.detail}</p>
37+
<a href="/login">Connexion</a>
38+
</section>
39+
{/if}
40+
41+
{#snippet copyable(value: string, sensitive = false)}
42+
<dd>
43+
<button
44+
onclick={() => {
45+
const clipboard = new Clipboard();
46+
clipboard.writeText(value);
47+
}}>copier</button
48+
>
49+
{#if sensitive}
50+
<SensitiveValue>{value}</SensitiveValue>
51+
{:else}
52+
{value}
53+
{/if}
54+
</dd>
55+
{/snippet}
56+
57+
{#if $PageApplication?.data?.application && $PageApplication.data.application.__typename === 'Application'}
58+
{@const app = $PageApplication.data.application}
59+
<h1>
60+
<img src={app.metaIcon} alt="Icone" class="logo" />
61+
{app.name}
62+
</h1>
63+
{#if app.launchUrl}
64+
<a href={app.launchUrl}>https://{new URL(app.launchUrl).hostname}</a>
65+
{/if}
66+
<p class="desc">{app.metaDescription}</p>
67+
{#if app.oauth2Provider?.__typename === 'OAuth2Provider'}
68+
{@const authorizedUris = uris(app.oauth2Provider.redirectUris)}
69+
<dl>
70+
<dt>Client ID</dt>
71+
{@render copyable(app.oauth2Provider.clientId)}
72+
<dt>Client secret</dt>
73+
{@render copyable(app.oauth2Provider.clientSecret, true)}
74+
<dt>Allowed redirect URIs</dt>
75+
<dd class="redirecturis">
76+
<small>Peut être un motif Regex d'URIs</small>
77+
{#if authorizedUris.includes('.*') && app.launchUrl}
78+
<p class="warn">
79+
Attention: utiliser <code>.*</code> est dangereux,
80+
<button
81+
onclick={() => {
82+
document.querySelectorAll('.redirecturis input').forEach((node) => {
83+
if (!(node instanceof HTMLInputElement)) return;
84+
if (!app.launchUrl) return;
85+
if (node.value !== '.*') return;
86+
node.value = `https?://(localhost|${new URL(app.launchUrl).hostname}).*`;
87+
});
88+
}}>utiliser localhost et {new URL(app.launchUrl).hostname}</button
89+
>
90+
</p>
91+
{/if}
92+
<form method="post" action="?/editAllowedURIs">
93+
<ul>
94+
{#each enumerate(authorizedUris) as [i, uri]}
95+
<li>
96+
<input type="text" name="uri:{i}" value={uri} />
97+
</li>
98+
{/each}
99+
<li class="new">
100+
<input
101+
placeholder="Ajouter une URI"
102+
type="text"
103+
name="uri:{uris(app.oauth2Provider.redirectUris).length}"
104+
/>
105+
</li>
106+
</ul>
107+
<input type="hidden" name="providerid" value={app.oauth2Provider.pk} />
108+
<button type="submit">enregistrer</button>
109+
</form>
110+
</dd>
111+
<p><em>Ne pas oublier le <code>/</code> à la fin des URLs 🙃</em></p>
112+
<dt>Authorize URL</dt>
113+
{@render copyable(`https://${env.PUBLIC_AUTHENTIK_INSTANCE}/o/authorize/`)}
114+
<dt>Token URL</dt>
115+
{@render copyable(`https://${env.PUBLIC_AUTHENTIK_INSTANCE}/o/token/`)}
116+
<dt>User-info URL</dt>
117+
{@render copyable(`https://${env.PUBLIC_AUTHENTIK_INSTANCE}/o/userinfo/`)}
118+
<dt>Logout URL</dt>
119+
{@render copyable(`https://${env.PUBLIC_AUTHENTIK_INSTANCE}/o/${app.slug}/end-session/`)}
120+
</dl>
121+
{/if}
122+
{/if}
123+
124+
<style>
125+
section.errors {
126+
color: red;
127+
background-color: lightpink;
128+
}
129+
130+
.logo {
131+
height: 1.2em;
132+
}
133+
134+
h1 {
135+
display: flex;
136+
align-items: center;
137+
gap: 0 1ch;
138+
}
139+
140+
dd:not(.redirecturis) {
141+
display: flex;
142+
align-items: center;
143+
gap: 0 1ch;
144+
}
145+
146+
.redirecturis input {
147+
width: 40ch;
148+
}
149+
</style>

0 commit comments

Comments
 (0)