mirror of
https://github.com/usatiuk/photos.git
synced 2025-10-28 15:27:49 +01:00
get users working
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,13 @@
|
||||
"noImplicitAny": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"~*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "mariadb",
|
||||
"host": "db",
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"username": "photos",
|
||||
"password": "photos",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"type": "mariadb",
|
||||
"host": "dbtest",
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"username": "photos",
|
||||
"password": "photos",
|
||||
|
||||
925
package-lock.json
generated
925
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
55
src/app.ts
55
src/app.ts
@@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -15,23 +15,39 @@ export interface IConfig {
|
||||
dbConnectionOptions: ConnectionOptions | null;
|
||||
}
|
||||
|
||||
const production: IConfig = {
|
||||
env: EnvType.production,
|
||||
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000,
|
||||
jwtSecret: ((): string => {
|
||||
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: 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
9
src/routes/dev.ts
Normal 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
117
src/routes/users.ts
Normal 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
8
src/tests/.eslintrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"plugins": [
|
||||
"mocha"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:mocha/recommended"
|
||||
]
|
||||
}
|
||||
142
src/tests/integration/users.test.ts
Normal file
142
src/tests/integration/users.test.ts
Normal 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;
|
||||
});
|
||||
});
|
||||
30
src/tests/integration/util.ts
Normal file
30
src/tests/integration/util.ts
Normal 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
4
src/types.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export interface IAPIResponse<T> {
|
||||
data: T | null;
|
||||
error: string | null;
|
||||
}
|
||||
@@ -12,10 +12,17 @@
|
||||
"sourceMap": true,
|
||||
"noImplicitAny": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true
|
||||
"strictNullChecks": true,
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"~*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./tests/**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"frontend"
|
||||
|
||||
Reference in New Issue
Block a user