This commit is contained in:
2019-01-01 19:05:21 +03:00
commit 58c36bf43c
34 changed files with 14132 additions and 0 deletions

3
.eslintrc.js Normal file
View File

@@ -0,0 +1,3 @@
module.exports = {
"extends": ["airbnb", "plugin:prettier/recommended"]
};

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.idea/
.vscode/
node_modules/
build/
tmp/
temp/
dist/
ormconfig.json
ormconfig.test.json
.env
.directory

23
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,23 @@
image: node:10
stages:
- test-backend
variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "true"
MYSQL_DATABASE: writer_test
MYSQL_USER: writer
MYSQL_PASSWORD: writer
cache:
paths:
- node_modules/
- frontend/node_modules
test-backend:
stage: test-backend
services:
- mariadb:10
script:
- npm i
- npm test

4
.prettierrc Normal file
View File

@@ -0,0 +1,4 @@
{
"trailingComma": "all",
"tabWidth": 4
}

7
README.md Normal file
View File

@@ -0,0 +1,7 @@
# Awesome Project Build with TypeORM
Steps to run this project:
1. Run `npm i` command
2. Setup database settings inside `ormconfig.json` file
3. Run `npm start` command

12
frontend/.gitignore vendored Normal file
View File

@@ -0,0 +1,12 @@
.idea/
.vscode/
node_modules/
build/
tmp/
temp/
dist/
ormconfig.json
ormconfig.test.json
.env
.cache
.directory

8990
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
frontend/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "writer-frontend",
"scripts": {
"start": "parcel src/index.html",
"build": "parcel build src/index.html",
"test": "echo \"Error: no test specified\" && exit 1"
},
"devDependencies": {
"@types/node-sass": "^3.10.32",
"@types/parcel-bundler": "^1.10.1",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.11",
"@types/react-router": "^4.4.3",
"@types/react-router-dom": "^4.3.1",
"node-sass": "^4.11.0",
"parcel-bundler": "^1.11.0"
},
"dependencies": {
"@blueprintjs/core": "^3.10.0",
"@blueprintjs/icons": "^3.4.0",
"react": "^16.7.0",
"react-dom": "^16.7.0",
"react-redux": "^6.0.0",
"react-router": "^4.3.1",
"react-router-dom": "^4.3.1",
"redux": "^4.0.1",
"redux-saga": "^0.16.2"
}
}

15
frontend/src/App.tsx Normal file
View File

@@ -0,0 +1,15 @@
import * as React from "react";
import { Route, Switch } from "react-router";
import { Login } from "~Auth/Login";
import { Home } from "~Home";
export function App() {
return (
<>
<Switch>
<Route exact={true} path="/" component={Home} />
<Route path="/login" component={Login} />
</Switch>
</>
);
}

View File

@@ -0,0 +1,21 @@
.AuthForm {
margin: auto;
margin-top: 10rem;
width: 20rem;
form {
display: flex;
flex-direction: column;
h2 {
margin-bottom: 1rem;
}
.buttons {
display: flex;
flex-direction: row;
button.submit {
margin-left: auto;
justify-self: flex-end;
align-self: flex-end;
}
}
}
}

View File

@@ -0,0 +1,27 @@
import "./Auth.scss";
import { Button, Card, FormGroup, H2, InputGroup } from "@blueprintjs/core";
import * as React from "react";
export function Login() {
return (
<>
<Card className="AuthForm" elevation={2}>
<form>
<H2>Login</H2>
<FormGroup label="Username">
<InputGroup leftIcon="person" />
</FormGroup>
<FormGroup label="Password">
<InputGroup leftIcon="key" />
</FormGroup>
<div className="buttons">
<Button className="submit" intent="primary">
Login
</Button>
</div>
</form>
</Card>
</>
);
}

26
frontend/src/Home.tsx Normal file
View File

@@ -0,0 +1,26 @@
import { Alignment, Button, Classes, Navbar } from "@blueprintjs/core";
import * as React from "react";
export function Home() {
return (
<>
{" "}
<Navbar>
<Navbar.Group align={Alignment.LEFT}>
<Navbar.Heading>Writer</Navbar.Heading>
<Navbar.Divider />
<Button
className={Classes.MINIMAL}
icon="home"
text="Home"
/>
<Button
className={Classes.MINIMAL}
icon="document"
text="Files"
/>
</Navbar.Group>
</Navbar>
</>
);
}

15
frontend/src/index.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Writer</title>
</head>
<body>
<div id="body">
</div>
<script src="./index.tsx"></script>
</body>
</html>

19
frontend/src/index.tsx Normal file
View File

@@ -0,0 +1,19 @@
import "@blueprintjs/core/lib/css/blueprint.css";
import "@blueprintjs/icons/lib/css/blueprint-icons.css";
import "normalize.css/normalize.css";
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import { App } from "~App";
import { store } from "~redux/store";
render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("body"),
);

View File

@@ -0,0 +1,10 @@
import { Action } from "redux";
export const AUTH_SUCCESS = "AUTH_SUCCESS";
class AuthSuccessAction implements Action {
public readonly type = AUTH_SUCCESS;
constructor(public jwt: string) {}
}
export type AuthAction = AuthSuccessAction;

View File

@@ -0,0 +1,27 @@
import { Reducer } from "react";
import { AUTH_SUCCESS, AuthAction } from "./actions";
export interface IAuthState {
jwt: string | null;
inProgress: boolean;
}
const defaultAuthState: IAuthState = {
jwt: null,
inProgress: false,
};
export const auth: Reducer<IAuthState, AuthAction> = (
state: IAuthState = defaultAuthState,
action: AuthAction,
) => {
switch (action.type) {
case AUTH_SUCCESS:
return { ...state, jwt: action.jwt, inProgress: false };
break;
default:
return state;
break;
}
};

View File

@@ -0,0 +1,4 @@
import { combineReducers } from "redux";
import { auth } from "~redux/auth/reducer";
export const rootReducer = combineReducers({ auth });

View File

@@ -0,0 +1,4 @@
import { createStore } from "redux";
import { rootReducer } from "~redux/reducers";
export const store = createStore(rootReducer);

View File

23
frontend/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"lib": [
"es2017",
"dom"
],
"jsx": "react",
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitAny": true,
"baseUrl": "./src",
"paths": {
"~*": [
"./*"
]
}
}
}

24
ormconfig.ci.json Normal file
View File

@@ -0,0 +1,24 @@
{
"type": "mariadb",
"host": "mariadb",
"port": 3306,
"username": "writer",
"password": "writer",
"database": "writer_test",
"synchronize": true,
"logging": false,
"entities": [
"src/entity/**/*.ts"
],
"migrations": [
"src/migration/**/*.ts"
],
"subscribers": [
"src/subscriber/**/*.ts"
],
"cli": {
"entitiesDir": "src/entity",
"migrationsDir": "src/migration",
"subscribersDir": "src/subscriber"
}
}

24
ormconfig.example.json Normal file
View File

@@ -0,0 +1,24 @@
{
"type": "mariadb",
"host": "localhost",
"port": 3306,
"username": "writer",
"password": "writer",
"database": "writer_test",
"synchronize": true,
"logging": false,
"entities": [
"src/entity/**/*.ts"
],
"migrations": [
"src/migration/**/*.ts"
],
"subscribers": [
"src/subscriber/**/*.ts"
],
"cli": {
"entitiesDir": "src/entity",
"migrationsDir": "src/migration",
"subscribersDir": "src/subscriber"
}
}

4531
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
package.json Normal file
View File

@@ -0,0 +1,56 @@
{
"name": "writer-backend",
"devDependencies": {
"@blueprintjs/tslint-config": "^1.7.0",
"@types/bcrypt": "^3.0.0",
"@types/chai": "^4.1.7",
"@types/eslint": "^4.16.5",
"@types/eslint-plugin-prettier": "^2.2.0",
"@types/jsonwebtoken": "^8.3.0",
"@types/koa": "^2.0.48",
"@types/koa-logger": "^3.1.1",
"@types/koa-router": "^7.0.35",
"@types/lodash": "^4.14.119",
"@types/mocha": "^5.2.5",
"@types/mysql": "^2.15.5",
"@types/node": "^10.12.18",
"@types/prettier": "^1.15.2",
"chai": "^4.2.0",
"concurrently": "^4.1.0",
"cross-env": "^5.2.0",
"eslint": "^5.11.1",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^3.3.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-prettier": "^3.0.1",
"eslint-plugin-react": "^7.12.0",
"mocha": "^5.2.0",
"prettier": "^1.15.3",
"ts-node": "7.0.1",
"ts-node-dev": "^1.0.0-pre.32",
"tsconfig-paths": "^3.7.0",
"tslint": "^5.12.0",
"tslint-config-prettier": "^1.17.0",
"tslint-no-unused-expression-chai": "^0.1.4",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "3.2.2"
},
"dependencies": {
"bcrypt": "^3.0.3",
"jsonwebtoken": "^8.4.0",
"koa": "^2.6.2",
"koa-logger": "^3.2.0",
"koa-router": "^7.4.0",
"lodash": "^4.17.11",
"mysql": "^2.16.0",
"reflect-metadata": "^0.1.12",
"typeorm": "0.2.9"
},
"scripts": {
"ts-node-dev": "ts-node-dev -r tsconfig-paths/register src/server.ts",
"frontend": "cd frontend && npm start",
"dev": "cross-env NODE_ENV=development concurrently npm:ts-node-dev npm:frontend",
"test": "cross-env NODE_ENV=test mocha --timeout 15000 -r ts-node/register -r tsconfig-paths/register 'tests/**/*.ts' "
}
}

8
src/app.ts Normal file
View File

@@ -0,0 +1,8 @@
import "reflect-metadata";
import * as Koa from "koa";
import * as logger from "koa-logger";
export const app = new Koa();
app.use(logger());

8
src/config/database.ts Normal file
View File

@@ -0,0 +1,8 @@
import "~entity/User";
import { Connection, createConnection } from "typeorm";
import { config } from "~config";
export async function connect(): Promise<Connection> {
return createConnection(config.dbConnectionOptions);
}

33
src/config/index.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as fs from "fs";
import { ConnectionOptions } from "typeorm";
export interface IConfig {
port: number;
jwtSecret: string;
dbConnectionOptions: ConnectionOptions | null;
}
const production: IConfig = {
port: parseInt(process.env.PORT, 10) || 3000,
jwtSecret: process.env.JWT_SECRET,
dbConnectionOptions: null,
};
const development: IConfig = {
...production,
jwtSecret: "DEVSECRET",
};
const test: IConfig = {
...production,
jwtSecret: "TESTSECRET",
dbConnectionOptions: process.env.CI
? JSON.parse(fs.readFileSync("./ormconfig.ci.json").toString())
: JSON.parse(fs.readFileSync("./ormconfig.test.json").toString()),
};
const envs: { [key: string]: IConfig } = { production, development, test };
const env = process.env.NODE_ENV;
const currentConfig = envs[env];
export { currentConfig as config };

61
src/entity/User.ts Normal file
View File

@@ -0,0 +1,61 @@
import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken";
import {
BaseEntity,
Column,
Entity,
Index,
PrimaryGeneratedColumn,
} from "typeorm";
import { config } from "~config";
export type IUserJSON = Pick<User, "id" | "username">;
export interface IUserJWT extends IUserJSON {
ext: number;
iat: number;
}
export interface IUserAuthJSON extends IUserJSON {
jwt: string;
}
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
public id: number;
@Column()
@Index({ unique: true })
public username: string;
@Column()
public passwordHash: string;
constructor(username: string) {
super();
this.username = username;
}
public async verifyPassword(password: string) {
return bcrypt.compare(password, this.passwordHash);
}
public async setPassword(password: string) {
this.passwordHash = await bcrypt.hash(password, 10);
}
public toJSON(): IUserJSON {
const { id, username } = this;
return { id, username };
}
public toAuthJSON(): IUserAuthJSON {
const { id, username } = this;
return { id, username, jwt: this.toJWT() };
}
public toJWT() {
return jwt.sign(this.toJSON(), config.jwtSecret, { expiresIn: "31d" });
}
}

11
src/server.ts Normal file
View File

@@ -0,0 +1,11 @@
import { app } from "~app";
import { config } from "~config";
import { connect } from "~config/database";
connect()
.then(async connection => {
console.log(`connected to ${connection.name}`);
app.listen(config.port);
console.log(`listening at ${config.port}`);
})
.catch(error => console.log(error));

View File

@@ -0,0 +1,30 @@
import { connect } from "config/database";
import { getConnection } from "typeorm";
import { ISeed, seedDB } from "./util";
let seed: ISeed;
describe("users", () => {
before(async () => {
await connect();
});
after(async () => {
await getConnection().close();
});
beforeEach(async () => {
seed = await seedDB();
});
it("should get user", async () => {});
it("should login user", async () => {});
it("should not login user with wrong password", async () => {});
it("should signup user", async () => {});
it("should not signup user with duplicate username", async () => {});
});

20
tests/integration/util.ts Normal file
View File

@@ -0,0 +1,20 @@
import { User } from "entity/User";
export interface ISeed {
user1: User;
user2: User;
}
export async function seedDB(): Promise<ISeed> {
await User.remove(await User.find());
const user1 = new User("User1");
await user1.setPassword("User1");
await user1.save();
const user2 = new User("User2");
await user2.setPassword("User2");
await user2.save();
return { user1, user2 };
}

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"lib": [
"es2017"
],
"target": "es6",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "./dist",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"sourceMap": true,
"noImplicitAny": true,
"baseUrl": "./src",
"paths": {
"~*": [
"./*"
]
}
},
"exclude": [
"frontend"
]
}

15
tslint.json Normal file
View File

@@ -0,0 +1,15 @@
{
"extends": [
"@blueprintjs/tslint-config",
"tslint-plugin-prettier",
"tslint-no-unused-expression-chai"
],
"rules": {
"prettier": true,
"no-console": false,
"object-literal-sort-keys": false,
"no-implicit-dependencies": false,
"no-submodule-imports": false,
"no-this-assignment": false
}
}

16
writer.code-workspace Normal file
View File

@@ -0,0 +1,16 @@
{
"folders": [
{
"path": "."
},
{
"path": "frontend"
}
],
"settings": {
"typescriptHero.imports.stringQuoteStyle": "\"",
"files.exclude": {
"**/node_modules": true
}
}
}