Skip to content

Commit 02571aa

Browse files
Merge pull request #6 from cuappdev/aayush/auth
Adding JWT auth
2 parents 087de54 + ed51755 commit 02571aa

13 files changed

Lines changed: 170 additions & 30 deletions

File tree

.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ NODE_ENV=development
1111
CACHE_REFRESH_HEADER=
1212
CACHE_REFRESH_SECRET=
1313

14+
# Authentication Configuration
15+
ACCESS_TOKEN_SECRET=
16+
1417
# Database Configuration
1518
DATABASE_URL=
1619

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"express-rate-limit": "^8.1.0",
3333
"firebase-admin": "^13.5.0",
3434
"helmet": "^8.1.0",
35+
"jsonwebtoken": "^9.0.2",
3536
"node-cache": "^5.1.2",
3637
"node-cron": "^4.2.1",
3738
"zod": "^4.1.12"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
Warnings:
3+
4+
- You are about to drop the column `deviceId` on the `User` table. All the data in the column will be lost.
5+
- A unique constraint covering the columns `[deviceUuid]` on the table `User` will be added. If there are existing duplicate values, this will fail.
6+
- A unique constraint covering the columns `[refreshToken]` on the table `User` will be added. If there are existing duplicate values, this will fail.
7+
- Added the required column `deviceUuid` to the `User` table without a default value. This is not possible if the table is not empty.
8+
- Added the required column `refreshToken` to the `User` table without a default value. This is not possible if the table is not empty.
9+
10+
*/
11+
-- DropIndex
12+
DROP INDEX "User_deviceId_idx";
13+
14+
-- DropIndex
15+
DROP INDEX "User_deviceId_key";
16+
17+
-- AlterTable
18+
ALTER TABLE "User" DROP COLUMN "deviceId",
19+
ADD COLUMN "deviceUuid" TEXT NOT NULL,
20+
ADD COLUMN "refreshToken" TEXT NOT NULL;
21+
22+
-- CreateIndex
23+
CREATE UNIQUE INDEX "User_deviceUuid_key" ON "User"("deviceUuid");
24+
25+
-- CreateIndex
26+
CREATE UNIQUE INDEX "User_refreshToken_key" ON "User"("refreshToken");

prisma/schema.prisma

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,16 +49,16 @@ enum EventType {
4949
}
5050

5151
model User {
52-
id Int @id @default(autoincrement())
53-
deviceId String @unique
52+
id Int @id @default(autoincrement())
53+
deviceUuid String @unique
54+
refreshToken String @unique
5455
5556
fcmTokens FCMToken[]
5657
reports Report[]
5758
favoritedEateries FavoritedEatery[]
5859
favoritedItemNames String[]
5960
userEventVotes UserEventVote[]
6061
61-
@@index([deviceId])
6262
@@index(favoritedItemNames, type: Gin) // The performance magic
6363
}
6464

src/auth/auth.schema.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import { z } from 'zod';
22

3-
export const authorizeDeviceIdSchema = z.object({
3+
export const verifyDeviceUuidSchema = z.object({
44
body: z.object({
5-
deviceId: z.string().nonempty('Device ID is required'),
5+
deviceUuid: z.string().nonempty('Device UUID is required'),
6+
}),
7+
});
8+
9+
export const refreshAccessTokenSchema = z.object({
10+
body: z.object({
11+
refreshToken: z.string().nonempty('Refresh token is required'),
612
}),
713
});

src/auth/authController.ts

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,63 @@
1+
import crypto from 'crypto';
2+
import jwt from 'jsonwebtoken';
3+
14
import type { Request, Response } from 'express';
25

36
import { prisma } from '../prisma.js';
7+
import { ForbiddenError } from '../utils/AppError.js';
48

5-
export const authorizeDeviceId = async (req: Request, res: Response) => {
6-
const { deviceId } = req.body;
9+
export const verifyDeviceUuid = async (req: Request, res: Response) => {
10+
const { deviceUuid } = req.body;
11+
const refreshToken = crypto.randomBytes(64).toString('hex');
712

813
const user = await prisma.user.upsert({
9-
where: { deviceId },
10-
update: {},
11-
create: {
12-
deviceId,
14+
where: { deviceUuid },
15+
// If the user exists, update the refreshToken
16+
update: {
17+
refreshToken,
1318
},
14-
select: {
15-
id: true,
16-
deviceId: true,
17-
favoritedEateries: {
18-
select: {
19-
eateryId: true,
20-
},
21-
},
22-
favoritedItemNames: true,
19+
// If the user does not exist, create a new user
20+
create: {
21+
deviceUuid,
22+
refreshToken,
2323
},
2424
});
2525

26+
const accessToken = jwt.sign(
27+
{ userId: user.id },
28+
process.env.ACCESS_TOKEN_SECRET!,
29+
{ expiresIn: '15m' },
30+
);
31+
32+
return res.json({ accessToken, refreshToken });
33+
};
34+
35+
export const refreshAccessToken = async (req: Request, res: Response) => {
36+
const { refreshToken } = req.body;
37+
38+
const user = await prisma.user.findFirst({
39+
where: { refreshToken },
40+
});
41+
42+
if (!user) {
43+
throw new ForbiddenError('Invalid refresh token');
44+
}
45+
46+
const newAccessToken = jwt.sign(
47+
{ userId: user.id },
48+
process.env.ACCESS_TOKEN_SECRET!,
49+
{ expiresIn: '15m' },
50+
);
51+
52+
const newRefreshToken = crypto.randomBytes(64).toString('hex');
53+
54+
await prisma.user.update({
55+
where: { id: user.id },
56+
data: { refreshToken: newRefreshToken },
57+
});
58+
2659
return res.json({
27-
...user,
28-
favoritedEateries: user.favoritedEateries.map((fe) => fe.eateryId),
60+
accessToken: newAccessToken,
61+
refreshToken: newRefreshToken,
2962
});
3063
};

src/auth/authRouter.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { Router } from 'express';
22

33
import { validateRequest } from '../middleware/validateRequest.js';
4-
import { authorizeDeviceIdSchema } from './auth.schema.js';
5-
import { authorizeDeviceId } from './authController.js';
4+
import {
5+
refreshAccessTokenSchema,
6+
verifyDeviceUuidSchema,
7+
} from './auth.schema.js';
8+
import { refreshAccessToken, verifyDeviceUuid } from './authController.js';
69

710
const router = Router();
811

912
router.post(
10-
'/authorize',
11-
validateRequest(authorizeDeviceIdSchema),
12-
authorizeDeviceId,
13+
'/verify-token',
14+
validateRequest(verifyDeviceUuidSchema),
15+
verifyDeviceUuid,
16+
);
17+
router.post(
18+
'/refresh-token',
19+
validateRequest(refreshAccessTokenSchema),
20+
refreshAccessToken,
1321
);
1422

1523
export default router;

src/middleware/authentication.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1+
import jwt from 'jsonwebtoken';
2+
13
import type { NextFunction, Request, Response } from 'express';
24

5+
import type { AuthJwtPayload } from '../types/express/index.js';
36
import { UnauthorizedError } from '../utils/AppError.js';
47

58
export const requireAuth = (
69
req: Request,
710
_res: Response,
811
next: NextFunction,
912
) => {
13+
// Development bypass
14+
if (process.env.NODE_ENV === 'development') {
15+
req.user = {
16+
userId: 1,
17+
};
18+
return next();
19+
}
20+
1021
const authHeader = req.headers.authorization;
1122
if (!authHeader || !authHeader.startsWith('Bearer ')) {
1223
throw new UnauthorizedError('No token provided or wrong format.');
@@ -17,8 +28,12 @@ export const requireAuth = (
1728
throw new UnauthorizedError('No token provided.');
1829
}
1930

20-
// TODO: Finish body when GET authentication is finalized
21-
// Override Express Request type to include session info in the request object
31+
jwt.verify(token, process.env.ACCESS_TOKEN_SECRET!, (err, decodedToken) => {
32+
if (err || !decodedToken || typeof decodedToken === 'string') {
33+
throw new UnauthorizedError('Invalid token.');
34+
}
35+
req.user = decodedToken as AuthJwtPayload;
36+
});
2237

2338
return next();
2439
};

src/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import express from 'express';
55
import type { Request, Response } from 'express';
66

77
import authRouter from './auth/authRouter.js';
8+
import { eateryRouter } from './eateries/eateryRouter.js';
89
import { requireAuth } from './middleware/authentication.js';
910
import { globalErrorHandler } from './middleware/errorHandler.js';
1011
import { requestLogger } from './middleware/logger.js';
1112
import { ipRateLimiter } from './middleware/rateLimit.js';
1213
import { prisma } from './prisma.js';
14+
import userRouter from './users/userRouter.js';
1315
import { refreshCacheFromDB } from './utils/cache.js';
1416

1517
const app = express();
@@ -48,9 +50,11 @@ router.get('/health', async (_: Request, res: Response) => {
4850

4951
// Public routes
5052
router.use('/auth', authRouter);
53+
router.use('/eateries', eateryRouter);
5154

52-
// Protected routes (require GET authentication)
55+
// Protected routes
5356
router.use(requireAuth);
57+
router.use('/users', userRouter);
5458

5559
app.use(router);
5660

0 commit comments

Comments
 (0)