Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions app/components/navigation/Bar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
<script setup lang="ts">
// TODO: login must come from backend
const isLoggedIn = ref(false);
const { currentUser } = useAuth();
</script>

<template>
Expand All @@ -21,6 +20,20 @@ const isLoggedIn = ref(false);
<span class="md:text-xl">Fresher</span>
</NavigationLink>
</li>
<li v-if="currentUser == null">
<a
class="cursor-pointer font-bold text-white hover:text-white hover:no-underline md:text-xl"
href="/api/auth/signIn">
Log In
</a>
</li>
<li v-else>
<a
class="cursor-pointer font-bold text-white hover:text-white hover:no-underline md:text-xl"
href="/api/auth/signOut">
Log Out
</a>
</li>
</ul>
</nav>
</template>
11 changes: 11 additions & 0 deletions app/composables/useAppState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default () => {
const currentState = useState<State>('state', () => 'closed');
const setState = (state: State) => {
currentState.value = state;
};

return {
currentState,
setState
};
};
11 changes: 11 additions & 0 deletions app/composables/useAuth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default () => {
const currentUser = useState<IStudent | null>('user', () => null);
const setUser = (newUser: IStudent | null) => {
currentUser.value = newUser;
};

return {
currentUser,
setUser
};
};
11 changes: 11 additions & 0 deletions app/middleware/auth.global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const headers = useRequestHeaders(['cookie']);
const { setUser } = useAuth();

const req = await useFetch('/api/family/me', {
credentials: 'same-origin',
headers: headers
});

setUser(!req.error.value ? req.data.value : null);
});
8 changes: 8 additions & 0 deletions app/middleware/restrictPageState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const { currentState, setState } = useAppState();
const { data: newState } = await useFetch('/api/admin/state');

setState(newState.value.state);

if (currentState.value != to.meta.state) return abortNavigation();
});
9 changes: 9 additions & 0 deletions app/middleware/restrictPageUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
const { currentUser } = useAuth();

if (
currentUser == undefined ||
(currentUser.value?.role != to.meta.auth && to.meta.auth != 'authenticated')
)
return abortNavigation();
});
41 changes: 28 additions & 13 deletions app/pages/finish-oauth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,31 @@ const route = useRoute();

const {
code: msCode,
status: msStatus,
state: msState,
error: msError,
error_description: msErrorDesc
} = route.query;
const { status, error } = useFetch('/api/auth/callback', {
method: 'POST',
body: {

let body;
if (msError == undefined) {
body = {
code: msCode,
status: msStatus,
state: msState
};
} else {
body = {
error: msError,
error_description: msErrorDesc
}
};
}

const { status, error } = await useFetch('/api/auth/callback', {
method: 'POST',
body: body,
server: false
});
watch(status, () => {
console.log(status.value);
if (status.value == 'success') {
navigateTo('/survey');
}
Expand All @@ -25,15 +36,19 @@ watch(status, () => {

<template>
<div>
<p v-if="status == 'pending'">We're signing you in...</p>
<p v-else-if="status == 'error'">Error: {{ error }}</p>
<div v-else>
<p>You are being redirected</p>
<p>
<Card v-if="status == 'pending' || status == 'idle'">
<CardTitle>We're signing you in...</CardTitle>
</Card>
<Card v-else-if="status == 'error'">
<CardText>{{ error }}</CardText>
</Card>
<Card v-else-if="status == 'success'">
<CardTitle>You are being redirected</CardTitle>
<CardText>
Please click
<NuxtLink to="/survey">this link</NuxtLink>
if not automatically redirected.
</p>
</div>
</CardText>
</Card>
</div>
</template>
5 changes: 5 additions & 0 deletions app/pages/parent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ const parent2: IStudent = {
};

const kids: IStudent[] = [];

definePageMeta({
auth: 'parent',
middleware: ['restrict-page-user']
});
</script>

<template>
Expand Down
23 changes: 18 additions & 5 deletions app/utils/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
declare interface IStudent {
firstName: string;
lastName: string;
shortcode: string;
preferredName?: string;
selfDescription?: string;
socialMedia?: string;
name: string | null;
jmc: boolean;
role: 'fresher' | 'parent';
completedSurvey: boolean;
gender: 'male' | 'female' | 'other' | 'n/a' | null;
interests: Map<string, 0 | 1 | 2>[] | null;
socials: string[] | null;
aboutMe: string | null;
}

// Is it worth making a sharedTypes for this one singular type?
// Unsure if IStudent would be able to go under that as it's a z.infer
declare const stateOptions = [
'parents_open',
'parents_close',
'freshers_open',
'closed'
] as const;
declare type State = (typeof stateOptions)[number];
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion example.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
TENANT_ID =
CLIENT_ID =
CLIENT_SECRET =
BASE_URL =
BASE_URL = http(s)://
JWT_SECRET =
WEBMASTERS = shortcode,codeshort
1 change: 0 additions & 1 deletion hono/admin/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export const admin = factory
hasJmc: bool
}
*/

})
.get('/all-unallocated-freshers', grantAccessTo('admin'), async ctx => {
/*
Expand Down
41 changes: 29 additions & 12 deletions hono/auth/auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { MicrosoftGraphClient, MsAuthClient } from './MsApiClient';
import { grantAccessTo, isFresherOrParent, newToken } from './jwt';
import {
generateCookieHeader,
grantAccessTo,
isFresherOrParent,
newToken
} from './jwt';
import factory from '../factory';
import { apiLogger } from '../logger';
import db from '../db';
import { students } from '../family/schema';
import { eq } from 'drizzle-orm';

const msAuth = new MsAuthClient(
['profile'],
['User.Read'],
{
tenantId: process.env.TENANT_ID!,
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!
},
`http://${process.env.BASE_URL}/finish-oauth`
`${process.env.BASE_URL}/finish-oauth`
);

const callbackSchema = z.object({
Expand All @@ -31,11 +36,26 @@ const auth = factory
// Redirect the user to the Microsoft oAuth sign in.
return ctx.redirect(msAuth.getRedirectUrl());
})
.post('/signOut', grantAccessTo('authenticated'), async ctx => {
// Delete their JWT cookie.
ctx.header('Set-Cookie', `Authorization= ; Max-Age=0; HttpOnly`);
return ctx.text('', 200);
})
.get(
'/signOut',
zValidator(
'query',
z.object({
redirect: z.string().optional()
})
),
grantAccessTo('authenticated'),
async ctx => {
// Delete their JWT cookie.
ctx.header('Set-Cookie', generateCookieHeader('', 0));
const query = ctx.req.valid('query');

const path = query.redirect || '';
const redirectUrl = process.env.BASE_URL! + path + '?loggedOut=true';

return ctx.redirect(redirectUrl);
}
)
.post(
'/callback',
grantAccessTo('unauthenticated'),
Expand Down Expand Up @@ -103,10 +123,7 @@ const auth = factory
// Expire the JWT after 4 weeks.
// Should be long enough for MaDs to only sign in once.
const maxAge = 28 * 24 * 60 * 60;
ctx.header(
'Set-Cookie',
`Authorization=${token}; Max-Age=${maxAge}; HttpOnly`
);
ctx.header('Set-Cookie', generateCookieHeader(token, maxAge));

let completedSurvey = false;
const studentInDb = await db
Expand Down
6 changes: 5 additions & 1 deletion hono/auth/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import { apiLogger } from '../logger';
const secret = process.env.JWT_SECRET!;
const webmasters = process.env.WEBMASTERS!.split(',');

export const generateCookieHeader = (token: string, maxAge: number) =>
`Authorization=${token}; Max-Age=${maxAge}; HttpOnly; SameSite=Lax; Path=/`;

export function isFresherOrParent(email: string): 'fresher' | 'parent' {
const entryYear = email.match(/[0-9]{2}(?=@)/);

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

if (roles.includes('admin') && webmasters.includes(shortcode)) return await next();
if (roles.includes('admin') && webmasters.includes(shortcode))
return await next();

if (roles.includes(role) || roles.includes('authenticated')) {
return await next();
Expand Down
12 changes: 7 additions & 5 deletions hono/db.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { Database } from 'bun:sqlite';
import { drizzle } from 'drizzle-orm/bun-sqlite';
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
import drizzleConfig from '../drizzle.config.json'
import drizzleConfig from '../drizzle.config.json';
import { meta } from './admin/schema';

const database = new Database('db.sqlite', { create: true, strict: true });
const db = drizzle(database);
migrate(db, { migrationsFolder: drizzleConfig.out });
try {
db.insert(meta).values({
id: 1,
state: 'parents_open'
}).run()
db.insert(meta)
.values({
id: 1,
state: 'parents_open'
})
.run();
} catch (e) {
// This just means the meta row is already inserted,
// so do nothing.
Expand Down
3 changes: 2 additions & 1 deletion hono/routes.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ type Response = {
{
proposal: string;
proposee: string;
}[]
}
[];
```

## `GET /me` - authenticated
Expand Down
Loading