From 423548804a5a3ea5883b80bd60bebb553da40566 Mon Sep 17 00:00:00 2001 From: Justin Lin Date: Wed, 16 Apr 2025 16:40:03 +0800 Subject: [PATCH] feat: migreate get user profile to trpc --- app/src/store/userProfile.ts | 4 +-- app/src/utils/api.ts | 6 ----- packages/models/src/index.ts | 2 +- server/src/api/controllers/userController.ts | 20 --------------- server/src/api/routes/index.ts | 19 -------------- server/src/api/routes/userRouter.ts | 9 ------- server/src/api/validators/questValidator.ts | 7 ++++++ server/src/api/validators/sessionValidator.ts | 7 +++--- server/src/routers/index.ts | 2 ++ server/src/routers/userRouter.ts | 25 +++++++++++++++++++ server/src/trpc.ts | 15 +++++++++-- 11 files changed, 54 insertions(+), 62 deletions(-) delete mode 100644 server/src/api/controllers/userController.ts delete mode 100644 server/src/api/routes/userRouter.ts create mode 100644 server/src/api/validators/questValidator.ts create mode 100644 server/src/routers/userRouter.ts diff --git a/app/src/store/userProfile.ts b/app/src/store/userProfile.ts index 2a7feaf..8a6f172 100644 --- a/app/src/store/userProfile.ts +++ b/app/src/store/userProfile.ts @@ -1,5 +1,5 @@ import { defineStore } from 'pinia' -import { getUserProfile } from '../utils/api' +import { trpc } from '../utils/trpc' interface IProgress { quest: string @@ -23,7 +23,7 @@ const useUserProfile = defineStore('userProfile', { }), actions: { async loadUserProfile() { - const res = await getUserProfile() + const res = await trpc.users.getMyProfile.query() this.$patch(res) }, async resetUserProfile() { diff --git a/app/src/utils/api.ts b/app/src/utils/api.ts index f17d3d9..5c9015f 100644 --- a/app/src/utils/api.ts +++ b/app/src/utils/api.ts @@ -1,7 +1,6 @@ import axios from 'axios' import { GetSessionsRequest } from '@linux-odyssey/constants' import { Session } from '../types' -import { UserProfile } from '../store/userProfile' const api = axios.create({ baseURL: '/api/v1', @@ -12,11 +11,6 @@ const api = axios.create({ }) export default api -export async function getUserProfile(): Promise { - const res = await api.get('/users/me') - return res.data -} - export async function createSession(questId: string): Promise { const res = await api.post('/sessions', { questId }) return res.data diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 59332a0..014f076 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -4,7 +4,7 @@ export { User, IUser } from './user.js' export { Quest, IQuest } from './quest.js' export { Session, ISession, ITask } from './session.js' export { Command, ICommand } from './command.js' -export { UserProfile, IUserProfile } from './userProfile.js' +export { UserProfile, IUserProfile, IProgress } from './userProfile.js' export { IFileCondition, ICondition } from './condition.js' export { IStage } from './stage.js' export { IRequirement } from './requirement.js' diff --git a/server/src/api/controllers/userController.ts b/server/src/api/controllers/userController.ts deleted file mode 100644 index 220cf2c..0000000 --- a/server/src/api/controllers/userController.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Express, Request, Response } from 'express' -import { UserProfile } from '@linux-odyssey/models' -import { asyncHandler } from '../../middleware/error.js' - -export const me = asyncHandler(async (req: Request, res: Response) => { - const { id, username, email } = req.user as Express.ExistingUser - - const userProfile = await UserProfile.findOne({ - user: id, - }) - if (!userProfile) { - throw new Error(`UserProfile not found for user ${username} ${id}!`) - } - const { progress } = userProfile - res.json({ - username, - email, - progress, - }) -}) diff --git a/server/src/api/routes/index.ts b/server/src/api/routes/index.ts index fb3b318..a9aeba6 100644 --- a/server/src/api/routes/index.ts +++ b/server/src/api/routes/index.ts @@ -4,7 +4,6 @@ import sessions from './sessionRouter.js' import commands from './commandRouter.js' import authRouter from './authRouter.js' import authRequired from '../../middleware/authRequired.js' -import userRouter from './userRouter.js' import config from '../../config.js' import leaderboardController from '../controllers/leaderboardController.js' @@ -28,24 +27,6 @@ router.use( router.use('/auth', authRouter) router.use('/sessions', authRequired, sessions) -router.use('/users', userRouter) - -router.get('/survey', (req, res) => { - if (!config.surveyUrl) { - res.status(404).send('Survey not configured') - return - } - res.redirect(config.surveyUrl) -}) - -router.get('/bug-report', (req, res) => { - if (!config.bugReportUrl) { - res.status(404).send('Bug report not configured') - return - } - res.redirect(config.bugReportUrl) -}) - router.get('/leaderboard', leaderboardController) export default router diff --git a/server/src/api/routes/userRouter.ts b/server/src/api/routes/userRouter.ts deleted file mode 100644 index 235127f..0000000 --- a/server/src/api/routes/userRouter.ts +++ /dev/null @@ -1,9 +0,0 @@ -import express from 'express' -import { me } from '../controllers/userController.js' -import authRequired from '../../middleware/authRequired.js' - -const router = express.Router() - -router.get('/me', authRequired, me) - -export default router diff --git a/server/src/api/validators/questValidator.ts b/server/src/api/validators/questValidator.ts new file mode 100644 index 0000000..3a2a18e --- /dev/null +++ b/server/src/api/validators/questValidator.ts @@ -0,0 +1,7 @@ +import { check } from 'express-validator' +import { noError } from '../../middleware/validator.js' + +export const checkQuestId = () => + check('questId').notEmpty().isString().isLowercase().isLength({ max: 64 }) + +export const questIdValidator = [checkQuestId(), noError] diff --git a/server/src/api/validators/sessionValidator.ts b/server/src/api/validators/sessionValidator.ts index 509defb..558b5b6 100644 --- a/server/src/api/validators/sessionValidator.ts +++ b/server/src/api/validators/sessionValidator.ts @@ -1,4 +1,5 @@ import { check } from 'express-validator' +import { checkQuestId } from './questValidator.js' import { noError } from '../../middleware/validator.js' const checkSessionStatus = () => @@ -7,7 +8,7 @@ const checkSessionStatus = () => export const checkSessionId = () => check('sessionId').notEmpty().isString().isMongoId() -export const sessionListValidator = [checkSessionStatus()] +export const sessionListValidator = [checkQuestId(), checkSessionStatus()] export const sessionDetailValidator = [checkSessionId()] -export const createSessionValidator = [noError] -export const activeSessionValidator = [noError] +export const createSessionValidator = [checkQuestId(), noError] +export const activeSessionValidator = [checkQuestId(), noError] diff --git a/server/src/routers/index.ts b/server/src/routers/index.ts index 288d2f3..7e18636 100644 --- a/server/src/routers/index.ts +++ b/server/src/routers/index.ts @@ -1,6 +1,7 @@ import { router, publicProcedure } from '../trpc.js' import { linkRouter } from './linkRouter.js' import { questRouter } from './questRouter.js' +import { userRouter } from './userRouter.js' export const appRouter = router({ userList: publicProcedure.query(async (opts) => { @@ -9,6 +10,7 @@ export const appRouter = router({ }), links: linkRouter, quests: questRouter, + users: userRouter, }) export type AppRouter = typeof appRouter diff --git a/server/src/routers/userRouter.ts b/server/src/routers/userRouter.ts new file mode 100644 index 0000000..46c0977 --- /dev/null +++ b/server/src/routers/userRouter.ts @@ -0,0 +1,25 @@ +import { UserProfile } from '@linux-odyssey/models' +import { TRPCError } from '@trpc/server' +import { protectedProcedure, router } from '../trpc.js' + +export const userRouter = router({ + getMyProfile: protectedProcedure.query(async (opts) => { + const { id, username, email } = opts.ctx.user + + const userProfile = await UserProfile.findOne({ + user: id, + }) + if (!userProfile) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: `UserProfile not found for user ${username} ${id}!`, + }) + } + const { progress } = userProfile + return { + username, + email, + progress, + } + }), +}) diff --git a/server/src/trpc.ts b/server/src/trpc.ts index f2d2ecd..8e7c939 100644 --- a/server/src/trpc.ts +++ b/server/src/trpc.ts @@ -1,12 +1,13 @@ -import { initTRPC } from '@trpc/server' +import { initTRPC, TRPCError } from '@trpc/server' import { CreateNextContextOptions } from '@trpc/server/adapters/next' export const createContext = async (opts: CreateNextContextOptions) => { - const { session, user, ip } = opts.req + const { session, user, ip, isAuthenticated } = opts.req return { session, user, ip, + isAuthenticated, } } @@ -18,3 +19,13 @@ const t = initTRPC.context().create() * that can be used throughout the router */ export const { router, procedure: publicProcedure } = t + +export const protectedProcedure = publicProcedure.use(async (opts) => { + if (opts.ctx.isAuthenticated() && !('newUser' in opts.ctx.user)) { + return opts.next() + } + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Login required', + }) +})