get users working

This commit is contained in:
2020-10-11 18:58:18 +03:00
committed by Stepan Usatiuk
parent e4b8ed0702
commit 978c0d9438
16 changed files with 1360 additions and 22 deletions

View File

@@ -40,6 +40,8 @@
}
},
"rules": {
"@typescript-eslint/require-await": "warn"
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-assignment": "off"
}
}

View File

@@ -15,7 +15,13 @@
"noImplicitAny": true,
"allowSyntheticDefaultImports": true,
"strictFunctionTypes": true,
"strictNullChecks": true
"strictNullChecks": true,
"baseUrl": "./src",
"paths": {
"~*": [
"./*"
]
}
},
"include": [
"./src/**/*.ts",

View File

@@ -1,6 +1,6 @@
{
"type": "mariadb",
"host": "db",
"host": "localhost",
"port": 3306,
"username": "photos",
"password": "photos",

View File

@@ -1,6 +1,6 @@
{
"type": "mariadb",
"host": "dbtest",
"host": "localhost",
"port": 3306,
"username": "photos",
"password": "photos",

925
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,41 +3,65 @@
"version": "0.0.1",
"scripts": {
"start-frontend": "cd frontend && npm start",
"ts-node-dev": "ts-node-dev ./src/server.ts",
"start": "ts-node -T -r tsconfig-paths/register src/server.ts",
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register ./src/server.ts",
"dev": "cross-env NODE_ENV=development concurrently npm:ts-node-dev npm:start-frontend -c 'blue,green'",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "cross-env NODE_ENV=test mocha --timeout 15000 -r ts-node/register -r tsconfig-paths/register 'src/tests/**/*.ts' ",
"lint": "eslint ./src/** --ext .js,.jsx,.ts,.tsx",
"lint-frontend": "cd frontend && npm run lint",
"lint-all": "npm run lint && npm run lint-frontend"
},
"license": "MIT",
"dependencies": {
"@koa/cors": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0",
"bcrypt": "^5.0.0",
"chai": "^4.2.0",
"concurrently": "^5.3.0",
"cross-env": "^7.0.2",
"eslint": "^7.10.0",
"eslint-config-prettier": "^6.12.0",
"eslint-import-resolver-typescript": "^2.3.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-mocha": "^8.0.0",
"eslint-plugin-prettier": "^3.1.4",
"jsonwebtoken": "^8.5.1",
"koa": "^2.13.0",
"koa-body": "^4.2.0",
"koa-jwt": "^4.0.0",
"koa-logger": "^3.2.1",
"koa-router": "^9.4.0",
"koa-send": "^5.0.1",
"koa-sslify": "^4.0.3",
"koa-static": "^5.0.0",
"mocha": "^8.1.3",
"mysql": "^2.18.1",
"prettier": "^2.1.2",
"prettier-eslint": "^11.0.0",
"supertest": "^5.0.0",
"ts-node": "^9.0.0",
"ts-node-dev": "^1.0.0-pre.63",
"tsconfig-paths": "^3.9.0",
"typeorm": "^0.2.28",
"typescript": "^4.0.3"
},
"devDependencies": {
"@types/bcrypt": "^3.0.0",
"@types/chai": "^4.2.13",
"@types/concurrently": "^5.2.1",
"@types/eslint": "^7.2.3",
"@types/eslint-plugin-prettier": "^3.1.0",
"@types/jsonwebtoken": "^8.5.0",
"@types/koa": "^2.11.4",
"@types/prettier": "^2.1.1"
"@types/koa-logger": "^3.1.1",
"@types/koa-router": "^7.4.1",
"@types/koa-send": "^4.1.2",
"@types/koa-sslify": "^4.0.1",
"@types/koa-static": "^4.0.1",
"@types/koa__cors": "^3.0.2",
"@types/mocha": "^8.0.3",
"@types/prettier": "^2.1.1",
"@types/supertest": "^2.0.10"
}
}

View File

@@ -1,7 +1,58 @@
import "reflect-metadata";
import * as cors from "@koa/cors";
import * as Koa from "koa";
import * as bodyParser from "koa-body";
import * as jwt from "koa-jwt";
import * as logger from "koa-logger";
import * as send from "koa-send";
import sslify, { xForwardedProtoResolver } from "koa-sslify";
import * as serve from "koa-static";
import { config, EnvType } from "~config";
import { userRouter } from "~routes/users";
import { devRouter } from "~routes/dev";
export const app = new Koa();
app.use(async (ctx) => {
ctx.body = "hello!";
app.use(cors());
app.use(logger());
app.use(bodyParser());
if (config.env === EnvType.production) {
app.use(sslify({ resolver: xForwardedProtoResolver }));
}
app.use(
jwt({
secret: config.jwtSecret,
passthrough: true,
}),
);
app.use(async (ctx, next) => {
try {
await next();
const status = ctx.status || 404;
if (status === 404) {
await send(ctx, "frontend/dist/index.html");
}
} catch (err) {
ctx.status = err.status || 500;
ctx.body = err.message;
ctx.app.emit("error", err, ctx);
}
});
app.use(serve("frontend/dist"));
app.use(userRouter.routes()).use(userRouter.allowedMethods());
if (config.env === EnvType.development) {
app.use(devRouter.routes()).use(devRouter.allowedMethods());
}
app.on("error", (err, ctx) => {
ctx.body = {
error: err.message,
data: false,
};
});

View File

@@ -4,5 +4,7 @@ import { Connection, createConnection } from "typeorm";
import { config } from "./";
export async function connect(): Promise<Connection> {
return createConnection(config.dbConnectionOptions);
return config.dbConnectionOptions
? createConnection(config.dbConnectionOptions)
: createConnection();
}

View File

@@ -15,23 +15,39 @@ export interface IConfig {
dbConnectionOptions: ConnectionOptions | null;
}
function getJwtSecret(): string {
switch (process.env.NODE_ENV) {
case "production":
if (process.env.JWT_SECRET === undefined) {
console.log("JWT_SECRET is not set");
process.exit(1);
} else {
return process.env.JWT_SECRET;
}
break;
case "development":
return "DEVSECRET";
break;
case "test":
return "TESTSECRET";
break;
default:
console.log("Unknown NODE_ENV");
process.exit(1);
break;
}
}
const production: IConfig = {
env: EnvType.production,
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
jwtSecret: ((): string => {
if (process.env.JWT_SECRET === undefined) {
console.log("JWT_SECRET is not set");
process.exit(1);
}
return process.env.JWT_SECRET;
})(),
jwtSecret: getJwtSecret(),
dbConnectionOptions: null,
};
const development: IConfig = {
...production,
env: EnvType.development,
jwtSecret: "DEVSECRET",
dbConnectionOptions:
process.env.NODE_ENV === "development"
? fs.existsSync("./ormconfig.dev.json")
@@ -45,7 +61,6 @@ const development: IConfig = {
const test: IConfig = {
...production,
env: EnvType.test,
jwtSecret: "TESTSECRET",
dbConnectionOptions:
process.env.NODE_ENV === "test"
? process.env.CI

9
src/routes/dev.ts Normal file
View File

@@ -0,0 +1,9 @@
import * as Router from "koa-router";
import { User } from "~entity/User";
export const devRouter = new Router();
devRouter.post("/dev/clean", async (ctx) => {
await User.remove(await User.find());
ctx.body = { success: true };
});

117
src/routes/users.ts Normal file
View File

@@ -0,0 +1,117 @@
import * as Router from "koa-router";
import { IUserJWT, User } from "~entity/User";
export const userRouter = new Router();
userRouter.get("/users/user", async (ctx) => {
if (!ctx.state.user) {
ctx.throw(401);
}
const jwt = ctx.state.user as IUserJWT;
const user = await User.findOne(jwt.id);
if (!user) {
ctx.throw(401);
return;
}
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/login", async (ctx) => {
const request = ctx.request;
if (!request.body) {
ctx.throw(400);
}
const { username, password } = request.body as {
username: string | undefined;
password: string | undefined;
};
if (!(username && password)) {
ctx.throw(400);
return;
}
const user = await User.findOne({ username });
if (!user || !(await user.verifyPassword(password))) {
ctx.throw(404, "User not found");
return;
}
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/signup", async (ctx) => {
const request = ctx.request;
if (!request.body) {
ctx.throw(400);
}
const { username, password, email } = request.body as {
username: string | undefined;
password: string | undefined;
email: string | undefined;
};
if (!(username && password && email)) {
ctx.throw(400);
return;
}
const user = new User(username, email);
await user.setPassword(password);
try {
await user.save();
} catch (e) {
if (e.code === "ER_DUP_ENTRY") {
ctx.throw(400, "User already exists");
}
}
ctx.body = { error: false, data: user.toAuthJSON() };
});
userRouter.post("/users/edit", async (ctx) => {
if (!ctx.state.user) {
ctx.throw(401);
}
const jwt = ctx.state.user as IUserJWT;
const user = await User.findOne(jwt.id);
const request = ctx.request;
if (!user) {
ctx.throw(401);
return;
}
if (!request.body) {
ctx.throw(400);
return;
}
const { password } = request.body as {
password: string | undefined;
};
if (!password) {
ctx.throw(400);
return;
}
await user.setPassword(password);
try {
await user.save();
} catch (e) {
ctx.throw(400);
}
ctx.body = { error: false, data: user.toAuthJSON() };
});

8
src/tests/.eslintrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"plugins": [
"mocha"
],
"extends": [
"plugin:mocha/recommended"
]
}

View File

@@ -0,0 +1,142 @@
import { expect } from "chai";
import { connect } from "config/database";
import * as request from "supertest";
import { getConnection } from "typeorm";
import { app } from "~app";
import { IUserAuthJSON, User } from "~entity/User";
import { ISeed, seedDB } from "./util";
const callback = app.callback();
let seed: ISeed;
describe("users", function () {
before(async function () {
await connect();
});
after(async function () {
await getConnection().close();
});
beforeEach(async function () {
seed = await seedDB();
});
it("should get user", async function () {
const response = await request(callback)
.get("/users/user")
.set({
Authorization: `Bearer ${seed.user1.toJWT()}`,
"Content-Type": "application/json",
})
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
expect(user).to.deep.equal(seed.user1.toJSON());
});
it("should login user", async function () {
const response = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" })
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
expect(user).to.deep.equal(seed.user1.toJSON());
});
it("should not login user with wrong password", async function () {
const response = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "asdf" })
.expect(404);
expect(response.body.error).to.be.equal("User not found");
expect(response.body.data).to.be.false;
});
it("should signup user", async function () {
const response = await request(callback)
.post("/users/signup")
.set({ "Content-Type": "application/json" })
.send({
username: "NUser1",
password: "NUser1",
email: "nuser1@users.com",
})
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
const newUser = await User.findOneOrFail({ username: "NUser1" });
expect(user).to.deep.equal(newUser.toJSON());
});
it("should not signup user with duplicate username", async function () {
const response = await request(callback)
.post("/users/signup")
.set({ "Content-Type": "application/json" })
.send({
username: "User1",
password: "NUser1",
email: "user1@users.com",
})
.expect(400);
expect(response.body.error).to.be.equal("User already exists");
expect(response.body.data).to.be.false;
});
it("should change user's password", async function () {
const response = await request(callback)
.post("/users/edit")
.set({
Authorization: `Bearer ${seed.user1.toJWT()}`,
"Content-Type": "application/json",
})
.send({
password: "User1NewPass",
})
.expect("Content-Type", /json/)
.expect(200);
expect(response.body.error).to.be.false;
const loginResponse = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1NewPass" })
.expect("Content-Type", /json/)
.expect(200);
expect(loginResponse.body.error).to.be.false;
const { jwt: _, ...user } = response.body.data as IUserAuthJSON;
expect(user).to.deep.equal(seed.user1.toJSON());
const badLoginResponse = await request(callback)
.post("/users/login")
.set({ "Content-Type": "application/json" })
.send({ username: "User1", password: "User1" })
.expect(404);
expect(badLoginResponse.body.error).to.be.equal("User not found");
expect(badLoginResponse.body.data).to.be.false;
});
});

View File

@@ -0,0 +1,30 @@
import { User } from "entity/User";
//import { Document } from "~entity/Document";
export interface ISeed {
user1: User;
user2: User;
// doc1: Document;
// doc2p: Document;
}
export async function seedDB(): Promise<ISeed> {
//await Document.remove(await Document.find());
await User.remove(await User.find());
const user1 = new User("User1", "user1@users.com");
await user1.setPassword("User1");
await user1.save();
const user2 = new User("User2", "user2@users.com");
await user2.setPassword("User2");
await user2.save();
//const doc1 = new Document(user1, "Doc1", "Doc1", false);
//const doc2p = new Document(user1, "Doc2", "Doc2", true);
//await doc1.save();
//await doc2p.save();
return { user1, user2 }; // doc1, doc2p };
}

4
src/types.ts Normal file
View File

@@ -0,0 +1,4 @@
export interface IAPIResponse<T> {
data: T | null;
error: string | null;
}

View File

@@ -12,10 +12,17 @@
"sourceMap": true,
"noImplicitAny": true,
"strictFunctionTypes": true,
"strictNullChecks": true
"strictNullChecks": true,
"baseUrl": "./src",
"paths": {
"~*": [
"./*"
]
}
},
"include": [
"./src/**/*.ts",
"./tests/**/*.ts",
],
"exclude": [
"frontend"