Skip to content

Commit c8e4748

Browse files
Dropheartcybercoder-naj
andauthoredSep 18, 2024
frontend: auth (#7)
* initial attempt at log in - committing to continue on another device * signin/signout buttons now work; some changes to backend cookie for this. * silly mistake + establish something is defined * env file now contains protocol * better (not good) log in button + auth composable * SSR friendly useAuth * i forgor how middleware works sighhhhhhhhh but now it works nice and clean * remove unnecessary log + add 'authenticated' * format + clear user if signed out / expired * made it a bit less janky but server is erroring * Add restrict page state * amended previous commit * change msauth scope --------- Co-authored-by: Nishant Aanjaney Jalan <[email protected]>
1 parent 283af7a commit c8e4748

18 files changed

+177
-89
lines changed
 

‎app/components/navigation/Bar.vue

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<script setup lang="ts">
2-
// TODO: login must come from backend
3-
const isLoggedIn = ref(false);
2+
const { currentUser } = useAuth();
43
</script>
54

65
<template>
@@ -21,6 +20,20 @@ const isLoggedIn = ref(false);
2120
<span class="md:text-xl">Fresher</span>
2221
</NavigationLink>
2322
</li>
23+
<li v-if="currentUser == null">
24+
<a
25+
class="cursor-pointer font-bold text-white hover:text-white hover:no-underline md:text-xl"
26+
href="/api/auth/signIn">
27+
Log In
28+
</a>
29+
</li>
30+
<li v-else>
31+
<a
32+
class="cursor-pointer font-bold text-white hover:text-white hover:no-underline md:text-xl"
33+
href="/api/auth/signOut">
34+
Log Out
35+
</a>
36+
</li>
2437
</ul>
2538
</nav>
2639
</template>

‎app/composables/useAppState.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default () => {
2+
const currentState = useState<State>('state', () => 'closed');
3+
const setState = (state: State) => {
4+
currentState.value = state;
5+
};
6+
7+
return {
8+
currentState,
9+
setState
10+
};
11+
};

‎app/composables/useAuth.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default () => {
2+
const currentUser = useState<IStudent | null>('user', () => null);
3+
const setUser = (newUser: IStudent | null) => {
4+
currentUser.value = newUser;
5+
};
6+
7+
return {
8+
currentUser,
9+
setUser
10+
};
11+
};

‎app/middleware/auth.global.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default defineNuxtRouteMiddleware(async (to, from) => {
2+
const headers = useRequestHeaders(['cookie']);
3+
const { setUser } = useAuth();
4+
5+
const req = await useFetch('/api/family/me', {
6+
credentials: 'same-origin',
7+
headers: headers
8+
});
9+
10+
setUser(!req.error.value ? req.data.value : null);
11+
});

‎app/middleware/restrictPageState.ts

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default defineNuxtRouteMiddleware(async (to, from) => {
2+
const { currentState, setState } = useAppState();
3+
const { data: newState } = await useFetch('/api/admin/state');
4+
5+
setState(newState.value.state);
6+
7+
if (currentState.value != to.meta.state) return abortNavigation();
8+
});

‎app/middleware/restrictPageUser.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default defineNuxtRouteMiddleware(async (to, from) => {
2+
const { currentUser } = useAuth();
3+
4+
if (
5+
currentUser == undefined ||
6+
(currentUser.value?.role != to.meta.auth && to.meta.auth != 'authenticated')
7+
)
8+
return abortNavigation();
9+
});

‎app/pages/finish-oauth.vue

+28-13
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,31 @@ const route = useRoute();
33
44
const {
55
code: msCode,
6-
status: msStatus,
6+
state: msState,
77
error: msError,
88
error_description: msErrorDesc
99
} = route.query;
10-
const { status, error } = useFetch('/api/auth/callback', {
11-
method: 'POST',
12-
body: {
10+
11+
let body;
12+
if (msError == undefined) {
13+
body = {
1314
code: msCode,
14-
status: msStatus,
15+
state: msState
16+
};
17+
} else {
18+
body = {
1519
error: msError,
1620
error_description: msErrorDesc
17-
}
21+
};
22+
}
23+
24+
const { status, error } = await useFetch('/api/auth/callback', {
25+
method: 'POST',
26+
body: body,
27+
server: false
1828
});
1929
watch(status, () => {
30+
console.log(status.value);
2031
if (status.value == 'success') {
2132
navigateTo('/survey');
2233
}
@@ -25,15 +36,19 @@ watch(status, () => {
2536

2637
<template>
2738
<div>
28-
<p v-if="status == 'pending'">We're signing you in...</p>
29-
<p v-else-if="status == 'error'">Error: {{ error }}</p>
30-
<div v-else>
31-
<p>You are being redirected</p>
32-
<p>
39+
<Card v-if="status == 'pending' || status == 'idle'">
40+
<CardTitle>We're signing you in...</CardTitle>
41+
</Card>
42+
<Card v-else-if="status == 'error'">
43+
<CardText>{{ error }}</CardText>
44+
</Card>
45+
<Card v-else-if="status == 'success'">
46+
<CardTitle>You are being redirected</CardTitle>
47+
<CardText>
3348
Please click
3449
<NuxtLink to="/survey">this link</NuxtLink>
3550
if not automatically redirected.
36-
</p>
37-
</div>
51+
</CardText>
52+
</Card>
3853
</div>
3954
</template>

‎app/pages/parent.vue

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ const parent2: IStudent = {
1717
};
1818
1919
const kids: IStudent[] = [];
20+
21+
definePageMeta({
22+
auth: 'parent',
23+
middleware: ['restrict-page-user']
24+
});
2025
</script>
2126

2227
<template>

‎app/utils/types.d.ts

+18-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,21 @@
11
declare interface IStudent {
2-
firstName: string;
3-
lastName: string;
42
shortcode: string;
5-
preferredName?: string;
6-
selfDescription?: string;
7-
socialMedia?: string;
3+
name: string | null;
4+
jmc: boolean;
5+
role: 'fresher' | 'parent';
6+
completedSurvey: boolean;
7+
gender: 'male' | 'female' | 'other' | 'n/a' | null;
8+
interests: Map<string, 0 | 1 | 2>[] | null;
9+
socials: string[] | null;
10+
aboutMe: string | null;
811
}
12+
13+
// Is it worth making a sharedTypes for this one singular type?
14+
// Unsure if IStudent would be able to go under that as it's a z.infer
15+
declare const stateOptions = [
16+
'parents_open',
17+
'parents_close',
18+
'freshers_open',
19+
'closed'
20+
] as const;
21+
declare type State = (typeof stateOptions)[number];

‎bun.lockb

724 Bytes
Binary file not shown.

‎example.env

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
TENANT_ID =
22
CLIENT_ID =
33
CLIENT_SECRET =
4-
BASE_URL =
4+
BASE_URL = http(s)://
55
JWT_SECRET =
66
WEBMASTERS = shortcode,codeshort

‎hono/admin/admin.ts

-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export const admin = factory
9898
hasJmc: bool
9999
}
100100
*/
101-
102101
})
103102
.get('/all-unallocated-freshers', grantAccessTo('admin'), async ctx => {
104103
/*

‎hono/auth/auth.ts

+29-12
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
import { zValidator } from '@hono/zod-validator';
22
import { z } from 'zod';
33
import { MicrosoftGraphClient, MsAuthClient } from './MsApiClient';
4-
import { grantAccessTo, isFresherOrParent, newToken } from './jwt';
4+
import {
5+
generateCookieHeader,
6+
grantAccessTo,
7+
isFresherOrParent,
8+
newToken
9+
} from './jwt';
510
import factory from '../factory';
611
import { apiLogger } from '../logger';
712
import db from '../db';
813
import { students } from '../family/schema';
914
import { eq } from 'drizzle-orm';
1015

1116
const msAuth = new MsAuthClient(
12-
['profile'],
17+
['User.Read'],
1318
{
1419
tenantId: process.env.TENANT_ID!,
1520
clientId: process.env.CLIENT_ID!,
1621
clientSecret: process.env.CLIENT_SECRET!
1722
},
18-
`http://${process.env.BASE_URL}/finish-oauth`
23+
`${process.env.BASE_URL}/finish-oauth`
1924
);
2025

2126
const callbackSchema = z.object({
@@ -31,11 +36,26 @@ const auth = factory
3136
// Redirect the user to the Microsoft oAuth sign in.
3237
return ctx.redirect(msAuth.getRedirectUrl());
3338
})
34-
.post('/signOut', grantAccessTo('authenticated'), async ctx => {
35-
// Delete their JWT cookie.
36-
ctx.header('Set-Cookie', `Authorization= ; Max-Age=0; HttpOnly`);
37-
return ctx.text('', 200);
38-
})
39+
.get(
40+
'/signOut',
41+
zValidator(
42+
'query',
43+
z.object({
44+
redirect: z.string().optional()
45+
})
46+
),
47+
grantAccessTo('authenticated'),
48+
async ctx => {
49+
// Delete their JWT cookie.
50+
ctx.header('Set-Cookie', generateCookieHeader('', 0));
51+
const query = ctx.req.valid('query');
52+
53+
const path = query.redirect || '';
54+
const redirectUrl = process.env.BASE_URL! + path + '?loggedOut=true';
55+
56+
return ctx.redirect(redirectUrl);
57+
}
58+
)
3959
.post(
4060
'/callback',
4161
grantAccessTo('unauthenticated'),
@@ -103,10 +123,7 @@ const auth = factory
103123
// Expire the JWT after 4 weeks.
104124
// Should be long enough for MaDs to only sign in once.
105125
const maxAge = 28 * 24 * 60 * 60;
106-
ctx.header(
107-
'Set-Cookie',
108-
`Authorization=${token}; Max-Age=${maxAge}; HttpOnly`
109-
);
126+
ctx.header('Set-Cookie', generateCookieHeader(token, maxAge));
110127

111128
let completedSurvey = false;
112129
const studentInDb = await db

‎hono/auth/jwt.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import { apiLogger } from '../logger';
1111
const secret = process.env.JWT_SECRET!;
1212
const webmasters = process.env.WEBMASTERS!.split(',');
1313

14+
export const generateCookieHeader = (token: string, maxAge: number) =>
15+
`Authorization=${token}; Max-Age=${maxAge}; HttpOnly; SameSite=Lax; Path=/`;
16+
1417
export function isFresherOrParent(email: string): 'fresher' | 'parent' {
1518
const entryYear = email.match(/[0-9]{2}(?=@)/);
1619

@@ -97,7 +100,8 @@ export const grantAccessTo = (...roles: [AuthRoles, ...AuthRoles[]]) =>
97100
else return ctx.text(no_auth, 403);
98101
}
99102

100-
if (roles.includes('admin') && webmasters.includes(shortcode)) return await next();
103+
if (roles.includes('admin') && webmasters.includes(shortcode))
104+
return await next();
101105

102106
if (roles.includes(role) || roles.includes('authenticated')) {
103107
return await next();

‎hono/db.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import { Database } from 'bun:sqlite';
22
import { drizzle } from 'drizzle-orm/bun-sqlite';
33
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
4-
import drizzleConfig from '../drizzle.config.json'
4+
import drizzleConfig from '../drizzle.config.json';
55
import { meta } from './admin/schema';
66

77
const database = new Database('db.sqlite', { create: true, strict: true });
88
const db = drizzle(database);
99
migrate(db, { migrationsFolder: drizzleConfig.out });
1010
try {
11-
db.insert(meta).values({
12-
id: 1,
13-
state: 'parents_open'
14-
}).run()
11+
db.insert(meta)
12+
.values({
13+
id: 1,
14+
state: 'parents_open'
15+
})
16+
.run();
1517
} catch (e) {
1618
// This just means the meta row is already inserted,
1719
// so do nothing.

‎hono/routes.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ type Response = {
107107
{
108108
proposal: string;
109109
proposee: string;
110-
}[]
110+
}
111+
[];
111112
```
112113

113114
## `GET /me` - authenticated

0 commit comments

Comments
 (0)
Please sign in to comment.