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_
|
_id: INT_
|
||||||
email: VARCHAR
|
email: VARCHAR
|
||||||
@@ -6,8 +6,8 @@ password: VARCHAR
|
|||||||
name: VARCHAR
|
name: VARCHAR
|
||||||
picture_path: VARCHAR
|
picture_path: VARCHAR
|
||||||
created_at: DATE
|
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
|
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>170</x><y>260</y><w>210</w><h>170</h></coordinates><panel_attributes>SNIPPET
|
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_
|
_id: INT_
|
||||||
title: VARCHAR
|
title: VARCHAR
|
||||||
@@ -17,20 +17,22 @@ updated_at: DATE
|
|||||||
_user_id: INT_
|
_user_id: INT_
|
||||||
_tag_id: INT_
|
_tag_id: INT_
|
||||||
_category_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_
|
_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_
|
_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
|
name: VARCHAR
|
||||||
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
|
_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_
|
_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
|
_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>550</x><y>350</y><w>50</w><h>140</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>340</x><y>540</y><w>100</w><h>60</h></coordinates><panel_attributes>LANGUAGE
|
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_
|
_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..*
|
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;70;70;70;70;120</additional_attributes></element></diagram>
|
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
|
password String
|
||||||
name String
|
name String
|
||||||
picture_path String
|
picture_path String
|
||||||
created_at DateTime
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime
|
updated_at DateTime @updatedAt
|
||||||
Snippet Snippet[]
|
Snippet Snippet[]
|
||||||
Category Category[]
|
Category Category[]
|
||||||
}
|
}
|
||||||
@@ -23,8 +23,8 @@ model Snippet {
|
|||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
title String
|
||||||
code String
|
code String
|
||||||
created_at DateTime
|
created_at DateTime @default(now())
|
||||||
updated_at DateTime
|
updated_at DateTime @updatedAt
|
||||||
|
|
||||||
user_id Int
|
user_id Int
|
||||||
user User @relation(fields: [user_id], references: [id])
|
user User @relation(fields: [user_id], references: [id])
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { initServer } from '../app.js'
|
import { initServer } from '../app.js'
|
||||||
import request from 'supertest'
|
import request from 'supertest'
|
||||||
|
import { User } from '.prisma/client'
|
||||||
|
|
||||||
const app = initServer()
|
const app = initServer()
|
||||||
|
let jwtCookie: string | undefined
|
||||||
|
|
||||||
describe('GET /v1', () => {
|
describe('GET /v1', () => {
|
||||||
it('returns status code 200 and api version message', async () => {
|
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 () => {
|
it('returns status code 200 and set httpOnly jwt token cookie', async () => {
|
||||||
const res = await request(app)
|
const res = await request(app)
|
||||||
.post('/v1/login')
|
.post('/v1/session/login')
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.send(JSON.stringify({
|
.send(JSON.stringify({
|
||||||
email: "a@a.com",
|
email: "a@a.com",
|
||||||
@@ -24,10 +26,39 @@ describe('POST /v1/login', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const jwtRegEx = /^jwt=.*Path=\/.*HttpOnly.*Secure.*SameSite=Strict.*$/
|
const jwtRegEx = /^jwt=.*Path=\/.*HttpOnly.*Secure.*SameSite=Strict.*$/
|
||||||
const jwtToken = res.get('Set-Cookie')
|
jwtCookie = res.get('Set-Cookie')
|
||||||
.filter(cookie => cookie.match(jwtRegEx))
|
?.filter(cookie => cookie.match(jwtRegEx))[0]
|
||||||
|
|
||||||
|
// jwtToken = jwtCookie.match('(^|;)\\s*jwt\\s*=\\s*([^;]+)')?.pop() || ''
|
||||||
|
|
||||||
expect(res.statusCode).toEqual(200)
|
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 cors from 'cors'
|
||||||
import cookieParser from 'cookie-parser'
|
import cookieParser from 'cookie-parser'
|
||||||
import { getJwtSecret } from './utils/jwt.js'
|
import { getJwtSecret } from './utils/jwt.js'
|
||||||
|
import sessionRouter from './routers/session.js'
|
||||||
|
|
||||||
const authMiddleware: RequestHandler = (req, res, next) => {
|
const authMiddleware: RequestHandler = (req, res, next) => {
|
||||||
const token = req.cookies.jwt
|
const token = req.cookies.jwt
|
||||||
@@ -54,7 +55,9 @@ export function initServer(): express.Express {
|
|||||||
|
|
||||||
appRouter.get('/', appRouterGet)
|
appRouter.get('/', appRouterGet)
|
||||||
|
|
||||||
appRouter.use(userRouter)
|
appRouter.use('/session/', sessionRouter)
|
||||||
|
appRouter.use('/user/', authMiddleware, userRouter)
|
||||||
|
|
||||||
app.use('/v1/', appRouter)
|
app.use('/v1/', appRouter)
|
||||||
|
|
||||||
return app
|
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
|
||||||
+26
-87
@@ -1,105 +1,44 @@
|
|||||||
import { PrismaClient, User } from '@prisma/client'
|
import { PrismaClient, User } from '@prisma/client'
|
||||||
import express, { RequestHandler } from 'express'
|
import express, { RequestHandler } from 'express'
|
||||||
|
|
||||||
import { z } from 'zod'
|
|
||||||
import bcrypt from 'bcrypt'
|
|
||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import { getJwtSecret } from '../utils/jwt.js'
|
|
||||||
|
|
||||||
interface LoginData {
|
|
||||||
email: string,
|
|
||||||
password: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRouter = express.Router()
|
const userRouter = express.Router()
|
||||||
const prisma = new PrismaClient()
|
const prisma = new PrismaClient()
|
||||||
|
|
||||||
const LoginValidator = z.object({
|
function parseJwtUserId(jwtoken: string): number | undefined {
|
||||||
email: z.string().email(),
|
const payload = jwt.decode(jwtoken)
|
||||||
password: z.string().min(4).max(20)
|
if (payload == undefined) {
|
||||||
})
|
return undefined
|
||||||
|
} else if (typeof payload == 'string') {
|
||||||
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
|
return undefined
|
||||||
}
|
}
|
||||||
|
return payload.userId
|
||||||
}
|
}
|
||||||
|
|
||||||
const userRouterPostLogin: RequestHandler = async (req, res) => {
|
async function updateUser(userId: number): Promise<User> {
|
||||||
const loginData = parseLoginData(req.body)
|
return await prisma.user.update({
|
||||||
if (loginData === undefined) {
|
where: { id: userId },
|
||||||
res.status(400).json({ message: 'Incorrect credentials format.' })
|
data: {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRouterPut: RequestHandler = async (req, res) => {
|
||||||
|
const userId = parseJwtUserId(req.cookies.jwt)
|
||||||
|
if (userId === undefined) {
|
||||||
|
res.status(400).json({
|
||||||
|
message: 'Incorrect JWT payload.'
|
||||||
|
})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser(loginData.email)
|
await updateUser(userId)
|
||||||
if (await isUserValid(user, loginData) === false) {
|
|
||||||
res.status(400).json({ message: 'Incorrect credentials.' })
|
res.json({
|
||||||
return;
|
message: 'User successfully updated.'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const jwtToken = jwt.sign({}, getJwtSecret(), { expiresIn: "1h" })
|
userRouter.put("/", userRouterPut)
|
||||||
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)
|
|
||||||
|
|
||||||
export default userRouter
|
export default userRouter
|
||||||
|
|||||||
Reference in New Issue
Block a user