Compare commits
2 commits
main
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
|
7d092176b2 | ||
|
ec0ae60b10 |
24 changed files with 1212 additions and 738 deletions
32
.github/workflows/app.yml
vendored
32
.github/workflows/app.yml
vendored
|
@ -27,23 +27,25 @@ jobs:
|
|||
semver: ${{ steps.gitversion.outputs.SemVer }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install GitVersion
|
||||
uses: gittools/actions/gitversion/setup@v4.1.0
|
||||
uses: gittools/actions/gitversion/setup@v3.2.1
|
||||
with:
|
||||
versionSpec: "6.x"
|
||||
versionSpec: "5.x"
|
||||
- name: Determine Version
|
||||
id: gitversion
|
||||
uses: gittools/actions/gitversion/execute@v4.1.0
|
||||
uses: gittools/actions/gitversion/execute@v3.2.1
|
||||
with:
|
||||
useConfigFile: true
|
||||
|
||||
build_and_publish:
|
||||
name: Build & Publish
|
||||
runs-on: ubuntu-latest
|
||||
needs: versionize
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build container images
|
||||
run: |
|
||||
|
@ -71,7 +73,7 @@ jobs:
|
|||
matrix:
|
||||
environment: [ 'prd' ]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
infrastructure
|
||||
|
@ -84,13 +86,13 @@ jobs:
|
|||
subscription-id: ${{ secrets.CALQUE_PRD_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Deploy bicep
|
||||
uses: azure/cli@v2
|
||||
uses: Azure/cli@v2
|
||||
with:
|
||||
azcliversion: 2.75.0
|
||||
inlineScript: >-
|
||||
az deployment sub create
|
||||
--location westeurope
|
||||
--template-file infrastructure/main.bicep
|
||||
--parameters infrastructure/params/${{ matrix.environment }}.bicepparam
|
||||
--parameters version=${{needs.versionize.outputs.semver}}
|
||||
--parameters registryUrl=${{ secrets.ACR_LOGIN_SERVER }}
|
||||
azcliversion: 2.66.0
|
||||
inlineScript: |
|
||||
az deployment sub create \
|
||||
--location westeurope \
|
||||
--template-file infrastructure/main.bicep \
|
||||
--parameters infrastructure/params/${{ matrix.environment }}.bicepparam \
|
||||
--parameters version=${{needs.versionize.outputs.semver}} \
|
||||
--parameters registryUrl=${{ secrets.ACR_LOGIN_SERVER }}
|
|
@ -4,17 +4,16 @@ WORKDIR /usr/src/app
|
|||
FROM base AS install
|
||||
RUN mkdir -p /temp/dev
|
||||
COPY package.json bun.lock /temp/dev
|
||||
COPY patches/ /temp/dev/patches/
|
||||
RUN cd /temp/dev && bun install --frozen-lockfile
|
||||
|
||||
RUN mkdir -p /temp/prod
|
||||
COPY package.json bun.lock /temp/prod/
|
||||
COPY patches/ /temp/prod/patches/
|
||||
RUN cd /temp/prod && bun install --frozen-lockfile --production
|
||||
|
||||
FROM base AS prerelease
|
||||
COPY --from=install /temp/dev/node_modules node_modules
|
||||
COPY . .
|
||||
# RUN echo "SESSION_SECRET=$(head -c 64 /dev/random | base64)" > .env
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV SERVER_PRESET=bun
|
||||
|
|
20
GitVersion.yml
Normal file
20
GitVersion.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
assembly-versioning-scheme: MajorMinorPatch
|
||||
assembly-file-versioning-scheme: MajorMinorPatchTag
|
||||
assembly-informational-format: "{InformationalVersion}"
|
||||
mode: Mainline
|
||||
tag-prefix: "[vV]"
|
||||
continuous-delivery-fallback-tag: ci
|
||||
major-version-bump-message: '\+semver:\s?(breaking|major)'
|
||||
minor-version-bump-message: '\+semver:\s?(feature|minor)'
|
||||
patch-version-bump-message: '\+semver:\s?(fix|patch)'
|
||||
no-bump-message: '\+semver:\s?(none|skip)'
|
||||
legacy-semver-padding: 4
|
||||
build-metadata-padding: 4
|
||||
commits-since-version-source-padding: 4
|
||||
commit-message-incrementing: Enabled
|
||||
branches: {}
|
||||
ignore:
|
||||
sha: []
|
||||
increment: Inherit
|
||||
commit-date-format: yyyy-MM-dd
|
||||
merge-message-formats: {}
|
|
@ -4,29 +4,14 @@ import devtools from 'solid-devtools/vite';
|
|||
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: [
|
||||
{ find: '@', replacement: 'F:\\Github\\calque\\node_modules\\' },
|
||||
],
|
||||
},
|
||||
html: {
|
||||
cspNonce: 'KAAS_IS_AWESOME',
|
||||
},
|
||||
// css: {
|
||||
// postcss: {
|
||||
// },
|
||||
// },
|
||||
plugins: [
|
||||
devtools({
|
||||
autoname: true,
|
||||
}),
|
||||
solidSvg(),
|
||||
{
|
||||
name: 'temp',
|
||||
configResolved(config) {
|
||||
console.log(config.resolve.alias);
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
solid: {
|
||||
|
|
|
@ -19,7 +19,7 @@ var context = {
|
|||
deployedAt: deployedAt
|
||||
}
|
||||
|
||||
resource calqueResourceGroup 'Microsoft.Resources/resourceGroups@2025-04-01' = {
|
||||
resource calqueResourceGroup 'Microsoft.Resources/resourceGroups@2025-03-01' = {
|
||||
name: 'rg-${locationAbbreviation}-${environment}-${projectName}'
|
||||
location: location
|
||||
}
|
||||
|
|
46
package.json
46
package.json
|
@ -6,50 +6,50 @@
|
|||
"bun": ">=1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solid-primitives/clipboard": "^1.6.2",
|
||||
"@solid-primitives/destructure": "^0.2.2",
|
||||
"@solid-primitives/i18n": "^2.2.1",
|
||||
"@solid-primitives/scheduled": "^1.5.2",
|
||||
"@solid-primitives/selection": "^0.1.3",
|
||||
"@solid-primitives/storage": "^4.3.3",
|
||||
"@solid-primitives/timer": "^1.4.2",
|
||||
"@solid-primitives/clipboard": "^1.6.0",
|
||||
"@solid-primitives/destructure": "^0.2.0",
|
||||
"@solid-primitives/i18n": "^2.2.0",
|
||||
"@solid-primitives/scheduled": "^1.5.0",
|
||||
"@solid-primitives/selection": "^0.1.0",
|
||||
"@solid-primitives/storage": "^4.3.1",
|
||||
"@solid-primitives/timer": "^1.4.0",
|
||||
"@solidjs/meta": "^0.29.4",
|
||||
"@solidjs/router": "^0.15.3",
|
||||
"@solidjs/start": "^1.1.7",
|
||||
"@solidjs/start": "^1.1.0",
|
||||
"dexie": "^4.0.11",
|
||||
"flag-icons": "^7.5.0",
|
||||
"flag-icons": "^7.3.2",
|
||||
"iterator-helpers-polyfill": "^3.0.1",
|
||||
"rehype-parse": "^9.0.1",
|
||||
"rehype-remark": "^10.0.1",
|
||||
"rehype-remark": "^10.0.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.1.2",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"sitemap": "^8.0.0",
|
||||
"solid-icons": "^1.1.0",
|
||||
"solid-js": "^1.9.7",
|
||||
"ts-pattern": "^5.7.1",
|
||||
"solid-js": "^1.9.4",
|
||||
"ts-pattern": "^5.6.2",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-find": "^3.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vinxi": "^0.5.8"
|
||||
"vinxi": "^0.5.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@happy-dom/global-registrator": "^18.0.1",
|
||||
"@happy-dom/global-registrator": "^17.0.3",
|
||||
"@sinonjs/fake-timers": "^14.0.0",
|
||||
"@solidjs/testing-library": "^0.8.10",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/sinonjs__fake-timers": "^8.1.5",
|
||||
"@types/wicg-file-system-access": "^2023.10.6",
|
||||
"@vitest/coverage-istanbul": "3.2.4",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"bun-types": "^1.2.19",
|
||||
"jsdom": "^26.1.0",
|
||||
"solid-devtools": "^0.34.3",
|
||||
"vite-plugin-solid": "^2.11.7",
|
||||
"@types/wicg-file-system-access": "^2023.10.5",
|
||||
"@vitest/coverage-istanbul": "3.1.4",
|
||||
"@vitest/coverage-v8": "3.1.4",
|
||||
"bun-types": "^1.2.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"solid-devtools": "^0.34.0",
|
||||
"vite-plugin-solid": "^2.11.2",
|
||||
"vite-plugin-solid-svg": "^0.8.1",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest": "^3.0.6",
|
||||
"workbox-window": "^7.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
5
public/.well-known/web-identity
Normal file
5
public/.well-known/web-identity
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"provider_urls": [
|
||||
"http://localhost:3000/auth/idp/api/config"
|
||||
]
|
||||
}
|
26
public/fedcm.json
Normal file
26
public/fedcm.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"accounts_endpoint": "/auth/idp/api/accounts",
|
||||
"client_metadata_endpoint": "/auth/idp/api/metadata",
|
||||
"id_assertion_endpoint": "/auth/idp/api/idtokens",
|
||||
"disconnect_endpoint": "/auth/idp/api/disconnect",
|
||||
"login_url": "/auth/idp",
|
||||
"modes": {
|
||||
"active": {
|
||||
"supports_use_other_account": true
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"background_color": "#6200ee",
|
||||
"color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"url": "/images/favicon.dark.svg",
|
||||
"size": 512
|
||||
},
|
||||
{
|
||||
"url": "/images/favicon.light.svg",
|
||||
"size": 512
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
1
public/privacy-policy.txt
Normal file
1
public/privacy-policy.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Privacy Policy comes here.
|
1
public/terms-of-service.txt
Normal file
1
public/terms-of-service.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Terms of Service comes here.
|
101
src/features/auth/index.ts
Normal file
101
src/features/auth/index.ts
Normal file
|
@ -0,0 +1,101 @@
|
|||
import { json, redirect } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
import { useSession } from "vinxi/http";
|
||||
|
||||
export type Middleware = (event: APIEvent) => Response | Promise<Response> | void | Promise<void> | Promise<void | Response>;
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
credential: string;
|
||||
givenName: string;
|
||||
familyName: string;
|
||||
picture: string;
|
||||
approvedClients: any[];
|
||||
}
|
||||
|
||||
const USERS: User[] = [
|
||||
{ id: '20d701f3-0f9f-4c21-a379-81b49f755f9e', username: 'chris', credential: 'test', givenName: 'Chris', familyName: 'Kruining', picture: '', approvedClients: [ '/auth/client' ] },
|
||||
{ id: '10199201-1564-47db-b67b-07088ff05de8', username: 'john', credential: 'test', givenName: 'John', familyName: 'Doe', picture: '', approvedClients: [ '/auth/client' ] },
|
||||
{ id: '633c44b3-8d3d-4dd1-8e1c-7de355d6dced', username: 'chris_alt', credential: 'test', givenName: 'Chris', familyName: 'Kruining', picture: '', approvedClients: [ '/auth/client' ] },
|
||||
{ id: 'b9759798-8a41-4961-94a6-feb2372de9cf', username: 'john_alt', credential: 'test', givenName: 'John', familyName: 'Doe', picture: '', approvedClients: [ '/auth/client' ] },
|
||||
];
|
||||
|
||||
export const getUser = (idOrUsername: string) => {
|
||||
return USERS.find(u => u.id === idOrUsername || u.username === idOrUsername);
|
||||
};
|
||||
|
||||
export const signIn = async (user: User) => {
|
||||
const { update } = await useSession<{ signedIn?: boolean, id?: string }>({
|
||||
password: process.env.SESSION_SECRET!,
|
||||
});
|
||||
|
||||
await update({ signedIn: true, id: user.id });
|
||||
};
|
||||
|
||||
export const signOut = async () => {
|
||||
const { update } = await useSession<{ signedIn?: boolean, id?: string }>({
|
||||
password: process.env.SESSION_SECRET!,
|
||||
});
|
||||
|
||||
await update({});
|
||||
};
|
||||
|
||||
export const use = (...middlewares: Middleware[]) => {
|
||||
return async (event: APIEvent) => {
|
||||
console.log(`received ${event.request.url}`);
|
||||
|
||||
for (const handler of middlewares) {
|
||||
const response = await handler(event);
|
||||
|
||||
console.log(response?.status);
|
||||
|
||||
if (response !== undefined) {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const assertCsrf: Middleware = async ({ request }: APIEvent) => {
|
||||
if (request.headers.get('Sec-Fetch-Dest') !== 'webidentity') {
|
||||
console.log('request failed the csrf test');
|
||||
|
||||
return json({ error: 'Invalid access' }, { status: 400 });
|
||||
}
|
||||
};
|
||||
|
||||
export const assertSession: Middleware = async ({ request, locals }: APIEvent) => {
|
||||
const user = await getSession();
|
||||
|
||||
if (user === undefined) {
|
||||
console.log('user session not available');
|
||||
|
||||
return redirect('/auth/idp', { status: 401 });
|
||||
}
|
||||
|
||||
locals.user = user;
|
||||
};
|
||||
|
||||
export const assertApiSession = async ({ request, locals }: APIEvent) => {
|
||||
const user = await getSession();
|
||||
|
||||
if (user === undefined) {
|
||||
console.log('user session not available');
|
||||
|
||||
return json({ error: 'not signed in' }, { status: 401 });
|
||||
}
|
||||
|
||||
locals.user = user;
|
||||
};
|
||||
|
||||
const getSession = async () => {
|
||||
const { data } = await useSession<{ signedIn?: boolean, id?: string }>({
|
||||
password: process.env.SESSION_SECRET!,
|
||||
});
|
||||
|
||||
if (data.signedIn !== true) {
|
||||
return;
|
||||
}
|
||||
|
||||
return USERS.find(u => u.id === data.id);
|
||||
};
|
|
@ -12,7 +12,7 @@ interface Contents extends Map<string, Map<string, string>> { }
|
|||
|
||||
export const read = (file: File): Promise<Map<string, string> | undefined> => {
|
||||
switch (file.type) {
|
||||
case 'application/json': return json.load(file.text());
|
||||
case 'application/json': return json.load(file.stream());
|
||||
|
||||
default: return Promise.resolve(undefined);
|
||||
}
|
||||
|
|
|
@ -1,20 +1,200 @@
|
|||
import { decode } from "~/utilities";
|
||||
|
||||
export async function load(text: Promise<string>): Promise<Map<string, string>> {
|
||||
const source = JSON.parse(await text);
|
||||
const result = new Map();
|
||||
const candidates = Object.entries(source);
|
||||
export async function load(stream: ReadableStream<Uint8Array>): Promise<Map<string, string>> {
|
||||
return new Map(await Array.fromAsync(parse(stream), ({ key, value }) => [key, value]));
|
||||
}
|
||||
|
||||
while (candidates.length !== 0) {
|
||||
const [ key, value ] = candidates.shift()!;
|
||||
interface Entry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
if (typeof value !== 'object' || value === null || value === undefined) {
|
||||
result.set(key, decode(value as string));
|
||||
interface State {
|
||||
(token: Token): State;
|
||||
entry?: Entry
|
||||
}
|
||||
|
||||
const states = {
|
||||
none(): State {
|
||||
return (token: Token) => {
|
||||
if (token.kind === 'braceOpen') {
|
||||
return states.object();
|
||||
}
|
||||
|
||||
return states.none;
|
||||
};
|
||||
},
|
||||
object({ path = [], expect = 'key' }: Partial<{ path: string[], expect: 'key' | 'colon' | 'value' }> = {}): State {
|
||||
return (token: Token) => {
|
||||
switch (expect) {
|
||||
case 'key': {
|
||||
if (token.kind === 'braceClose') {
|
||||
return states.object({
|
||||
path: path.slice(0, -1),
|
||||
expect: 'key',
|
||||
});
|
||||
}
|
||||
else if (token.kind === 'string') {
|
||||
return states.object({
|
||||
path: [...path, token.value],
|
||||
expect: 'colon'
|
||||
});
|
||||
}
|
||||
|
||||
return states.error(`Expected a key, got ${token.kind} instead`);
|
||||
}
|
||||
|
||||
case 'colon': {
|
||||
if (token.kind !== 'colon') {
|
||||
return states.error(`Expected a ':', got ${token.kind} instead`);
|
||||
}
|
||||
|
||||
return states.object({
|
||||
path,
|
||||
expect: 'value'
|
||||
});
|
||||
}
|
||||
|
||||
case 'value': {
|
||||
if (token.kind === 'braceOpen') {
|
||||
return states.object({
|
||||
path,
|
||||
expect: 'key',
|
||||
});
|
||||
}
|
||||
else if (token.kind === 'string') {
|
||||
const next = states.object({
|
||||
path: path.slice(0, -1),
|
||||
expect: 'key',
|
||||
});
|
||||
|
||||
next.entry = { key: path.join('.'), value: decode(token.value) };
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
return states.error(`Invalid value type found '${token.kind}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return states.none();
|
||||
}
|
||||
else {
|
||||
candidates.unshift(...Object.entries(value).map<[string, any]>(([ k, v ]) => [`${key}.${k}`, v]));
|
||||
},
|
||||
error(message: string): State {
|
||||
throw new Error(message);
|
||||
|
||||
return states.none();
|
||||
},
|
||||
} as const;
|
||||
|
||||
async function* parse(stream: ReadableStream<Uint8Array>): AsyncGenerator<any, void, unknown> {
|
||||
let state = states.none();
|
||||
|
||||
for await (const token of tokenize(read(toGenerator(stream)))) {
|
||||
try {
|
||||
state = state(token);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.entry) {
|
||||
yield state.entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
async function* take<T>(iterable: AsyncIterable<T>, numberToTake: number): AsyncGenerator<T, void, unknown> {
|
||||
let i = 0;
|
||||
for await (const entry of iterable) {
|
||||
yield entry;
|
||||
|
||||
i++;
|
||||
|
||||
if (i === numberToTake) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Token = { start: number, length: number } & (
|
||||
| { kind: 'braceOpen' }
|
||||
| { kind: 'braceClose' }
|
||||
| { kind: 'colon' }
|
||||
| { kind: 'string', value: string }
|
||||
);
|
||||
|
||||
async function* tokenize(characters: AsyncIterable<number>): AsyncGenerator<Token, void, unknown> {
|
||||
let buffer: string = '';
|
||||
let clearBuffer = false;
|
||||
let start = 0;
|
||||
let i = 0;
|
||||
|
||||
for await (const character of characters) {
|
||||
if (buffer.length === 0) {
|
||||
start = i;
|
||||
}
|
||||
|
||||
buffer += String.fromCharCode(character);
|
||||
const length = buffer.length;
|
||||
|
||||
if (buffer === '{') {
|
||||
yield { kind: 'braceOpen', start, length };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer === '}') {
|
||||
yield { kind: 'braceClose', start, length };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer === ':') {
|
||||
yield { kind: 'colon', start, length };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer.length > 1 && buffer.startsWith('"') && buffer.endsWith('"') && buffer.at(-2) !== '\\') {
|
||||
yield { kind: 'string', start, length, value: buffer.slice(1, buffer.length - 1) };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer === ',') {
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer.trim() === '') {
|
||||
clearBuffer = true;
|
||||
}
|
||||
|
||||
if (clearBuffer) {
|
||||
buffer = '';
|
||||
clearBuffer = false;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
async function* read(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<number, void, unknown> {
|
||||
for await (const chunk of chunks) {
|
||||
for (const character of chunk) {
|
||||
yield character;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function* toGenerator<T>(stream: ReadableStream<T>): AsyncGenerator<T, void, unknown> {
|
||||
const reader = stream.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
33
src/routes/auth/client/index.tsx
Normal file
33
src/routes/auth/client/index.tsx
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { onMount } from "solid-js";
|
||||
|
||||
export default function Index() {
|
||||
onMount(async () => {
|
||||
const user = await fetch('/auth/idp/api/user-info').then(r => r.json());
|
||||
|
||||
console.log(user);
|
||||
|
||||
if (user === undefined || true) {
|
||||
try {
|
||||
const credential = await navigator.credentials.get({
|
||||
identity: {
|
||||
providers: [{
|
||||
configURL: new URL('http://localhost:3000/auth/idp/api/config'),
|
||||
clientId: '/auth/client',
|
||||
nonce: 'kaas',
|
||||
loginHint: 'chris',
|
||||
}],
|
||||
mode: 'passive',
|
||||
context: undefined,
|
||||
},
|
||||
mediation: 'silent',
|
||||
});
|
||||
|
||||
console.log(credential);
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return 'WOOT';
|
||||
}
|
8
src/routes/auth/idp/api/[...404].ts
Normal file
8
src/routes/auth/idp/api/[...404].ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { json } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
|
||||
export const GET = ({ request }: APIEvent) => {
|
||||
console.error(`url not found ${request.url}`);
|
||||
|
||||
return json({ error: `url ${request.url} is not implemented` }, { status: 404 })
|
||||
};
|
23
src/routes/auth/idp/api/accounts.ts
Normal file
23
src/routes/auth/idp/api/accounts.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { json } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
import { assertApiSession, assertCsrf, use, User } from "~/features/auth";
|
||||
|
||||
export const GET = use(assertCsrf, assertApiSession, async ({ locals }: APIEvent) => {
|
||||
const { user } = locals;
|
||||
|
||||
console.log('accounts endpoint', user);
|
||||
|
||||
return json({
|
||||
accounts: [
|
||||
{
|
||||
id: user.id,
|
||||
given_name: user.givenName,
|
||||
name: `${user.givenName} ${user.familyName}`,
|
||||
email: user.username,
|
||||
picture: user.picture,
|
||||
login_hints: [user.username],
|
||||
approved_clients: user.approvedClients,
|
||||
}
|
||||
],
|
||||
});
|
||||
});
|
33
src/routes/auth/idp/api/config.ts
Normal file
33
src/routes/auth/idp/api/config.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { json } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
|
||||
export const GET = async ({ request }: APIEvent) => {
|
||||
console.log('config requested', request);
|
||||
|
||||
return json({
|
||||
"accounts_endpoint": "/auth/idp/api/accounts",
|
||||
"client_metadata_endpoint": "/auth/idp/api/metadata",
|
||||
"id_assertion_endpoint": "/auth/idp/api/idtokens",
|
||||
"disconnect_endpoint": "/auth/idp/api/disconnect",
|
||||
"login_url": "/auth/idp",
|
||||
"modes": {
|
||||
"active": {
|
||||
"supports_use_other_account": true
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
"background_color": "#6200ee",
|
||||
"color": "#ffffff",
|
||||
"icons": [
|
||||
{
|
||||
"url": "/images/favicon.dark.svg",
|
||||
"size": 512
|
||||
},
|
||||
{
|
||||
"url": "/images/favicon.light.svg",
|
||||
"size": 512
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
11
src/routes/auth/idp/api/disconnect.ts
Normal file
11
src/routes/auth/idp/api/disconnect.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { json } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
import { use, assertCsrf, assertApiSession } from "~/features/auth";
|
||||
|
||||
export const POST = use(assertCsrf, assertApiSession, async ({ request, locals }: APIEvent) => {
|
||||
console.log(locals, request);
|
||||
|
||||
return json({
|
||||
account_id: locals.user.id,
|
||||
});
|
||||
});
|
15
src/routes/auth/idp/api/idtokens.ts
Normal file
15
src/routes/auth/idp/api/idtokens.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { json } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
import { use, assertCsrf, assertApiSession } from "~/features/auth";
|
||||
|
||||
export const POST = use(assertCsrf, assertApiSession, async ({ request }: APIEvent) => {
|
||||
console.log('id token requested', request.url);
|
||||
|
||||
return json({
|
||||
token: 'THIS IS A BEAUTIFUL TOKEN',
|
||||
}, {
|
||||
headers: {
|
||||
'Set-Login': 'logged-in'
|
||||
}
|
||||
});
|
||||
});
|
39
src/routes/auth/idp/api/login.ts
Normal file
39
src/routes/auth/idp/api/login.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { json, redirect } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
import { getUser, signIn } from "~/features/auth";
|
||||
|
||||
export const POST = async ({ request }: APIEvent) => {
|
||||
console.log('login requested', request.url);
|
||||
|
||||
const formData = await request.formData();
|
||||
const username = formData.get('username');
|
||||
const password = formData.get('password');
|
||||
|
||||
if (typeof username !== 'string' || /^[a-z0-9-_]+$/.test(username) !== true) {
|
||||
return json({ error: 'Bad request' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (typeof password !== 'string' || password.length === 0) {
|
||||
return json({ error: 'Bad request' }, { status: 400 })
|
||||
}
|
||||
|
||||
const user = getUser(username);
|
||||
|
||||
if (user === undefined) {
|
||||
return json({ error: 'Invalid credentials' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (user.credential !== password) {
|
||||
return json({ error: 'Invalid credentials' }, { status: 400 });
|
||||
}
|
||||
|
||||
await signIn(user);
|
||||
|
||||
const token = 'THIS IS MY AWESOME TOKEN';
|
||||
|
||||
return json({ token }, {
|
||||
headers: {
|
||||
'Set-Login': 'logged-in',
|
||||
}
|
||||
});
|
||||
};
|
12
src/routes/auth/idp/api/metadata.ts
Normal file
12
src/routes/auth/idp/api/metadata.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { json } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
|
||||
export const GET = ({ request }: APIEvent) => {
|
||||
console.log('metadata requested', request.url);
|
||||
|
||||
return json({
|
||||
privacy_policy_url: '/privacy-policy.txt',
|
||||
terms_of_service_url: '/terms-of-service.txt',
|
||||
icons: []
|
||||
});
|
||||
};
|
17
src/routes/auth/idp/api/user-info.ts
Normal file
17
src/routes/auth/idp/api/user-info.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { json } from "@solidjs/router";
|
||||
import { APIEvent } from "@solidjs/start/server";
|
||||
import { assertApiSession, use } from "~/features/auth";
|
||||
|
||||
export const GET = use(assertApiSession, async ({ locals }: APIEvent) => {
|
||||
const { user } = locals;
|
||||
|
||||
return json({
|
||||
id: user.id,
|
||||
given_name: user.givenName,
|
||||
name: `${user.givenName} ${user.familyName}`,
|
||||
email: user.username,
|
||||
picture: user.picture,
|
||||
login_hints: [user.username],
|
||||
approved_clients: user.approvedClients,
|
||||
});
|
||||
});
|
17
src/routes/auth/idp/index.tsx
Normal file
17
src/routes/auth/idp/index.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { useParams, useSearchParams } from "@solidjs/router"
|
||||
|
||||
|
||||
export default function Login() {
|
||||
const [params] = useSearchParams();
|
||||
|
||||
return <div>
|
||||
<h1>Login</h1>
|
||||
|
||||
<form method="post" action="/auth/idp/api/login">
|
||||
<label>username: <input type="text" name="username" value={params.login_hint} /></label>
|
||||
<label>password: <input type="password" name="password" value="test" /></label>
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue