mirror of
https://github.com/Floriansylvain/SnippetsManager.git
synced 2026-08-19 11:43:15 +02:00
DB improv., sessions/users handlers improv.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
<diagram program="umletino" version="15.0.0"><zoom_level>10</zoom_level><element><id>UMLClass</id><coordinates><x>170</x><y>10</y><w>210</w><h>140</h></coordinates><panel_attributes>USER
|
||||
<diagram program="umletino" version="15.0.0"><zoom_level>10</zoom_level><element><id>UMLClass</id><coordinates><x>279</x><y>60</y><w>210</w><h>140</h></coordinates><panel_attributes>USER
|
||||
--
|
||||
_id: INT_
|
||||
email: VARCHAR
|
||||
@@ -6,8 +6,8 @@ password: VARCHAR
|
||||
name: VARCHAR
|
||||
picture_path: VARCHAR
|
||||
created_at: DATE
|
||||
updated_at: DATE</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>260</x><y>140</y><w>50</w><h>140</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;10;10;120</additional_attributes></element><element><id>UMLClass</id><coordinates><x>170</x><y>260</y><w>210</w><h>170</h></coordinates><panel_attributes>SNIPPET
|
||||
updated_at: DATE</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>369</x><y>190</y><w>50</w><h>140</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;10;10;120</additional_attributes></element><element><id>UMLClass</id><coordinates><x>279</x><y>310</y><w>210</w><h>170</h></coordinates><panel_attributes>SNIPPET
|
||||
--
|
||||
_id: INT_
|
||||
title: VARCHAR
|
||||
@@ -17,20 +17,22 @@ updated_at: DATE
|
||||
_user_id: INT_
|
||||
_tag_id: INT_
|
||||
_category_id: INT_
|
||||
_language_id: INT_</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>510</x><y>470</y><w>100</w><h>60</h></coordinates><panel_attributes>TAG
|
||||
_language_id: INT_</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>619</x><y>520</y><w>100</w><h>60</h></coordinates><panel_attributes>TAG
|
||||
--
|
||||
_id: INT_
|
||||
name: VARCHAR</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>170</x><y>540</y><w>110</w><h>60</h></coordinates><panel_attributes>CATEGORY
|
||||
name: VARCHAR</panel_attributes><additional_attributes></additional_attributes></element><element><id>UMLClass</id><coordinates><x>79</x><y>330</y><w>110</w><h>80</h></coordinates><panel_attributes>CATEGORY
|
||||
--
|
||||
_id: INT_
|
||||
name: VARCHAR</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>220</x><y>420</y><w>50</w><h>140</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;120;10;10</additional_attributes></element><element><id>UMLClass</id><coordinates><x>510</x><y>300</y><w>100</w><h>60</h></coordinates><panel_attributes>SNIPPET_TAG
|
||||
name: VARCHAR
|
||||
_user_id: INT_</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>179</x><y>350</y><w>120</w><h>40</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;10;100;10</additional_attributes></element><element><id>UMLClass</id><coordinates><x>619</x><y>350</y><w>100</w><h>60</h></coordinates><panel_attributes>SNIPPET_TAG
|
||||
--
|
||||
_snippet_id: INT_
|
||||
_tag_id: INT_</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>370</x><y>320</y><w>160</w><h>40</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;10;140;10</additional_attributes></element><element><id>Relation</id><coordinates><x>550</x><y>350</y><w>50</w><h>140</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;120;10;10</additional_attributes></element><element><id>UMLClass</id><coordinates><x>340</x><y>540</y><w>100</w><h>60</h></coordinates><panel_attributes>LANGUAGE
|
||||
_tag_id: INT_</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>479</x><y>370</y><w>160</w><h>40</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;10;140;10</additional_attributes></element><element><id>Relation</id><coordinates><x>659</x><y>400</y><w>50</w><h>140</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>10;120;10;10</additional_attributes></element><element><id>UMLClass</id><coordinates><x>339</x><y>580</y><w>100</w><h>60</h></coordinates><panel_attributes>LANGUAGE
|
||||
--
|
||||
_id: INT_
|
||||
name: VARCHAR</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>320</x><y>420</y><w>100</w><h>140</h></coordinates><panel_attributes>m1=0..*
|
||||
m2=1</panel_attributes><additional_attributes>10;10;10;70;70;70;70;120</additional_attributes></element></diagram>
|
||||
name: VARCHAR</panel_attributes><additional_attributes></additional_attributes></element><element><id>Relation</id><coordinates><x>379</x><y>470</y><w>50</w><h>130</h></coordinates><panel_attributes>m1=0..*
|
||||
m2=1</panel_attributes><additional_attributes>10;10;10;110</additional_attributes></element><element><id>Relation</id><coordinates><x>119</x><y>120</y><w>180</w><h>230</h></coordinates><panel_attributes>m1=1
|
||||
m2=0..*</panel_attributes><additional_attributes>160;10;10;10;10;210</additional_attributes></element></diagram>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 279 KiB After Width: | Height: | Size: 347 KiB |
@@ -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);
|
||||
@@ -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])
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
+4
-1
@@ -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
|
||||
|
||||
@@ -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<boolean> {
|
||||
if (user == undefined) return false
|
||||
return await bcrypt.compare(loginData.password, user.password)
|
||||
}
|
||||
|
||||
async function isUserEmailAlreadyUsed(email: string): Promise<boolean> {
|
||||
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<User | null> {
|
||||
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
|
||||
+22
-83
@@ -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<boolean> {
|
||||
if (user == undefined) return false
|
||||
return await bcrypt.compare(loginData.password, user.password)
|
||||
}
|
||||
|
||||
async function isUserEmailAlreadyUsed(email: string): Promise<boolean> {
|
||||
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<User | null> {
|
||||
return await prisma.user.findFirst({
|
||||
where: {
|
||||
email: email
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function parseLoginData(data: any): LoginData | undefined {
|
||||
try {
|
||||
const loginData: LoginData = LoginValidator.parse(data)
|
||||
return loginData
|
||||
} catch {
|
||||
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
|
||||
}
|
||||
|
||||
const userRouterPostLogin: RequestHandler = async (req, res) => {
|
||||
const loginData = parseLoginData(req.body)
|
||||
if (loginData === undefined) {
|
||||
res.status(400).json({ message: 'Incorrect credentials format.' })
|
||||
return;
|
||||
async function updateUser(userId: number): Promise<User> {
|
||||
return await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
}
|
||||
|
||||
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.' })
|
||||
const userRouterPut: RequestHandler = async (req, res) => {
|
||||
const userId = parseJwtUserId(req.cookies.jwt)
|
||||
if (userId === undefined) {
|
||||
res.status(400).json({
|
||||
message: 'Incorrect JWT payload.'
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
||||
if (await isUserEmailAlreadyUsed(loginData.email)) {
|
||||
res.status(400).json({ message: 'Email already linked to an account.' })
|
||||
return;
|
||||
}
|
||||
await updateUser(userId)
|
||||
|
||||
const hashedPassword = await bcrypt.hash(loginData.password, 10)
|
||||
|
||||
createUser(loginData.email, hashedPassword)
|
||||
|
||||
res.json({ message: 'User successfully created!' })
|
||||
res.json({
|
||||
message: 'User successfully updated.'
|
||||
})
|
||||
}
|
||||
|
||||
userRouter.post("/login", userRouterPostLogin)
|
||||
userRouter.post("/register", userRouterPostRegister)
|
||||
userRouter.put("/", userRouterPut)
|
||||
|
||||
export default userRouter
|
||||
|
||||
Reference in New Issue
Block a user