diff --git a/.env.example b/.env.example index e9605bf..7c179e9 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,8 @@ API_PORT= RABBITMQ_URL= RABBITMQ_USER= RABBITMQ_PASSWD= -RABBITMQ_QUEUE= \ No newline at end of file +RABBITMQ_QUEUE= + +DATABASE_URL= + +JWT_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 980e730..b9343b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # idea .idea +volume # Logs logs diff --git a/README.md b/README.md index 5bfae61..1387129 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ The CodeBench main API. ## Requirements - NodeJS v14+ -- RabbitMQ see the `docker-compose.yml` +- RabbitMQ and Postgres see the `docker-compose.yml` - Copy `.env.example` to `.env` and fill it as needed. ## Setup diff --git a/docker-compose.yml b/docker-compose.yml index c25b870..de4cda4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,3 +9,14 @@ services: ports: - 5672:5672 - 15672:15672 + postgres: + image: postgres:13.2-alpine + environment: + POSTGRES_USER: admin + POSTGRES_PASSWORD: admin + PGDATA: /data/postgres + POSTGRES_DB: codebench + volumes: + - ./volume/postgres:/data/postgres + ports: + - 5432:5432 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 6df3702..725847a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,25 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@prisma/client": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-2.22.1.tgz", + "integrity": "sha512-JQjbsY6QSfFiovXHEp5WeJHa5p2CuR1ZFPAeYXmUsOAQOaMCrhgQmKAL6w2Q3SRA7ALqPjrKywN9/QfBc4Kp1A==", + "requires": { + "@prisma/engines-version": "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c" + } + }, + "@prisma/engines": { + "version": "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c.tgz", + "integrity": "sha512-KmWdogrsfsSLYvfqY3cS3QcDGzaEFklE+T6dNJf+k/KPQum4A29IwDalafMwh5cMN8ivZobUbowNSwWJrMT08Q==", + "dev": true + }, + "@prisma/engines-version": { + "version": "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c.tgz", + "integrity": "sha512-OkkVwk6iTzTbwwl8JIKAENyxmh4TFORal55QMKQzrHEY8UzbD0M90mQnmziz3PAopQUZgTFFMlaPAq1WNrLMtA==" + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -23,6 +42,7 @@ "version": "0.5.17", "resolved": "https://registry.npmjs.org/@types/amqplib/-/amqplib-0.5.17.tgz", "integrity": "sha512-RImqiLP1swDqWBW8UX9iBXVEOw6MYzNmxdXqfemDfdwtUvdTM/W0s2RlSuMVIGkRhaWvpkC9O/N81VzzQwfAbw==", + "dev": true, "requires": { "@types/bluebird": "*", "@types/node": "*" @@ -31,7 +51,8 @@ "@types/bluebird": { "version": "3.5.34", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.34.tgz", - "integrity": "sha512-QMc57Pf067Rr78l6f4FftvuIXPYxu0VYFRKrZk1Clv+LWy7gN2fTBiAiv68askFHEHZcTLPFd01kNlpKOiSPgQ==" + "integrity": "sha512-QMc57Pf067Rr78l6f4FftvuIXPYxu0VYFRKrZk1Clv+LWy7gN2fTBiAiv68askFHEHZcTLPFd01kNlpKOiSPgQ==", + "dev": true }, "@types/body-parser": { "version": "1.19.0", @@ -84,7 +105,8 @@ "@types/node": { "version": "12.20.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.12.tgz", - "integrity": "sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg==" + "integrity": "sha512-KQZ1al2hKOONAs2MFv+yTQP1LkDWMrRJ9YCVRalXltOfXsBmH5IownLxQaiq0lnAHwAViLnh2aTYqrPcRGEbgg==", + "dev": true }, "@types/qs": { "version": "6.9.6", @@ -286,6 +308,11 @@ "fill-range": "^7.0.1" } }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -544,6 +571,14 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -876,12 +911,60 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", "dev": true }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -900,6 +983,41 @@ "package-json": "^6.3.0" } }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", @@ -1114,6 +1232,15 @@ "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "dev": true }, + "prisma": { + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-2.22.1.tgz", + "integrity": "sha512-hwvCM3zyxgSda/+/p+GW7nz93jRebtMU01wAG7YVVnl0OKU+dpw1wPvPFmQRldkZHk8fTCleYmjc24WaSdVPZQ==", + "dev": true, + "requires": { + "@prisma/engines": "2.22.0-21.60cc71d884972ab4e897f0277c4b84383dddaf6c" + } + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -1251,8 +1378,7 @@ "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-diff": { "version": "3.1.1", diff --git a/package.json b/package.json index 8e80497..f60f5f5 100644 --- a/package.json +++ b/package.json @@ -18,10 +18,13 @@ }, "homepage": "https://github.com/codebench-esgi/codebench-api#readme", "dependencies": { + "@prisma/client": "^2.22.1", "amqplib": "^0.7.1", "body-parser": "^1.19.0", "dotenv": "^9.0.2", "express": "^4.17.1", + "js-sha256": "^0.9.0", + "jsonwebtoken": "^8.5.1", "uuid": "^8.3.2" }, "devDependencies": { @@ -30,6 +33,7 @@ "@types/node": "^12.20.12", "@types/uuid": "^8.3.0", "nodemon": "^2.0.7", + "prisma": "^2.22.1", "ts-node": "9.1.1", "typescript": "4.2.4" } diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..0d8e341 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,73 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + username String @unique @db.VarChar(255) + email String @unique @db.VarChar(255) + password String @db.VarChar(65) + profilepicurl String? + benchmarks Benchmark[] + comments Comment[] + sessions Session[] +} + +model Benchmark { + id Int @id @default(autoincrement()) + title String + subject String + giturl String? + createdAt DateTime @default(now()) @db.Timestamp(6) + difficulty String? + userId Int + user User @relation(fields: [userId], references: [id]) + comments Comment[] + try Try[] +} + +model Comment { + id Int @id @default(autoincrement()) + comment String + createdAt DateTime @default(now()) @db.Timestamp(6) + benchmarkId Int + userId Int + benchmark Benchmark @relation(fields: [benchmarkId], references: [id]) + user User @relation(fields: [userId], references: [id]) +} + +model Language { + id Int @id @default(autoincrement()) + name String @db.VarChar(255) + implementation String @db.VarChar(255) + version String @db.VarChar(255) + try Try[] +} + +model Session { + id Int @id @default(autoincrement()) + token String + createdAt DateTime @default(now()) @db.Timestamp(6) + deletedAt DateTime? @db.Timestamp(6) + userId Int + user User @relation(fields: [userId], references: [id]) +} + +model Try { + id Int @id @default(autoincrement()) + code String? + stdoutput String? + erroutput String? + status String @db.VarChar(255) + rate Int? + createdAt DateTime @default(now()) @db.Timestamp(6) + benchmarkId Int + languageId Int + benchmark Benchmark @relation(fields: [benchmarkId], references: [id]) + language Language @relation(fields: [languageId], references: [id]) +} diff --git a/src/controllers/auth.controller.ts b/src/controllers/auth.controller.ts new file mode 100644 index 0000000..9bc8e1d --- /dev/null +++ b/src/controllers/auth.controller.ts @@ -0,0 +1,37 @@ +import { sha256 } from 'js-sha256'; +import { UserDao } from "../dao/user.dao"; +import { Request, Response } from "express"; +const jwt = require('jsonwebtoken'); + +export class AuthController { + + static async login(email: string, password: string, req: Request, res: Response) { + const jwt_key = process.env.JWT_KEY || 'wonderfullkey'; + const user = await UserDao.getByEmail(email); + + if (user === null) { + res.status(404).json({ message: 'Unknown user' } ); + return; + } + + console.log(password) + console.log(user.password) + console.log(sha256(password)) + + if (user.password == sha256(password)) { + // JWT auth sign token + jwt.sign({user}, jwt_key, {expiresIn: '1d'}, (err: any, token: any) => { + console.log("INSIDE " + token); + if (err){ + res.status(500).json({ message: 'Failed to generate your token' } ); + return; + } + res.status(200).json(token); + }); + + } else { + res.status(403).json({ message: 'Passwords doesn\'t match' } ); + } + } + +} \ No newline at end of file diff --git a/src/dao/user.dao.ts b/src/dao/user.dao.ts new file mode 100644 index 0000000..8aafeb9 --- /dev/null +++ b/src/dao/user.dao.ts @@ -0,0 +1,35 @@ +import {PrismaClient} from "@prisma/client"; + +export class UserDao { + + static async createUser(user: { password: string; email: string; username: string }) { + const prisma = new PrismaClient(); + + try { + return await prisma.user.create({ + data: { + username: user.username, + email: user.email, + password: user.password + }, + }); + } catch (e) { + if (e.code === 'P2002') + return {message: "This username or email already taken"} + } + } + + static async getAll() { + const prisma = new PrismaClient(); + return await prisma.user.findMany(); + } + + static async getByEmail(email: string) { + const prisma = new PrismaClient(); + return await prisma.user.findUnique({ + where: { + email: email, + } + }); + } +} diff --git a/src/routes/auth.route.ts b/src/routes/auth.route.ts new file mode 100644 index 0000000..d35c643 --- /dev/null +++ b/src/routes/auth.route.ts @@ -0,0 +1,25 @@ +import express from "express"; +import bodyParser = require('body-parser'); +import {AuthController} from "../controllers/auth.controller"; + +module.exports = function (app: express.Application) { + + app.get('/api', async (req, res) => { + res.status(200).json({ message: 'Welcome to CodeBench-API' }); + }); + + app.post('/api/login', bodyParser.json(), async (req, res) => { + const data = req.body; + + if (data.password == null || data.email == null) { + res.status(400).json({message : 'You must provide your email and password to login'}); + } + + const response = await AuthController.login(data.email, data.password, req, res); + + res.send({ response: response}); + + }); + + +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 1d7849c..39df40c 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,7 +1,9 @@ import express from 'express'; const tryRoutes = require('./try.route') +const authRoutes = require('./auth.route') module.exports = function (app: express.Application) { tryRoutes(app); + authRoutes(app); }; diff --git a/src/routes/try.route.ts b/src/routes/try.route.ts index 2368316..8089caa 100644 --- a/src/routes/try.route.ts +++ b/src/routes/try.route.ts @@ -12,9 +12,6 @@ const option = { }; module.exports = function (app: express.Application) { - app.get('/api', async (req, res) => { - res.status(200).json({ message: 'Welcome to CodeBench-API' }); - }); app.post('/api/try', bodyParser.json(), async (req, res) => { const job = req.body;