separate client and server apps

This commit is contained in:
Joseph Ditton 2021-11-16 19:14:46 -07:00
parent e5f6840013
commit cba40b6aff
32 changed files with 3910 additions and 178 deletions

View File

@ -14,11 +14,12 @@ module.exports = {
node: true, node: true,
jest: true, jest: true,
}, },
ignorePatterns: ['.eslintrc.js'], ignorePatterns: ['.eslintrc.js', '.babelrc'],
rules: { rules: {
'@typescript-eslint/interface-name-prefix': 'off', '@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-empty-function': 'off'
}, },
}; };

5
.gitignore vendored
View File

@ -1,6 +1,7 @@
# compiled output # compiled output
/dist dist
/node_modules static
node_modules
# Logs # Logs
logs logs

View File

@ -1,12 +1,49 @@
# Nest Starter App # Nest Starter App
## Description ## Description
A starter app with LTI1p0 enabled for NestJS A starter app with Postgres, NestJS, and React
## Prerequisites
### asdf-vm
Tool versions are managed using `asdf-vm`. You will need to have `asdf-vm` installed first.
## Setup ## Setup
### Tool versions
Install the tool versions by running
```bash
$ asdf install
```
### Install yarn
We will use `yarn` instead of `npm` for package managment
```bash
$ npm install -g yarn
```
### .env
Create a file in the root called `.env` and copy the contents of `.env.example` Create a file in the root called `.env` and copy the contents of `.env.example`
## SSL ### Dependencies
To install the dependencies run
```bash
$ yarn # this is same thing as `yarn install`
```
### Database
Create the database
```bash
$ pc_ctl start # this starts postgres
$ createdb neststarterappdevelopement # creates a postgres database
```
Run the migrations
```bash
yarn db:migrate
```
Migrations need to be run again everytime a new migration is created
### SSL
Create a ssl key and certificate an place them in the root directory Create a ssl key and certificate an place them in the root directory
```bash ```bash
@ -14,48 +51,28 @@ $ openssl req -x509 -newkey rsa:4096 -keyout private-key.pem -out public-cert.pe
``` ```
Where this key will only be used for development you can leave all of the information blank. Where this key will only be used for development you can leave all of the information blank.
## Installation
```bash
$ npm install
```
## Running the app ## Running the app
```bash ```bash
# development # development
$ npm run start $ yarn start
# watch mode # watch mode
$ npm run start:dev $ yarn start:dev
# production mode # production mode
$ npm run start:prod $ yarn start:prod
``` ```
## Test ## Test
```bash ```bash
# unit tests # unit tests
$ npm run test $ yarn test
# e2e tests # e2e tests
$ npm run test:e2e $ yarn test:e2e
# test coverage # test coverage
$ npm run test:cov $ yarn test:cov
``` ```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

39
client/app.jsx Normal file
View File

@ -0,0 +1,39 @@
import { useState } from 'react';
import { setConstantValue } from 'typescript';
const App = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const submit = () => {
fetch('/sign_in', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
};
return (
<>
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="button" onClick={submit}>Login</button>
</>
);
};
export default App;

5
client/index.js Normal file
View File

@ -0,0 +1,5 @@
import ReactDOM from 'react-dom';
import App from './app';
const app = document.getElementById('app');
ReactDOM.render(<App />, app);

13
client/package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "nestclientstarterapp-client",
"version": "1.0.0",
"description": "",
"scripts": {},
"author": "",
"license": "ISC",
"devDependencies": {},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}

45
client/yarn.lock Normal file
View File

@ -0,0 +1,45 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
loose-envify@^1.1.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
react-dom@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler "^0.20.2"
react@^17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"

View File

@ -1,4 +1,4 @@
{ {
"collection": "@nestjs/schematics", "collection": "@nestjs/schematics",
"sourceRoot": "src" "sourceRoot": "server"
} }

View File

@ -6,28 +6,40 @@
"private": true, "private": true,
"license": "UNLICENSED", "license": "UNLICENSED",
"scripts": { "scripts": {
"start:db": "pg_ctl status || pg_ctl start", "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js --config src/database/cli_config.ts",
"stop:db": "pg_ctl status && pg_ctl stop", "db:start": "pg_ctl status || pg_ctl start",
"db:stop": "pg_ctl status && pg_ctl stop",
"db:migration:create": "cd server/migrations && typeorm migration:create -n ",
"db:migrate": "yarn db:start && yarn typeorm migration:run",
"db:migrate:undo": "yarn db:start && yarn typeorm migration:revert",
"prebuild": "rimraf dist", "prebuild": "rimraf dist",
"build": "nest build", "build": "nest build && yarn client:build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "format": "prettier --write \"server/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start", "start": "nest start",
"start:dev": "yarn start:db && nest start --watch", "start:dev": "yarn db:start && nest start --watch",
"start:debug": "yarn start:db && nest start --debug --watch", "start:debug": "yarn db:start && nest start --debug --watch",
"start:prod": "node dist/main", "start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint": "eslint \"{server,apps,libs,test}/**/*.ts\" --fix",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",
"test:cov": "jest --coverage", "test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json" "test:e2e": "jest --config ./test/jest-e2e.json",
"client:build": "parcel build ./client/index.js --dist-dir static",
"client:watch": "parcel watch ./client/index.js --dist-dir static"
}, },
"dependencies": { "dependencies": {
"@nestjs/common": "^8.0.0", "@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0", "@nestjs/core": "^8.0.0",
"@nestjs/platform-express": "^8.0.0", "@nestjs/platform-express": "^8.0.0",
"@nestjs/serve-static": "^2.2.2",
"@nestjs/typeorm": "^8.0.2", "@nestjs/typeorm": "^8.0.2",
"bcrypt": "^5.0.1",
"cookie-parser": "^1.4.6",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"hbs": "^4.1.2",
"jsonwebtoken": "^8.5.1",
"morgan": "^1.10.0",
"pg": "^8.7.1", "pg": "^8.7.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
@ -38,6 +50,7 @@
"@nestjs/cli": "^8.0.0", "@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0", "@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0", "@nestjs/testing": "^8.0.0",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.13", "@types/express": "^4.17.13",
"@types/jest": "^27.0.1", "@types/jest": "^27.0.1",
"@types/node": "^16.0.0", "@types/node": "^16.0.0",
@ -48,10 +61,10 @@
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"jest": "^27.0.6", "jest": "^27.0.6",
"prettier": "^2.3.2", "parcel": "^2.0.1",
"prettier": "^2.4.1",
"supertest": "^6.1.3", "supertest": "^6.1.3",
"ts-jest": "^27.0.3", "ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1", "tsconfig-paths": "^3.10.1",
"typescript": "^4.3.5" "typescript": "^4.3.5"
@ -62,7 +75,7 @@
"json", "json",
"ts" "ts"
], ],
"rootDir": "src", "rootDir": "server",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"

8
server/app.controller.ts Normal file
View File

@ -0,0 +1,8 @@
import { Controller, Get, Render } from '@nestjs/common';
@Controller()
export class AppController {
@Get()
@Render('index')
index() {}
}

13
server/app.module.ts Normal file
View File

@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { config } from './database/config';
import { UsersModule } from './modules/users.module';
@Module({
imports: [TypeOrmModule.forRoot(config), UsersModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@ -0,0 +1,27 @@
import { Body, Controller, Post, Res } from '@nestjs/common';
import { UsersService } from 'server/providers/services/users.service';
import { SignInDto } from '../dto/sign_in.dto';
import { Response } from 'express';
// this is kind of a misnomer because we are doing token based auth
// instead of session based auth
@Controller()
export class SessionsController {
constructor(private usersService: UsersService) {}
@Post('/sign_in')
async signIn(@Body() body: SignInDto, @Res() res: Response) {
console.log("DO I GET RAN?")
const verified = await this.usersService.verify(
body.username,
body.password,
);
if (!verified) {
res.status(400);
console.log("here too??")
res.json({ message: 'Invalid email or password' });
return;
}
res.json({ success: true });
}
}

View File

@ -0,0 +1,22 @@
import '../env';
export = process.env.NODE_ENV === 'development'
? {
type: 'postgres',
host: 'localhost',
port: 5432,
database: process.env.DATABASE_URL,
autoLoadEntities: true,
migrations: ['src/database/migrations/*.ts'],
cli: {
migrationsDir: 'src/database/migrations',
},
}
: {
url: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: true },
migrations: ['src/database/migrations/*.ts'],
cli: {
migrationsDir: 'src/database/migrations',
},
};

16
server/database/config.ts Normal file
View File

@ -0,0 +1,16 @@
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import '../env';
export const config: TypeOrmModuleOptions =
process.env.NODE_ENV === 'development'
? {
type: 'postgres',
host: 'localhost',
port: 5432,
database: process.env.DATABASE_URL,
autoLoadEntities: true,
}
: {
url: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: true },
};

View File

@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class AddUser1637028716848 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'user',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
},
{
name: 'name',
type: 'text',
isNullable: false,
},
{
name: 'password_hash',
type: 'text',
isNullable: false,
},
{
name: 'email',
type: 'text',
isNullable: false,
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('user');
}
}

View File

@ -0,0 +1,4 @@
export class SignInDto {
username: string;
password: string;
}

View File

@ -0,0 +1,16 @@
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
email: string;
@Column()
name: string;
@Column()
password_hash: string;
}

2
server/env.ts Normal file
View File

@ -0,0 +1,2 @@
import { config } from 'dotenv';
config();

26
server/main.ts Normal file
View File

@ -0,0 +1,26 @@
import './env';
import * as fs from 'fs';
import { NestFactory } from '@nestjs/core';
import { join } from 'path';
import * as morgan from 'morgan';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
let httpsOptions;
if (process.env.NODE_ENV === 'development') {
httpsOptions = {
key: fs.readFileSync('./private-key.pem'),
cert: fs.readFileSync('./public-cert.pem'),
};
}
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
httpsOptions,
});
app.use(morgan('tiny'));
app.useStaticAssets(join(__dirname, '..', 'static'));
app.setBaseViewsDir(join(__dirname, '../', 'views'));
app.setViewEngine('hbs');
await app.listen(process.env.PORT);
}
bootstrap();

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from 'server/entities/user.entity';
import { SessionsController } from '../controllers/sessions.controller';
import { UsersService } from '../providers/services/users.service';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [SessionsController],
providers: [UsersService],
})
export class UsersModule {}

View File

@ -0,0 +1,27 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import bcrypt from 'bcrypt';
import { User } from '../../entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRespository: Repository<User>,
) {}
findBy(options: Record<string, any>) {
return this.usersRespository.findOne(options);
}
find(id: number) {
return this.usersRespository.findOne(id);
}
async verify(email: string, password: string) {
const user = await this.usersRespository.findOne({ email });
if (!user) return false;
return bcrypt.compare(password, user.password_hash);
}
}

View File

@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

View File

@ -1,22 +0,0 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot(process.env.NODE_ENV === 'development' ? {
type: 'postgres',
host: 'localhost',
port: 5432,
database: process.env.DATABASE_URL,
autoLoadEntities: true,
} : {
url: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: true }
})
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@ -1,21 +0,0 @@
import { config } from 'dotenv';
import * as fs from 'fs';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
config();
async function bootstrap() {
let httpsOptions;
if (process.env.NODE_ENV === 'development') {
httpsOptions = {
key: fs.readFileSync('./private-key.pem'),
cert: fs.readFileSync('./public-cert.pem'),
}
}
const app = await NestFactory.create(AppModule, {
httpsOptions
});
await app.listen(process.env.PORT);
}
bootstrap();

9
views/index.hbs Normal file
View File

@ -0,0 +1,9 @@
<html>
<head>
</head>
<body>
<div id="app" />
<script src="index.js"></script>
</body>
</html>

3595
yarn.lock

File diff suppressed because it is too large Load Diff