diff --git a/app/layouts/shell.tsx b/app/layouts/shell.tsx index 5fb903f..76f37cb 100644 --- a/app/layouts/shell.tsx +++ b/app/layouts/shell.tsx @@ -63,6 +63,11 @@ export async function loader({ return false; } + if (context.sessions.onboardForSubject(sessionUser.subject)) { + // Assume onboarded + return true; + } + return subject === sessionUser.subject; }); @@ -102,7 +107,8 @@ export default function Shell() { return ( <>
- {data.uiAccess ? ( + {/* Always show the outlet if we are onboarding */} + {(data.onboarding ? true : data.uiAccess) ? ( ) : ( diff --git a/app/routes.ts b/app/routes.ts index 845ff15..fd56844 100644 --- a/app/routes.ts +++ b/app/routes.ts @@ -15,6 +15,7 @@ export default [ // Double nested to separate error propagations layout('layouts/shell.tsx', [ route('/onboarding', 'routes/users/onboarding.tsx'), + route('/onboarding/skip', 'routes/users/onboarding-skip.tsx'), layout('layouts/dashboard.tsx', [ ...prefix('/machines', [ index('routes/machines/overview.tsx'), diff --git a/app/routes/users/onboarding-skip.tsx b/app/routes/users/onboarding-skip.tsx new file mode 100644 index 0000000..06a9837 --- /dev/null +++ b/app/routes/users/onboarding-skip.tsx @@ -0,0 +1,16 @@ +import { LoaderFunctionArgs, redirect } from 'react-router'; +import { LoadContext } from '~/server'; + +export async function loader({ + request, + context, +}: LoaderFunctionArgs) { + const session = await context.sessions.auth(request); + const user = session.get('user'); + if (!user) { + return redirect('/login'); + } + + context.sessions.overrideOnboarding(user.subject, true); + return redirect('/machines'); +} diff --git a/app/routes/users/onboarding.tsx b/app/routes/users/onboarding.tsx index 4f7b505..13faca7 100644 --- a/app/routes/users/onboarding.tsx +++ b/app/routes/users/onboarding.tsx @@ -1,3 +1,4 @@ +import { ArrowRight } from 'lucide-react'; import { useEffect } from 'react'; import { GrApple } from 'react-icons/gr'; import { ImFinder } from 'react-icons/im'; @@ -335,6 +336,12 @@ export default function Page() { )} + + + ); diff --git a/app/server/web/sessions.ts b/app/server/web/sessions.ts index c370ba3..0cc05a0 100644 --- a/app/server/web/sessions.ts +++ b/app/server/web/sessions.ts @@ -47,12 +47,12 @@ interface CookieOptions { class Sessionizer { private storage: SessionStorage; - private caps: Record; + private caps: Record; private capsPath?: string; constructor( options: CookieOptions, - caps: Record, + caps: Record, capsPath?: string, ) { this.caps = caps; @@ -86,7 +86,7 @@ class Sessionizer { } roleForSubject(subject: string): keyof typeof Roles | undefined { - const role = this.caps[subject]; + const role = this.caps[subject].c; // We need this in string form based on Object.keys of the roles for (const [key, value] of Object.entries(Roles)) { if (value === role) { @@ -95,6 +95,10 @@ class Sessionizer { } } + onboardForSubject(subject: string) { + return this.caps[subject].oo ?? false; + } + // Given an OR of capabilities, check if the session has the required // capabilities. If not, return false. Can throw since it calls auth() async check(request: Request, capabilities: Capabilities) { @@ -120,10 +124,10 @@ class Sessionizer { const role = this.caps[subject]; if (!role) { const memberRole = await this.registerSubject(subject); - return (capabilities & memberRole) === capabilities; + return (capabilities & memberRole.c) === capabilities; } - return (capabilities & role) === capabilities; + return (capabilities & role.c) === capabilities; } async checkSubject(subject: string, capabilities: Capabilities) { @@ -143,10 +147,10 @@ class Sessionizer { const role = this.caps[subject]; if (!role) { const memberRole = await this.registerSubject(subject); - return (capabilities & memberRole) === capabilities; + return (capabilities & memberRole.c) === capabilities; } - return (capabilities & role) === capabilities; + return (capabilities & role.c) === capabilities; } // This code is very simple, if the user does not exist in the database @@ -160,13 +164,13 @@ class Sessionizer { if (Object.keys(this.caps).length === 0) { log.debug('auth', 'First user registered as owner: %s', subject); - this.caps[subject] = Roles.owner; + this.caps[subject] = { c: Roles.owner }; await this.flushUserDatabase(); return this.caps[subject]; } log.debug('auth', 'New user registered as member: %s', subject); - this.caps[subject] = Roles.member; + this.caps[subject] = { c: Roles.member }; await this.flushUserDatabase(); return this.caps[subject]; } @@ -176,7 +180,11 @@ class Sessionizer { return; } - const data = Object.entries(this.caps).map(([u, c]) => ({ u, c })); + const data = Object.entries(this.caps).map(([u, { c, oo }]) => ({ + u, + c, + oo, + })); try { const handle = await open(this.capsPath, 'w'); await handle.write(JSON.stringify(data)); @@ -193,11 +201,17 @@ class Sessionizer { return false; } - this.caps[subject] = Roles[role]; + this.caps[subject].c = Roles[role]; await this.flushUserDatabase(); return true; } + // Overrides the onboarding status for a subject + async overrideOnboarding(subject: string, onboarding: boolean) { + this.caps[subject].oo = onboarding; + await this.flushUserDatabase(); + } + getOrCreate(request: Request) { return this.storage.getSession(request.headers.get('cookie')) as Promise< Session @@ -217,7 +231,13 @@ export async function createSessionStorage( options: CookieOptions, usersPath?: string, ) { - const map: Record = {}; + const map: Record< + string, + { + c: number; + oo?: boolean; + } + > = {}; if (usersPath) { // We need to load our users from the file (default to empty map) // We then translate each user into a capability object using the helper @@ -226,7 +246,10 @@ export async function createSessionStorage( log.debug('config', 'Loaded %d users from database', data.length); for (const user of data) { - map[user.u] = user.c; + map[user.u] = { + c: user.c, + oo: user.oo, + }; } } @@ -248,7 +271,11 @@ async function loadUserFile(path: string) { try { const data = await readFile(realPath, 'utf8'); - const users = JSON.parse(data.trim()) as { u?: string; c?: number }[]; + const users = JSON.parse(data.trim()) as { + u?: string; + c?: number; + oo?: boolean; + }[]; // Never trust user input return users.filter( @@ -256,6 +283,7 @@ async function loadUserFile(path: string) { ) as { u: string; c: number; + oo?: boolean; }[]; } catch (error) { log.debug('config', 'Error reading user database file: %s', error);