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);