diff --git a/api/Diagram 2023-01-24 09-53-59.uxf b/api/Diagram 2023-01-24 09-53-59.uxf index 667104a..f49c4be 100644 --- a/api/Diagram 2023-01-24 09-53-59.uxf +++ b/api/Diagram 2023-01-24 09-53-59.uxf @@ -1,4 +1,4 @@ -10UMLClass17010210140USER +10UMLClass27960210140USER -- _id: INT_ email: VARCHAR @@ -6,8 +6,8 @@ password: VARCHAR name: VARCHAR picture_path: VARCHAR created_at: DATE -updated_at: DATERelation26014050140m1=1 -m2=0..*10;10;10;120UMLClass170260210170SNIPPET +updated_at: DATERelation36919050140m1=1 +m2=0..*10;10;10;120UMLClass279310210170SNIPPET -- _id: INT_ title: VARCHAR @@ -17,20 +17,22 @@ updated_at: DATE _user_id: INT_ _tag_id: INT_ _category_id: INT_ -_language_id: INT_UMLClass51047010060TAG +_language_id: INT_UMLClass61952010060TAG -- _id: INT_ -name: VARCHARUMLClass17054011060CATEGORY +name: VARCHARUMLClass7933011080CATEGORY -- _id: INT_ -name: VARCHARRelation22042050140m1=1 -m2=0..*10;120;10;10UMLClass51030010060SNIPPET_TAG +name: VARCHAR +_user_id: INT_Relation17935012040m1=1 +m2=0..*10;10;100;10UMLClass61935010060SNIPPET_TAG -- _snippet_id: INT_ -_tag_id: INT_Relation37032016040m1=1 -m2=0..*10;10;140;10Relation55035050140m1=1 -m2=0..*10;120;10;10UMLClass34054010060LANGUAGE +_tag_id: INT_Relation47937016040m1=1 +m2=0..*10;10;140;10Relation65940050140m1=1 +m2=0..*10;120;10;10UMLClass33958010060LANGUAGE -- _id: INT_ -name: VARCHARRelation320420100140m1=0..* -m2=110;10;10;70;70;70;70;120 \ No newline at end of file +name: VARCHARRelation37947050130m1=0..* +m2=110;10;10;110Relation119120180230m1=1 +m2=0..*160;10;10;10;10;210 \ No newline at end of file diff --git a/api/Diagramme relation entité.png b/api/Diagramme relation entité.png index 0f8a553..47634c8 100644 Binary files a/api/Diagramme relation entité.png and b/api/Diagramme relation entité.png differ diff --git a/prisma/migrations/20230127144342_create_and_update_defaults/migration.sql b/prisma/migrations/20230127144342_create_and_update_defaults/migration.sql new file mode 100644 index 0000000..801c0a1 --- /dev/null +++ b/prisma/migrations/20230127144342_create_and_update_defaults/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE `Snippet` MODIFY `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); + +-- AlterTable +ALTER TABLE `User` MODIFY `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a9e3d37..2efd6ce 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,8 +13,8 @@ model User { password String name String picture_path String - created_at DateTime - updated_at DateTime + created_at DateTime @default(now()) + updated_at DateTime @updatedAt Snippet Snippet[] Category Category[] } @@ -23,8 +23,8 @@ model Snippet { id Int @id @default(autoincrement()) title String code String - created_at DateTime - updated_at DateTime + created_at DateTime @default(now()) + updated_at DateTime @updatedAt user_id Int user User @relation(fields: [user_id], references: [id]) diff --git a/src/__tests__/app.test.ts b/src/__tests__/app.test.ts index 88b2b01..75c1169 100644 --- a/src/__tests__/app.test.ts +++ b/src/__tests__/app.test.ts @@ -1,7 +1,9 @@ import { initServer } from '../app.js' import request from 'supertest' +import { User } from '.prisma/client' const app = initServer() +let jwtCookie: string | undefined describe('GET /v1', () => { it('returns status code 200 and api version message', async () => { @@ -13,10 +15,10 @@ describe('GET /v1', () => { }) }) -describe('POST /v1/login', () => { +describe('POST /v1/session/login', () => { it('returns status code 200 and set httpOnly jwt token cookie', async () => { const res = await request(app) - .post('/v1/login') + .post('/v1/session/login') .set('Content-Type', 'application/json') .send(JSON.stringify({ email: "a@a.com", @@ -24,10 +26,39 @@ describe('POST /v1/login', () => { })) const jwtRegEx = /^jwt=.*Path=\/.*HttpOnly.*Secure.*SameSite=Strict.*$/ - const jwtToken = res.get('Set-Cookie') - .filter(cookie => cookie.match(jwtRegEx)) + jwtCookie = res.get('Set-Cookie') + ?.filter(cookie => cookie.match(jwtRegEx))[0] + + // jwtToken = jwtCookie.match('(^|;)\\s*jwt\\s*=\\s*([^;]+)')?.pop() || '' expect(res.statusCode).toEqual(200) - expect(jwtToken).not.toBe(undefined) + expect(jwtCookie).not.toBe(undefined) }) }) + + +describe('PUT /v1/user', () => { + it('returns status code 200', async () => { + const res = await request(app) + .put('/v1/user') + .set('Content-Type', 'application/json') + .set('Cookie', jwtCookie as string) + .send(JSON.stringify({ + name: 'didier' + })) + + expect(res.statusCode).toEqual(200) + }) + + it('returns status code 400 and an error message', async () => { + const res = await request(app) + .put('/v1/user') + .set('Content-Type', 'application/json') + .set('Cookie', 'jwt=23897yr0287yf.12423fv.23f4325') + .send(JSON.stringify({ + name: 'didier' + })) + + expect(res.statusCode).toEqual(400) + }) +}) \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 9685603..3bf5633 100644 --- a/src/app.ts +++ b/src/app.ts @@ -5,6 +5,7 @@ import jwt from 'jsonwebtoken' import cors from 'cors' import cookieParser from 'cookie-parser' import { getJwtSecret } from './utils/jwt.js' +import sessionRouter from './routers/session.js' const authMiddleware: RequestHandler = (req, res, next) => { const token = req.cookies.jwt @@ -54,7 +55,9 @@ export function initServer(): express.Express { appRouter.get('/', appRouterGet) - appRouter.use(userRouter) + appRouter.use('/session/', sessionRouter) + appRouter.use('/user/', authMiddleware, userRouter) + app.use('/v1/', appRouter) return app diff --git a/src/routers/session.ts b/src/routers/session.ts new file mode 100644 index 0000000..7e00465 --- /dev/null +++ b/src/routers/session.ts @@ -0,0 +1,105 @@ +import { PrismaClient, User } from "@prisma/client" +import express from "express" +import { RequestHandler } from "express-serve-static-core" +import { z } from "zod" +import { getJwtSecret } from "../utils/jwt.js" +import bcrypt from 'bcrypt' +import jwt from 'jsonwebtoken' + +interface LoginData { + email: string, + password: string +} + +const sessionRouter = express.Router() +const prisma = new PrismaClient() + +const LoginValidator = z.object({ + email: z.string().email(), + password: z.string().min(4).max(20) +}) + +async function isUserValid(user: User | null, loginData: any): Promise { + if (user == undefined) return false + return await bcrypt.compare(loginData.password, user.password) +} + +async function isUserEmailAlreadyUsed(email: string): Promise { + const user = await prisma.user.findFirst({ where: { email } }) + return user != undefined +} + +async function createUser(email: string, password: string) { + await prisma.user.create({ + data: { + email: email, + password: password, + name: '', + picture_path: '', + created_at: new Date(), + updated_at: new Date() + }, + }) +} + +async function getUser(email: string): Promise { + return await prisma.user.findFirst({ + where: { + email: email + } + }) +} + +function parseLoginData(data: any): LoginData | undefined { + try { + const loginData: LoginData = LoginValidator.parse(data) + return loginData + } catch { + return undefined + } +} + +export const userRouterPostLogin: RequestHandler = async (req, res) => { + const loginData = parseLoginData(req.body) + if (loginData === undefined) { + res.status(400).json({ message: 'Incorrect credentials format.' }) + return; + } + + const user = await getUser(loginData.email) + if (await isUserValid(user, loginData) === false) { + res.status(400).json({ message: 'Incorrect credentials.' }) + return; + } + + const jwtToken = jwt.sign({ userId: user?.id }, getJwtSecret(), { expiresIn: "1h" }) + res.cookie('jwt', jwtToken, { + httpOnly: true, + secure: true, + sameSite: 'strict' + }).json({ message: 'Logged in! httpOnly cookie set.' }) +} + +export const userRouterPostRegister: RequestHandler = async (req, res) => { + const loginData = parseLoginData(req.body) + if (loginData == undefined) { + res.status(400).json({ message: 'Incorrect credentials format.' }) + return; + } + + if (await isUserEmailAlreadyUsed(loginData.email)) { + res.status(400).json({ message: 'Email already linked to an account.' }) + return; + } + + const hashedPassword = await bcrypt.hash(loginData.password, 10) + + createUser(loginData.email, hashedPassword) + + res.json({ message: 'User successfully created!' }) +} + +sessionRouter.post('/login', userRouterPostLogin) +sessionRouter.post('/register', userRouterPostRegister) + +export default sessionRouter \ No newline at end of file diff --git a/src/routers/user.ts b/src/routers/user.ts index e6c9fb6..f571cd4 100644 --- a/src/routers/user.ts +++ b/src/routers/user.ts @@ -1,105 +1,44 @@ import { PrismaClient, User } from '@prisma/client' import express, { RequestHandler } from 'express' - -import { z } from 'zod' -import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' -import { getJwtSecret } from '../utils/jwt.js' - -interface LoginData { - email: string, - password: string -} const userRouter = express.Router() const prisma = new PrismaClient() -const LoginValidator = z.object({ - email: z.string().email(), - password: z.string().min(4).max(20) -}) - -async function isUserValid(user: User | null, loginData: any): Promise { - if (user == undefined) return false - return await bcrypt.compare(loginData.password, user.password) +function parseJwtUserId(jwtoken: string): number | undefined { + const payload = jwt.decode(jwtoken) + if (payload == undefined) { + return undefined + } else if (typeof payload == 'string') { + return undefined + } + return payload.userId } -async function isUserEmailAlreadyUsed(email: string): Promise { - const user = await prisma.user.findFirst({ where: { email } }) - return user != undefined -} - -async function createUser(email: string, password: string) { - await prisma.user.create({ +async function updateUser(userId: number): Promise { + return await prisma.user.update({ + where: { id: userId }, data: { - email: email, - password: password, - name: '', - picture_path: '', - created_at: new Date(), - updated_at: new Date() - }, - }) -} - -async function getUser(email: string): Promise { - return await prisma.user.findFirst({ - where: { - email: email } }) } -function parseLoginData(data: any): LoginData | undefined { - try { - const loginData: LoginData = LoginValidator.parse(data) - return loginData - } catch { - return undefined +const userRouterPut: RequestHandler = async (req, res) => { + const userId = parseJwtUserId(req.cookies.jwt) + if (userId === undefined) { + res.status(400).json({ + message: 'Incorrect JWT payload.' + }) + return; } + + await updateUser(userId) + + res.json({ + message: 'User successfully updated.' + }) } -const userRouterPostLogin: RequestHandler = async (req, res) => { - const loginData = parseLoginData(req.body) - if (loginData === undefined) { - res.status(400).json({ message: 'Incorrect credentials format.' }) - return; - } - - const user = await getUser(loginData.email) - if (await isUserValid(user, loginData) === false) { - res.status(400).json({ message: 'Incorrect credentials.' }) - return; - } - - const jwtToken = jwt.sign({}, getJwtSecret(), { expiresIn: "1h" }) - res.cookie('jwt', jwtToken, { - httpOnly: true, - secure: true, - sameSite: 'strict' - }).json({ message: "Logged in! httpOnly cookie set." }) -} - -const userRouterPostRegister: RequestHandler = async (req, res) => { - const loginData = parseLoginData(req.body) - if (loginData == undefined) { - res.status(400).json({ message: 'Incorrect credentials format.' }) - return; - } - - if (await isUserEmailAlreadyUsed(loginData.email)) { - res.status(400).json({ message: 'Email already linked to an account.' }) - return; - } - - const hashedPassword = await bcrypt.hash(loginData.password, 10) - - createUser(loginData.email, hashedPassword) - - res.json({ message: 'User successfully created!' }) -} - -userRouter.post("/login", userRouterPostLogin) -userRouter.post("/register", userRouterPostRegister) +userRouter.put("/", userRouterPut) export default userRouter