1
0
forked from fr/deguapp

Added: tests, signin api, not working docker

This commit is contained in:
Filip Rojek 2024-05-01 23:24:03 +02:00
parent dc1b955a8a
commit bd8a5d607f
14 changed files with 553 additions and 83 deletions

23
api/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM node:18-alpine
WORKDIR /nork
COPY ../../nork .
RUN npm install
RUN npm run build
RUN npm link
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

19
api/docker-compose.yaml Normal file
View File

@ -0,0 +1,19 @@
services:
mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: root
ports:
- 27017:27017
mongo-express:
image: mongo-express
restart: always
ports:
- 8081:8081
environment:
ME_CONFIG_MONGODB_ADMINUSERNAME: root
ME_CONFIG_MONGODB_ADMINPASSWORD: root
ME_CONFIG_MONGODB_URL: mongodb://root:root@mongo:27017/
ME_CONFIG_BASICAUTH: false

View File

@ -19,22 +19,29 @@
"author": "Filip Rojek", "author": "Filip Rojek",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bcrypt": "^5.1.1",
"colors": "1.4.0", "colors": "1.4.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"fs-extra": "^10.0.0", "fs-extra": "^10.0.0",
"inquirer": "^8.1.2", "inquirer": "^8.1.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.3.3",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"pad": "^3.2.0", "pad": "^3.2.0",
"path": "^0.12.7" "path": "^0.12.7",
"yup": "^1.4.0",
"yup-password": "^0.4.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.7.1", "@biomejs/biome": "1.7.1",
"@types/bcrypt": "^5.0.2",
"@types/chai": "^4.2.22", "@types/chai": "^4.2.22",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/fs-extra": "^9.0.13", "@types/fs-extra": "^9.0.13",
"@types/inquirer": "^8.1.3", "@types/inquirer": "^8.1.3",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/shelljs": "^0.8.11", "@types/shelljs": "^0.8.11",
@ -46,6 +53,7 @@
"http": "^0.0.1-security", "http": "^0.0.1-security",
"jest": "^29.7.0", "jest": "^29.7.0",
"mocha": "^9.1.3", "mocha": "^9.1.3",
"mongodb-memory-server": "^9.2.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.7.1", "prettier": "^2.7.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",

View File

@ -1,5 +1,81 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import env from '../config/environment';
import Docs from '../services/docsService';
import User from '../models/User';
import {Log} from 'nork'
import { IUser, signupExam, ISignin, signinExam } from '../validators/authValidator';
export function signup_post(req: Request, res: Response) { new Docs('user', 'signup', '/api/v1/auth/signup', 'POST', 'user signup api', undefined, signupExam, 'status object');
res.send("logged in"); export async function signup_post(req: Request, res: Response) {
try {
const payload: IUser = req.body;
payload.password = await bcrypt.hash(payload.password, 12);
const user = new User(payload);
await user.save();
res.status(201).json(Log.info(201, 'user was successfully signed up'));
} catch (err: any) {
if (err.code == 11000) {
res.status(400).json(Log.error(400, 'this user already exists'));
return;
}
Log.error(500, err);
res.status(500).json(Log.error(500, 'something went wrong'));
}
}
new Docs('user', 'signin', '/api/v1/auth/signin', 'POST', 'user signin api', undefined, signinExam, 'status object');
export async function signin_post(req: Request, res: Response) {
try {
const payload: ISignin = req.body;
const user = await User.findOne({ email: payload.email });
if (!user) {
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 });
res.cookie('auth', false, { httpOnly: false, maxAge: 0 });
res.status(401).json(Log.error(401, 'email or password is wrong'));
return;
}
if (await bcrypt.compare(payload.password, user.password)) {
const maxAge = 3 * 24 * 60 * 60; // 3 days in seconds
const createToken = (id: any) => {
return jwt.sign({ id }, env.JWT_SECRET, {
expiresIn: maxAge
});
};
const token = createToken(user._id);
res.cookie('jwt', token, { httpOnly: true, maxAge: maxAge * 1000 });
res.cookie('auth', true, { httpOnly: false, maxAge: maxAge * 1000 });
res.json(Log.info(200, 'user is logged in'));
return;
}
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 });
res.cookie('auth', false, { httpOnly: false, maxAge: 0 });
res.status(401).json(Log.error(401, 'email or password is wrong'));
} catch (err: any) {
Log.error(500, err);
res.status(500).json(Log.error(500, 'something went wrong'));
}
}
new Docs('user', 'logout', '/api/v1/auth/logout', 'POST', 'user logout api', undefined, {}, 'status object');
export function logout_post(req: Request, res: Response) {
res.cookie('jwt', '', { httpOnly: true, maxAge: 0 });
res.cookie('auth', false, { httpOnly: false, maxAge: 0 });
res.json(Log.info(200, 'user was logged out'));
}
new Docs('user', 'status', '/api/v1/auth/status', 'GET', 'user login status api', undefined, undefined, 'status code | user object');
export function status_get(req: Request, res: Response) {
let userObject = res.locals.user;
userObject.password = undefined;
userObject.__v = undefined;
res.status(200).json(Log.info(200, 'user is logged in', userObject));
} }

View File

@ -0,0 +1,15 @@
import { Request, Response, NextFunction } from 'express';
import { object, AnySchema } from 'yup';
import { Log } from 'nork';
const validate = (schema: AnySchema) => async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.validate(req.body);
next();
} catch (err: any) {
return res.status(400).json(Log.error(400, 'validation error', err));
}
};
export default validate;

26
api/src/models/User.ts Normal file
View File

@ -0,0 +1,26 @@
import path from 'path';
import { Schema, model } from 'mongoose';
import { IUser } from '../validators/authValidator';
const schema = new Schema<IUser>(
{
username: {
type: String,
required: true
},
email: {
type: String,
required: true,
unique: true
},
password: {
type: String,
required: true
}
},
{
timestamps: true
}
);
export default model(path.basename(__filename).split('.')[0], schema);

View File

@ -1,6 +1,7 @@
import { Router } from "express"; import { Router } from "express";
import * as authController from "../controllers/authController"; import * as authController from "../controllers/authController";
//import authValidator from "../validators/authValidator"; import validate from '../middlewares/validateRequest'
import * as AuthVal from '../validators/authValidator'
//import handleValidation from "../middlewares/handleValidation"; //import handleValidation from "../middlewares/handleValidation";
//import { requireAuth } from "../middlewares/authMiddleware"; //import { requireAuth } from "../middlewares/authMiddleware";
@ -8,7 +9,7 @@ const router = Router();
//const mws = [requireAuth, handleValidation.handleValidationError]; //const mws = [requireAuth, handleValidation.handleValidationError];
router.post("/signup", authController.signup_post); router.post("/auth/signup",validate(AuthVal.signup) , authController.signup_post);
//router.post( //router.post(
// "/login", // "/login",

View File

@ -1,9 +1,9 @@
import { Request, Response, Router } from "express"; import { Request, Response, Router } from "express";
import path from "path"; import path from "path";
import authRoutes from "./authRoutes"; import api_v1 from "./api_v1";
export const router = Router(); export const router = Router();
router.use("/api/auth", authRoutes); router.use("/api/v1", api_v1);
//router.get("*", (req: Request, res: Response) => { //router.get("*", (req: Request, res: Response) => {
// res.sendFile(path.join(__dirname, "../views/index.html")); // res.sendFile(path.join(__dirname, "../views/index.html"));

View File

@ -1,26 +1,40 @@
import http from "http"; import http from "http";
import { app } from "./app"; import { app } from "./app";
import env from "./config/environment"; import env from "./config/environment";
//const env = { import mongoose from 'mongoose' // TODO: dopsat nork module pro db
// APP_PORT: 8080,
// APP_HOSTNAME: "127.0.0.1",
//};
import { Log } from "nork"; import { Log } from "nork";
//import database from './config/database'
const port: number = env.APP_PORT || 8080; const port: number = env.APP_PORT || 8080;
const hostname: string = env.APP_HOSTNAME || "localhost"; const hostname: string = env.APP_HOSTNAME || "localhost";
export const server = http.createServer(app); export const server = http.createServer(app);
// Server // Server
export function runServer(): void { //export function runServer(): void {
server.listen(port, hostname, () => { // server.listen(port, hostname, () => {
Log.info(200, `Server is listening on http://${hostname}:${port}`); // Log.info(200, `Server is listening on http://${hostname}:${port}`);
}); // });
} //}
//
//if (!env.NORK.database) { ////if (!env.NORK.database) {
runServer(); //runServer();
//} else { //} else {
// const db_connection = database() // const db_connection = database()
// runServer() // runServer()
//} //}
(async () => {
if (!process.env.DOCS_GEN) {
try {
await mongoose.connect(env.DB_URI);
Log.info(200, 'connected to db');
server.listen(port, () => {
Log.info(200, `Server is listening on http://localhost:${port}`);
});
} catch (err: any) {
Log.error(500, err);
}
}
})();

View File

@ -0,0 +1,62 @@
import fs from 'fs';
import path from 'path';
export type HttpMethods = 'POST' | 'GET' | 'PUT' | 'DELETE';
export type ApiResponse = 'status object' | Object | string;
export class Docs {
private name: string;
private operation: string;
private route: string;
private method: HttpMethods;
private parameters?: Object[];
private description?: string;
private body?: Object;
private response?: ApiResponse;
public constructor(name: string, operation: string, route: string, method: HttpMethods, description?: string, parameters?: Object[], body?: Object, response?: ApiResponse) {
this.name = name;
this.operation = operation;
this.route = route;
this.method = method;
description ? (this.description = description) : null;
parameters ? (this.parameters = parameters) : null;
body ? (this.body = body) : null;
response ? (this.response = response) : null;
this.generate();
}
private generate(): boolean {
if (!process.env.DOCS_GEN) {
return false;
}
const jsonPath = path.join(__dirname, '../public/api.json');
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '../../package.json')).toString());
const jsonAPI = () => JSON.parse(fs.readFileSync(jsonPath).toString());
const genJsonAPI = () => fs.writeFileSync(jsonPath, JSON.stringify({ version: pkg.version, endpoints: {} }));
if (!fs.existsSync(path.join(__dirname, '../public'))) {
fs.mkdirSync(path.join(__dirname, '../public'));
}
if (!fs.existsSync(jsonPath)) {
genJsonAPI();
}
if (jsonAPI().version != pkg.version) {
genJsonAPI();
}
const data = jsonAPI();
data.endpoints[this.name] ? undefined : (data.endpoints[this.name] = {});
data.endpoints[this.name][this.operation] = this;
fs.writeFileSync(jsonPath, JSON.stringify(data));
return true;
}
}
export default Docs;

View File

@ -1,48 +0,0 @@
import { Sequelize } from 'sequelize';
let sequelize: Sequelize | null = null;
const connectDB = async () => {
sequelize = new Sequelize({
dialect: 'mariadb',
host: 'localhost',
username: 'your_username',
password: 'your_password',
database: 'your_database',
});
try {
await sequelize.authenticate();
console.log('Connection to the database has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}
};
const dropDB = async () => {
if (sequelize) {
try {
await sequelize.drop();
console.log('Database dropped successfully.');
} catch (error) {
console.error('Error dropping database:', error);
} finally {
await sequelize.close();
}
}
};
const dropTables = async () => {
if (sequelize) {
try {
await sequelize.sync({ force: true });
console.log('All tables dropped successfully.');
} catch (error) {
console.error('Error dropping tables:', error);
} finally {
await sequelize.close();
}
}
};
export { connectDB, dropDB, dropTables };

View File

@ -0,0 +1,30 @@
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongo: any = null;
const connectDB = async () => {
mongo = await MongoMemoryServer.create({ binary: { os: { os: 'linux', dist: 'ubuntu', release: '18.04' } } }); // TODO: check that host OS is Void Linux, else remove the argument
const uri = mongo.getUri();
await mongoose.connect(uri);
};
const dropDB = async () => {
if (mongo) {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
await mongo.stop();
}
};
const dropCollections = async () => {
if (mongo) {
const collections = await mongoose.connection.db.collections();
for (let collection of collections) {
await collection.remove();
}
}
};
export { connectDB, dropDB, dropCollections };

View File

@ -0,0 +1,34 @@
import * as yup from 'yup';
import YupPassword from 'yup-password';
YupPassword(yup);
import { Schema } from 'mongoose';
interface mongooseAddition {
_id?: Schema.Types.ObjectId;
createdAt?: Schema.Types.Date;
updatedAt?: Schema.Types.Date;
}
// SignUp
export const signup = yup.object({
username: yup.string().required(),
email: yup.string().email().required(),
password: yup.string().min(8).minLowercase(1).minUppercase(1).minNumbers(1).required()
});
export interface IUser extends yup.InferType<typeof signup>, mongooseAddition {}
export const signupExam: IUser = {
username: 'testuser',
email: 'text@example.com',
password: 'Test1234'
};
// SignIn
export const signin = yup.object({
email: yup.string().email().required(),
password: yup.string().min(8).minLowercase(1).minUppercase(1).minNumbers(1).required()
});
export interface ISignin extends yup.InferType<typeof signin>, mongooseAddition {}
export const signinExam: ISignin = {
email: 'text@example.com',
password: 'Test1234'
};

View File

@ -1,17 +1,227 @@
import request from "supertest" import supertest from 'supertest';
import {server as app} from "../server" import { app } from '../src/app';
import { connectDB, dropDB, dropCollections } from '../src/utils/test_mongodb';
describe('Auth API Endpoints', () => { const request = supertest(app);
afterAll((done) => {
app.close(done);
})
it('should signup new user', async () => { export const getJWT = async () => {
const res = await request(app) try {
.post('/api/auth/signup') const resReg: any = await request.post('/api/v1/auth/signup').send({
.send({}); email: 'test@example.local',
password: 'admin1234',
username: 'Test Test'
});
expect(res.statusCode).toEqual(200) const resLog: any = await request.post('/api/auth/login').send({
email: 'test@example.local',
password: 'admin1234'
});
if (resLog.statusCode != 200) throw 'error while logging in';
}) const body = JSON.parse(resLog.text);
}) return Promise.resolve(body.data.jwt);
} catch (err: any) {
console.log(err);
return err;
}
};
/**
*
* @returns JWT cookie
*/
export async function login(): Promise<string> {
const res = await request.post('/api/v1/auth/signin').send({
email: 'thisistest@host.local',
password: 'Admin1234'
});
return res.headers['set-cookie'];
}
export async function signup(): Promise<boolean> {
const res = await request.post('/api/v1/auth/signup').send({
email: 'thisistest@host.local',
password: 'Admin1234',
username: 'Test Test'
});
if (res.statusCode == 201) return true;
return false;
}
describe('POST /api/v1/auth/signup', () => {
describe('should drop validation error', () => {
it('should drop 400 (empty request))', async () => {
const res: any = await request.post('/api/v1/auth/signup').send({});
expect(res.statusCode).toBe(400);
});
it('should drop 400 (email))', async () => {
const res: any = await request.post('/api/v1/auth/signup').send({
email: '',
username: 'User Admin',
password: 'Admin1234'
});
console.log(res)
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('email');
});
it('should drop 400 (username))', async () => {
const res: any = await request.post('/api/v1/auth/signup').send({
email: 'admin@localhost.local',
username: '',
password: 'Admin1234'
});
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('username');
});
it('should drop 400 (password))', async () => {
const res: any = await request.post('/api/v1/auth/signup').send({
email: 'admin@localhost.local',
username: 'User Admin',
password: ''
});
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password');
});
it('should drop 400 (password - min 8 chars', async () => {
const res = await request.post('/api/v1/auth/signup').send({
email: 'admin@localhost.local',
username: 'User Admin',
password: 'Admin12'
});
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password');
});
it('should drop 400 (password - min 1 number', async () => {
const res = await request.post('/api/v1/auth/signup').send({
email: 'admin@localhost.local',
username: 'User Admin',
password: 'Adminadmin'
});
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password');
});
it('should drop 400 (password - min 1 uppercase', async () => {
const res = await request.post('/api/v1/auth/signup').send({
email: 'admin@localhost.local',
username: 'User Admin',
password: 'admin1234'
});
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password');
});
});
test('should register an user', async () => {
const res: any = await request.post('/api/v1/auth/signup').send({
email: 'thisistest@host.local',
password: 'Admin1234',
username: 'Test Test'
});
expect(res.statusCode).toBe(201);
});
});
/*
describe('POST /api/v1/auth/signin', () => {
const url = '/api/v1/auth/signin';
describe('should drop an validation error', () => {
it('should drop 400 (empty)', async () => {
const res = await request.post(url).send();
expect(res.statusCode).toBe(400);
});
it('should drop 400 (email)', async () => {
const res = await request.post(url).send({
password: 'Admin1234'
});
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('email');
});
it('should drop 400 (password)', async () => {
const res = await request.post(url).send({
email: 'thisistest@host.local'
});
const body = JSON.parse(res.text);
expect(res.statusCode).toBe(400);
expect(body.data.path).toBe('password');
});
});
test('should drop 401', async () => {
const res = await request.post(url).send({
email: 'thisistest@host.local',
password: 'Test12365465132'
});
expect(res.statusCode).toBe(401);
});
test('should login an user', async () => {
const res: any = await request.post(url).send({
email: 'thisistest@host.local',
password: 'Admin1234'
});
expect(res.statusCode).toBe(200);
});
});
/**
* Throws errors idk
describe('POST /api/v1/auth/logout', () => {
const url = '/api/v1/auth/logout';
test('should drop 401 error', async () => {
const res = await request.post(url).send({});
expect(res.statusCode).toBe(401);
});
test('should logout an user', async () => {
const jwt = await login();
const res = await request.post(url).set('Cookie', jwt).send({});
res.headers['set-cookie'].forEach((el: any) => {
if (el.split('=')[0] == 'jwt') {
expect(Number(el.split('=')[2][0])).toBe(0);
}
});
expect(res.statusCode).toBe(200);
});
});
*/
/*
describe('GET /api/v1/auth/status', () => {
const url = '/api/v1/auth/status';
test('should return login status 401', async () => {
const res = await request.get(url).send();
expect(res.statusCode).toBe(401);
});
test('should return login status 200', async () => {
const jwt = await login();
const res = await request.get(url).set('Cookie', jwt).send();
expect(res.statusCode).toBe(200);
});
});
*/