Skip to content

Commit

Permalink
feat: add header and more styling
Browse files Browse the repository at this point in the history
  • Loading branch information
FreekBes committed Jul 16, 2024
1 parent 9db8cb0 commit 5888488
Show file tree
Hide file tree
Showing 14 changed files with 414 additions and 117 deletions.
27 changes: 23 additions & 4 deletions src/handlers/authentication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import passport from 'passport';
import OAuth2Strategy from 'passport-oauth2';
import { PrismaClient } from '@prisma/client';
import { INTRA_API_UID, INTRA_API_SECRET, URL_ORIGIN, SESSION_SECRET } from '../env';
import { authenticate, IntraUser } from '../intra/oauth';
import { getIntraUser, IntraUser } from '../intra/oauth';
import { isStudentOrStaff } from '../utils';

export const setupPassport = function(prisma: PrismaClient): void {
passport.use(new OAuth2Strategy({
Expand All @@ -13,8 +14,13 @@ export const setupPassport = function(prisma: PrismaClient): void {
clientSecret: INTRA_API_SECRET,
callbackURL: `${URL_ORIGIN}/login/42/callback`,
}, async (accessToken: string, refreshToken: string, profile: any, cb: any) => {
const user = await authenticate(accessToken);;
return cb(null, user);
try {
const user = await getIntraUser(accessToken);;
return cb(null, user);
}
catch (err) {
return cb(err, false);
}
}));

passport.serializeUser((user: Express.User, cb: any) => {
Expand All @@ -34,7 +40,20 @@ export const setupPassport = function(prisma: PrismaClient): void {
if (!user) {
return cb(new Error('User not found'));
}
cb(null, user);
const intraUser: IntraUser = {
id: user.id,
email: user.email,
login: user.login,
first_name: user.first_name,
last_name: user.last_name,
usual_first_name: user.usual_first_name,
usual_full_name: user.usual_full_name,
display_name: user.display_name,
kind: user.kind,
isStudentOrStaff: await isStudentOrStaff(user),
image_url: user.image,
};
cb(null, intraUser);
});
});
};
Expand Down
5 changes: 5 additions & 0 deletions src/handlers/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,9 @@ export const setupNunjucksFilters = function(app: Express): void {
}
return '';
});

// Debug function to display raw json data
nunjucksEnv.addFilter('dump', (data: any) => {
return JSON.stringify(data, null, 2);
});
};
40 changes: 16 additions & 24 deletions src/handlers/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import express from 'express';
import { Request, Response, NextFunction } from "express";
import { IntraUser } from "../intra/oauth";
import { CustomSessionData } from "./session";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
import { IntraUser } from '../intra/oauth';


const checkIfAuthenticated = function(req: Request, res: Response, next: NextFunction) {
Expand All @@ -22,44 +20,38 @@ const checkIfAuthenticated = function(req: Request, res: Response, next: NextFun
};

export const checkIfStudentOrStaff = async function(req: Request, res: Response, next: NextFunction) {
// If the user account is of kind "admin", let them continue
if ((req.user as IntraUser)?.kind === 'admin') {
return next();
if (!req.user) {
console.warn(`User is not authenticated, denying access to ${req.path}.`);
res.status(401);
return res.send('Unauthorized');
}
// If the student has an ongoing 42cursus, let them continue
const userId = (req.user as IntraUser)?.id;
const cursusUser = await prisma.cursusUser.findFirst({
where: {
user_id: userId,
cursus_id: 21,
end_at: null,
},
});
if (cursusUser) {
if ((req.user as IntraUser)?.isStudentOrStaff === true) {
return next();
}
else {
console.warn(`User ${userId} is not a student with an active 42cursus or staff member, denying access to ${req.path}.`);
console.warn(`User ${(req.user as IntraUser)?.id} is not a student with an active 42cursus or staff member, denying access to ${req.path}.`);
res.status(403);
return res.send('Forbidden');
}
};

const expressErrorHandler = function(err: any, req: Request, res: Response, next: NextFunction) {
if (err === 'User not found') {
return res.redirect('/login/failed');
}
else {
console.error(err);
res.status(500);
return res.send('Internal server error');
console.error(err);
res.status(500);
return res.send('Internal server error');
};

const includeUser = function(req: Request, res: Response, next: NextFunction) {
if (req.isAuthenticated()) {
res.locals.user = req.user;
}
next();
};

export const setupExpressMiddleware = function(app: any) {
app.use(express.static('static'));
app.use(express.static('intra')); // synced content from Intra, like user pictures
app.use(checkIfAuthenticated);
app.use(includeUser);
app.use(expressErrorHandler); // should remain last
};
20 changes: 19 additions & 1 deletion src/intra/oauth.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import https from 'https';
import { CAMPUS_ID } from '../env';
import { isStudentOrStaff } from '../utils';

export interface IntraUser extends Express.User {
id: number;
Expand All @@ -10,10 +12,11 @@ export interface IntraUser extends Express.User {
usual_full_name: string;
display_name: string;
kind: string;
isStudentOrStaff: boolean;
image_url: string | null;
};

export const authenticate = async function(accessToken: string): Promise<IntraUser> {
export const getIntraUser = async function(accessToken: string): Promise<IntraUser> {
try {
const me = await new Promise<any>((resolve, reject) => {
const req = https.get('https://api.intra.42.fr/v2/me', {
Expand All @@ -34,6 +37,20 @@ export const authenticate = async function(accessToken: string): Promise<IntraUs
});
});

if (!me || !me.id || !me.email || !me.login || !me.first_name || !me.last_name || !me.displayname || !me.kind) {
throw new Error('Invalid user data');
}

if (!me.campus || me.campus.length === 0) {
throw new Error('User has no campus');
}

// Check if the user is part of the campus this website was set up for
const campus = me.campus.find((campus: any) => campus.id === CAMPUS_ID);
if (!campus) {
throw new Error('User is not part of the correct campus');
}

const user: IntraUser = {
id: me.id,
email: me.email,
Expand All @@ -44,6 +61,7 @@ export const authenticate = async function(accessToken: string): Promise<IntraUs
usual_full_name: me.usual_full_name,
display_name: me.displayname,
kind: me.kind,
isStudentOrStaff: await isStudentOrStaff(me),
image_url: (me.image && me.image.link ? me.image.versions.medium : null),
};

Expand Down
6 changes: 3 additions & 3 deletions src/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const setupUsersRoutes = function(app: Express, prisma: PrismaClient): vo
],
});

return res.render('users.njk', { users });
return res.render('users.njk', { subtitle: 'Students', users });
});

app.get('/users/staff', passport.authenticate('session'), async (req, res) => {
Expand Down Expand Up @@ -70,7 +70,7 @@ export const setupUsersRoutes = function(app: Express, prisma: PrismaClient): vo
],
});

return res.render('users.njk', { users });
return res.render('users.njk', { subtitle: 'Staff', users });
});

app.get('/users/pisciners', passport.authenticate('session'), async (req, res) => {
Expand Down Expand Up @@ -132,6 +132,6 @@ export const setupUsersRoutes = function(app: Express, prisma: PrismaClient): vo
],
});

return res.render('users.njk', { piscines, users, year, month });
return res.render('users.njk', { subtitle: 'Pisciners', piscines, users, year, month });
});
};
20 changes: 19 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PrismaClient, Location, CursusUser, ProjectUser } from "@prisma/client";
import { PrismaClient, Location, CursusUser, ProjectUser, User } from "@prisma/client";
import { PISCINE_CURSUS_IDS } from "./intra/cursus";
import { IntraUser } from "./intra/oauth";
import NodeCache from "node-cache";
const allPiscineCache = new NodeCache();
const PISCINE_MIN_USER_COUNT = 60;
const prisma = new PrismaClient();

export const monthToNumber = (month: string): number => {
const months = [
Expand Down Expand Up @@ -106,6 +108,22 @@ export const isPiscineDropout = function(cursusUser: CursusUser): boolean {
return cursusUser.end_at.getTime() + precision < usualPiscineEnd.getTime();
};

export const isStudentOrStaff = async function(intraUser: IntraUser | User): Promise<boolean> {
// If the user account is of kind "admin", let them continue
if (intraUser.kind === 'admin') {
return true;
}
// If the student has an ongoing 42cursus, let them continue
const userId = intraUser.id;
const cursusUser = await prisma.cursusUser.findFirst({
where: {
user_id: userId,
cursus_id: 21,
end_at: null,
},
});
return (cursusUser !== null);
};

export const formatDate = function(date: Date): string {
// YYYY-MM-DD HH:MM:SS
Expand Down
Loading

0 comments on commit 5888488

Please sign in to comment.