Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trpc #467

Merged
merged 67 commits into from
Oct 23, 2024
Merged

Trpc #467

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
6681b61
working
js0mmer Feb 25, 2024
2aa8369
Merge branch 'master' into trpc
js0mmer Mar 2, 2024
9f82fe9
Merge branch 'master' into trpc
js0mmer Mar 20, 2024
ad0ee60
Implement reports route in tRPC
js0mmer Mar 21, 2024
1d45ff5
Merge branch 'master' into trpc
js0mmer Mar 21, 2024
12b159c
reports
ptruong0 Apr 10, 2024
16d2536
users
ptruong0 Apr 10, 2024
9ba1107
courses, professors, schedule
ptruong0 Apr 12, 2024
cadc40f
fixes
ptruong0 Apr 19, 2024
86c8b4e
lint
ptruong0 Apr 20, 2024
c30d0c2
lint
ptruong0 Apr 20, 2024
5ad4d72
uninstall zod
ptruong0 Apr 29, 2024
ab287c0
Merge branch 'master' into trpc
ptruong0 May 6, 2024
b0af219
merge conflict
ptruong0 May 6, 2024
3ce6793
types
ptruong0 May 6, 2024
5888f4e
a
ptruong0 May 6, 2024
56e95e3
help
ptruong0 May 8, 2024
b575831
h
ptruong0 May 8, 2024
b50cc60
h
ptruong0 May 8, 2024
93058b7
d
ptruong0 May 8, 2024
a096487
pull pnpm
ptruong0 May 20, 2024
1bae181
Merge branch 'master' into trpc
ptruong0 May 22, 2024
fcc19c1
merge conflict fix
ptruong0 May 22, 2024
41fdc5e
Merge branch 'master' into trpc
js0mmer Oct 11, 2024
1081240
fix lint error
js0mmer Oct 11, 2024
cfe33e8
fix other random lint error
js0mmer Oct 11, 2024
8caa6f0
fix sst build error
js0mmer Oct 11, 2024
4dbcab2
switch to zod
js0mmer Oct 11, 2024
20c6187
start work on shared types
js0mmer Oct 11, 2024
2f2a4bc
fix lockfile
js0mmer Oct 11, 2024
40c147c
fix reviews get aggregate to work with id for report page
js0mmer Oct 11, 2024
07664b9
migrate report types to shared types package. tweak query to have mon…
js0mmer Oct 11, 2024
dcbcfd8
Update reportgroup useEffect deps
js0mmer Oct 11, 2024
3d008b6
update reports useEffect deps
js0mmer Oct 11, 2024
398c020
remove hello world query
js0mmer Oct 11, 2024
a22fab9
grades return types
js0mmer Oct 11, 2024
3ece2eb
schedule return types
js0mmer Oct 11, 2024
301f2ca
update api type imports
js0mmer Oct 11, 2024
5df806e
report content length validation
js0mmer Oct 11, 2024
413cc4c
adjust report types
js0mmer Oct 12, 2024
1680a23
Shared course & professor types
js0mmer Oct 12, 2024
a2d79b7
Passport user type
js0mmer Oct 12, 2024
a945ddf
Organize types
js0mmer Oct 12, 2024
a3dd3dd
Fix user type
js0mmer Oct 12, 2024
df6dc1c
Prereq tree type
js0mmer Oct 12, 2024
70048bb
Roadmap types and trpc
js0mmer Oct 12, 2024
0742627
change trpc client export name
js0mmer Oct 12, 2024
31fc4d0
Merge branch 'master' into trpc
js0mmer Oct 16, 2024
da6a2d5
migrate reviews to trpc + review types
js0mmer Oct 17, 2024
93ebd53
remove package lock
js0mmer Oct 17, 2024
a1998c8
move nodemon to dev dep
js0mmer Oct 17, 2024
4cd1cb9
remove package-lock
js0mmer Oct 17, 2024
90897f6
Add comments to reviews route
js0mmer Oct 17, 2024
83c18e0
Remove console log
js0mmer Oct 17, 2024
dd8c1c7
adjust type usage in models
js0mmer Oct 17, 2024
9dbba20
Update site/src/helpers/planner.ts
js0mmer Oct 22, 2024
b0f39ed
Apply suggestions from code review
js0mmer Oct 22, 2024
526c098
fix formatting
js0mmer Oct 22, 2024
3c9a8ba
non-null assertion
js0mmer Oct 22, 2024
0944b3a
remove unused type
js0mmer Oct 22, 2024
edf7e1b
add todo to migrate legacy quarter names
js0mmer Oct 22, 2024
300a3a0
YearAndQuarter type
js0mmer Oct 22, 2024
53823ce
fix missing dep on admin page + remove unused scss
js0mmer Oct 23, 2024
a6d2042
add missing hook deps on schedule component
js0mmer Oct 23, 2024
0515a4e
tweaks to auth routes
js0mmer Oct 23, 2024
a5e4250
tweak admin prop on session
js0mmer Oct 23, 2024
665329e
remove empty object export
js0mmer Oct 23, 2024
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
14 changes: 9 additions & 5 deletions api/package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "peterportal-core-api",
"version": "1.0.0",
"main": "dist/app.js",
"main": "src/app.ts",
"type": "module",
"scripts": {
"start": "node dist/app.js",
"dev": "tsc-watch --onSuccess \"pnpm start\"",
"dev": "nodemon --exec tsx ./src/app.ts",
Awesome-E marked this conversation as resolved.
Show resolved Hide resolved
"build": "tsc",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0 && tsc --noEmit"
},
"dependencies": {
"@trpc/server": "^10.45.1",
"@vendia/serverless-express": "^4.12.6",
"axios": "^1.6.8",
"connect-mongodb-session": "^5.0.0",
Expand All @@ -20,9 +21,10 @@
"morgan": "^1.10.0",
"passport": "^0.7.0",
"passport-google-oauth": "^2.0.0",
"typescript": "^5.4.5"
"zod": "^3.23.8"
},
"devDependencies": {
"@peterportal/types": "workspace:*",
"@types/connect-mongodb-session": "^2.4.7",
"@types/cookie-parser": "^1.4.7",
"@types/dotenv-flow": "^3.3.3",
Expand All @@ -35,6 +37,8 @@
"@typescript-eslint/parser": "^7.8.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"tsc-watch": "^6.2.0"
"nodemon": "^3.1.7",
"tsx": "^4.19.1",
"typescript": "^5.4.2"
}
}
34 changes: 17 additions & 17 deletions api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import session from 'express-session';
import MongoDBStore from 'connect-mongodb-session';
import dotenv from 'dotenv-flow';
import serverlessExpress from '@vendia/serverless-express';
import * as trpcExpress from '@trpc/server/adapters/express';
import mongoose, { Mongoose } from 'mongoose';
// load env
dotenv.config();
Expand All @@ -19,15 +20,12 @@ dotenv.config();
import { DB_NAME, COLLECTION_NAMES } from './helpers/mongo';

// Custom Routes
import coursesRouter from './controllers/courses';
import professorsRouter from './controllers/professors';
import scheduleRouter from './controllers/schedule';
import reviewsRouter from './controllers/reviews';
import usersRouter from './controllers/users';
import roadmapRouter from './controllers/roadmap';
import reportsRouter from './controllers/reports';
import authRouter from './controllers/auth';

import { SESSION_LENGTH } from './config/constants';
import { createContext } from './helpers/trpc';
import { appRouter } from './controllers';
import passportInit from './config/passport';

// instantiate app
const app = express();
Expand Down Expand Up @@ -69,7 +67,7 @@ app.use(
if (process.env.GOOGLE_CLIENT && process.env.GOOGLE_SECRET) {
app.use(passport.initialize());
app.use(passport.session());
require('./config/passport');
passportInit();
} else {
console.log('GOOGLE_CLIENT and/or GOOGLE_SECRET env var(s) not defined! Google login will not be available.');
}
Expand Down Expand Up @@ -100,15 +98,17 @@ app.use(function (req, res, next) {
*/

// Enable custom routes
const router = express.Router();
router.use('/courses', coursesRouter);
router.use('/professors', professorsRouter);
router.use('/schedule', scheduleRouter);
router.use('/reviews', reviewsRouter);
router.use('/users', usersRouter);
router.use('/roadmap', roadmapRouter);
router.use('/reports', reportsRouter);
app.use('/api', router);
const expressRouter = express.Router();
expressRouter.use('/users/auth', authRouter);
expressRouter.use(
'/trpc',
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext,
}),
);

app.use('/api', expressRouter);

/**
* Error Handler
Expand Down
68 changes: 35 additions & 33 deletions api/src/config/passport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,43 @@
@module PassportConfig
*/

import { User } from 'express-session';
import { User } from '@peterportal/types';
import passport from 'passport';
import { OAuth2Strategy as GoogleStrategy } from 'passport-google-oauth';

passport.serializeUser(function (user, done) {
done(null, user);
});
export default function passportInit() {
passport.serializeUser(function (user, done) {
done(null, user);
});

passport.deserializeUser(function (user: false | User | null | undefined, done) {
done(null, user);
});
passport.deserializeUser(function (user: false | User | null | undefined, done) {
done(null, user);
});

/**
* Configuration for Google Strategy
*/
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.PRODUCTION_DOMAIN + '/api/users/auth/google/callback',
},
function (accessToken, refreshToken, profile, done) {
let email = '';
// get the first registered email
if (profile.emails && profile.emails.length! > 0) {
email = profile.emails[0].value;
}
const userData = {
id: profile.id,
email: email,
name: profile.displayName,
picture: profile._json.picture,
};
done(null, userData);
},
),
);
/**
* Configuration for Google Strategy
*/
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT,
clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.PRODUCTION_DOMAIN + '/api/users/auth/google/callback',
},
function (accessToken, refreshToken, profile, done) {
let email = '';
// get the first registered email
if (profile.emails && profile.emails.length! > 0) {
email = profile.emails[0].value;
}
const userData = {
id: profile.id,
email: email,
name: profile.displayName,
picture: profile._json.picture,
};
done(null, userData);
},
),
);
}
84 changes: 84 additions & 0 deletions api/src/controllers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import express, { Request, Response } from 'express';
import passport from 'passport';
import { SESSION_LENGTH } from '../config/constants';
import { User } from '@peterportal/types';

const router = express.Router();

/**
* Called after successful authentication
* @param req Express Request Object
* @param res Express Response Object
*/
function successLogin(req: Request, res: Response) {
// set the user cookie
res.cookie('user', req.user, {
maxAge: SESSION_LENGTH,
});
// redirect browser to the page they came from
const returnTo = req.session.returnTo ?? '/';
delete req.session.returnTo;
res.redirect(returnTo!);
}

/**
* Initiate authentication with Google
*/
router.get('/google', function (req, res) {
req.session.returnTo = req.headers.referer;
passport.authenticate('google', {
scope: ['https://www.googleapis.com/auth/userinfo.profile', 'https://www.googleapis.com/auth/userinfo.email'],
state: req.headers.host,
})(req, res);
});

/**
* Callback for Google authentication
*/
router.get('/google/callback', function (req, res) {
const returnTo = req.session.returnTo;
const host: string = req.query.state as string;
// all staging auths will redirect their callback to prod since all callback URLs must be registered
// with google cloud for security reasons and it isn't feasible to register the callback URLs for all
// staging instances
// if we are not on a staging instance (on prod or local) but original host is a staging instance, redirect back to host
if (host.startsWith('staging-') && !req.headers.host?.startsWith('staging')) {
// req.url doesn't include /api/users part, only /auth/google/callback? and whatever params after that
res.redirect(`https://${host}/api/users${req.url}`);
return;
}
passport.authenticate(
'google',
{ failureRedirect: '/', session: true },
// provides user information to determine whether or not to authenticate
function (err: Error, user: User | false | null) {
if (err) return console.error(err);
if (!user) return console.error('Invalid login data');
// manually login
req.login(user, function (err) {
if (err) return console.error(err);
// check if user is an admin
const allowedUsers = JSON.parse(process.env.ADMIN_EMAILS ?? '[]');
if (allowedUsers.includes(user.email)) {
req.session.passport!.isAdmin = true;
}
req.session.returnTo = returnTo;
successLogin(req, res);
});
},
)(req, res);
});

/**
* Endpoint to logout
*/
router.get('/logout', function (req, res) {
req.session.destroy(function (err) {
if (err) console.error(err);
// clear the user cookie
res.clearCookie('user');
res.redirect('back');
});
});

export default router;
112 changes: 57 additions & 55 deletions api/src/controllers/courses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,71 @@
@module CoursesRoute
*/

import express, { Request } from 'express';
import { z } from 'zod';
import { getCourseQuery } from '../helpers/gql';
const router = express.Router();
import { publicProcedure, router } from '../helpers/trpc';
import { CourseAAPIResponse, CourseBatchAAPIResponse, GradesRaw } from '@peterportal/types';

/**
* PPAPI proxy for course data
*/
router.get('/api', (req: Request<never, unknown, never, { courseID: string }, never>, res) => {
const r = fetch(process.env.PUBLIC_API_URL + 'courses/' + encodeURIComponent(req.query.courseID), {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
});
console.log(req.query.courseID);

r.then((response) => response.json()).then((data) => res.send(data.payload));
});

/**
* PPAPI proxy for course data
*/
router.post('/api/batch', (req: Request<never, unknown, { courses: string[] }, never>, res) => {
if (req.body.courses.length == 0) {
res.json({});
} else {
const r = fetch(process.env.PUBLIC_API_GRAPHQL_URL, {
method: 'POST',
const coursesRouter = router({
/**
* PPAPI proxy for getting course data
*/
get: publicProcedure.input(z.object({ courseID: z.string() })).query(async ({ input }) => {
const r = fetch(process.env.PUBLIC_API_URL + 'courses/' + encodeURIComponent(input.courseID), {
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({
query: getCourseQuery(req.body.courses),
}),
});

r.then((response) => response.json()).then((data) =>
res.json(
Object.fromEntries(
Object.values(data.data)
.filter((x) => x !== null)
.map((x) => [(x as { id: string }).id, x]),
),
),
);
}
});
return r.then((response) => response.json()).then((data) => data.payload as CourseAAPIResponse);
}),

/**
* PPAPI proxy for grade distribution
*/
router.get('/api/grades', (req: Request<never, unknown, never, { department: string; number: string }>, res) => {
const r = fetch(
process.env.PUBLIC_API_URL +
'grades/raw?department=' +
encodeURIComponent(req.query.department) +
'&courseNumber=' +
req.query.number,
);
/**
* PPAPI proxy for batch course data
*/
batch: publicProcedure.input(z.object({ courses: z.string().array() })).mutation(async ({ input }) => {
if (input.courses.length == 0) {
return {};
} else {
const r = fetch(process.env.PUBLIC_API_GRAPHQL_URL!, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: getCourseQuery(input.courses),
}),
});

// change keys from _0,...,_x to course IDs
return r
.then((response) => response.json())
.then(
(data: CourseBatchAAPIResponse) =>
Object.fromEntries(
(Object.values(data.data) as CourseAAPIResponse[])
.filter((course) => course !== null)
.map((course) => [course.id, course]),
) as CourseBatchAAPIResponse,
);
}
}),

/**
* PPAPI proxy for grade distribution
*/
grades: publicProcedure.input(z.object({ department: z.string(), number: z.string() })).query(async ({ input }) => {
const r = fetch(
process.env.PUBLIC_API_URL +
'grades/raw?department=' +
encodeURIComponent(input.department) +
'&courseNumber=' +
input.number,
);

r.then((response) => response.json()).then((data) => {
res.send(data.payload);
});
return r.then((response) => response.json()).then((data) => data.payload as GradesRaw);
}),
});

export default router;
export default coursesRouter;
Loading
Loading