feat: support skipping onboarding
This commit is contained in:
parent
7b1340c93e
commit
d5fb8a2966
@ -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 (
|
||||
<>
|
||||
<Header {...data} />
|
||||
{data.uiAccess ? (
|
||||
{/* Always show the outlet if we are onboarding */}
|
||||
{(data.onboarding ? true : data.uiAccess) ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<Card className="mx-auto w-fit mt-24">
|
||||
|
||||
@ -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'),
|
||||
|
||||
16
app/routes/users/onboarding-skip.tsx
Normal file
16
app/routes/users/onboarding-skip.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import { LoaderFunctionArgs, redirect } from 'react-router';
|
||||
import { LoadContext } from '~/server';
|
||||
|
||||
export async function loader({
|
||||
request,
|
||||
context,
|
||||
}: LoaderFunctionArgs<LoadContext>) {
|
||||
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');
|
||||
}
|
||||
@ -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() {
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<NavLink to="/onboarding/skip" className="col-span-2 w-max mx-auto">
|
||||
<Button className="flex items-center gap-1">
|
||||
I already know what I'm doing
|
||||
<ArrowRight className="p-1" />
|
||||
</Button>
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -47,12 +47,12 @@ interface CookieOptions {
|
||||
|
||||
class Sessionizer {
|
||||
private storage: SessionStorage<JoinedSession, Error>;
|
||||
private caps: Record<string, Capabilities>;
|
||||
private caps: Record<string, { c: Capabilities; oo?: boolean }>;
|
||||
private capsPath?: string;
|
||||
|
||||
constructor(
|
||||
options: CookieOptions,
|
||||
caps: Record<string, Capabilities>,
|
||||
caps: Record<string, { c: Capabilities; oo?: boolean }>,
|
||||
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<T extends JoinedSession = AuthSession>(request: Request) {
|
||||
return this.storage.getSession(request.headers.get('cookie')) as Promise<
|
||||
Session<T, Error>
|
||||
@ -217,7 +231,13 @@ export async function createSessionStorage(
|
||||
options: CookieOptions,
|
||||
usersPath?: string,
|
||||
) {
|
||||
const map: Record<string, Capabilities> = {};
|
||||
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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user