diff --git a/.env.example b/.env.example index 82059dc..2e8b00d 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,10 @@ PORT=3000 NODE_ENV=development +USE_SSL=false # in production this will be the full url # but in dev it is the name of the database -DATABASE_URL=neststarterappdevelopement +DATABASE_URL=neststarterappdevelopment # recommend using randomkeygen.com to generate a key ENCRYPTION_KEY=yourencryptionkey diff --git a/README.md b/README.md index b069c1f..edb27da 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,21 @@ $ npm install -g yarn Create a file in the root called `.env` and copy the contents of `.env.example` ### Dependencies -To install the dependencies run +To install the server dependencies run ```bash $ yarn # this is same thing as `yarn install` ``` +To install the client dependencies run +```bash +$ cd client && yarn && cd .. +``` + ### Database Create the database ```bash -$ pc_ctl start # this starts postgres -$ createdb neststarterappdevelopement # creates a postgres database +$ pg_ctl start # starts postgres +$ createdb neststarterappdevelopment # creates a postgres database ``` Run the migrations @@ -44,24 +49,23 @@ 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 and place them in the root directory ```bash $ openssl req -x509 -newkey rsa:4096 -keyout private-key.pem -out public-cert.pem -sha256 -nodes ``` -Where this key will only be used for development you can leave all of the information blank. +Enter `US` for the country code. Where this key will only be used for development you can leave all of the rest of information blank. ## Running the app - +To start the server run ```bash -# development -$ yarn start - # watch mode $ yarn start:dev +``` -# production mode -$ yarn start:prod +To start the client run +```bash +$ yarn client:watch ``` ## Test diff --git a/client/app.jsx b/client/app.jsx index a7de3cf..e1e4c03 100644 --- a/client/app.jsx +++ b/client/app.jsx @@ -26,6 +26,8 @@ export const App = () => { setLoading(false); }, []); + // before displaying anything try getting a token using cookies, + // can display a loading screen here if desired if (loading) return null; return ( diff --git a/client/components/router.jsx b/client/components/router.jsx index fa552fc..ccdb83a 100644 --- a/client/components/router.jsx +++ b/client/components/router.jsx @@ -12,7 +12,7 @@ export const Router = () => { : } // no jwt means not logged in + element={authToken ? : } // no token means not logged in /> } /> } /> diff --git a/package.json b/package.json index 27dc29c..b30d5b9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "db:migration:create": "cd server/database/migrations && typeorm migration:create -n ", "db:migrate": "yarn db:start && yarn typeorm migration:run", "db:migrate:undo": "yarn db:start && yarn typeorm migration:revert", + "db:seed": "cd server/database && ts-node -r tsconfig-paths/register seeds.ts", "prebuild": "rimraf dist", "build": "nest build && yarn client:build", "format": "prettier --write \"server/**/*.ts\" \"test/**/*.ts\"", @@ -40,6 +41,7 @@ "dotenv": "^10.0.0", "hbs": "^4.1.2", "jsonwebtoken": "^8.5.1", + "morgan": "^1.10.0", "pg": "^8.7.1", "reflect-metadata": "^0.1.13", "rimraf": "^3.0.2", diff --git a/server/database/migrations/1638307700052-AddRoles.ts b/server/database/migrations/1638307700052-AddRoles.ts new file mode 100644 index 0000000..237fb53 --- /dev/null +++ b/server/database/migrations/1638307700052-AddRoles.ts @@ -0,0 +1,73 @@ +import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm'; + +export class AddRoles1638307700052 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: 'role', + columns: [ + { + name: 'id', + type: 'int', + isPrimary: true, + isGenerated: true, + }, + { + name: 'key', + type: 'text', + isNullable: false, + }, + ], + }), + ); + + await queryRunner.createTable( + new Table({ + name: 'user_role', + columns: [ + { + name: 'id', + type: 'int', + isPrimary: true, + isGenerated: true, + }, + { + name: 'userId', + type: 'int', + isNullable: false, + }, + { + name: 'roleId', + type: 'int', + isNullable: false, + }, + ], + }), + ); + + await queryRunner.createForeignKey( + 'user_role', + new TableForeignKey({ + columnNames: ['userId'], + referencedColumnNames: ['id'], + referencedTableName: 'user', + onDelete: 'CASCADE', + }), + ); + + await queryRunner.createForeignKey( + 'user_role', + new TableForeignKey({ + columnNames: ['roleId'], + referencedColumnNames: ['id'], + referencedTableName: 'role', + onDelete: 'CASCADE', + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable('user_role'); + await queryRunner.dropTable('role'); + } +} diff --git a/server/entities/role.entity.ts b/server/entities/role.entity.ts new file mode 100644 index 0000000..35b4ac3 --- /dev/null +++ b/server/entities/role.entity.ts @@ -0,0 +1,21 @@ +import { Entity, PrimaryGeneratedColumn, OneToMany, Column } from 'typeorm'; +import { UserRole } from './user_role.entity'; + +@Entity() +export class Role { + static ADMIN = 'admin'; + static USER = 'user'; + + // make sure add additional roles to this arraylist as it + // will be used during seeds to initiallize all roles. + static ROLES = [Role.ADMIN, Role.USER]; + + @PrimaryGeneratedColumn() + id: number; + + @Column() + key: string; + + @OneToMany(() => UserRole, (userRole) => userRole.role) + userRoles: UserRole[]; +} diff --git a/server/entities/user.entity.ts b/server/entities/user.entity.ts index 6ddbeeb..faf054f 100644 --- a/server/entities/user.entity.ts +++ b/server/entities/user.entity.ts @@ -1,5 +1,6 @@ import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { RefreshToken } from './refresh_token.entity'; +import { UserRole } from './user_role.entity'; @Entity() export class User { @@ -17,4 +18,7 @@ export class User { @OneToMany(() => RefreshToken, (token) => token.user) refreshTokens: RefreshToken[]; + + @OneToMany(() => UserRole, (userRole) => userRole.user) + userRoles: UserRole[]; } diff --git a/server/entities/user_role.entity.ts b/server/entities/user_role.entity.ts new file mode 100644 index 0000000..0a6c5c6 --- /dev/null +++ b/server/entities/user_role.entity.ts @@ -0,0 +1,15 @@ +import { Entity, PrimaryGeneratedColumn, ManyToOne } from 'typeorm'; +import { Role } from './role.entity'; +import { User } from './user.entity'; + +@Entity() +export class UserRole { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne(() => Role, (role) => role.userRoles) + role: Role; + + @ManyToOne(() => User, (user) => user.userRoles) + user: User; +} diff --git a/server/main.ts b/server/main.ts index b4f319b..a8dc0ae 100644 --- a/server/main.ts +++ b/server/main.ts @@ -1,14 +1,16 @@ import './env'; import * as fs from 'fs'; import { NestFactory } from '@nestjs/core'; +import { Logger } from '@nestjs/common'; import { join } from 'path'; import { NestExpressApplication } from '@nestjs/platform-express'; import * as cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; +import * as morgan from 'morgan'; async function bootstrap() { let httpsOptions; - if (process.env.NODE_ENV === 'development') { + if (process.env.USE_SSL === 'true') { httpsOptions = { key: fs.readFileSync('./private-key.pem'), cert: fs.readFileSync('./public-cert.pem'), @@ -18,11 +20,18 @@ async function bootstrap() { httpsOptions, logger: ['verbose'], }); - app.use(cookieParser()); app.useStaticAssets(join(__dirname, '..', 'static')); app.setBaseViewsDir(join(__dirname, '..', 'views')); app.setViewEngine('hbs'); + const logger = new Logger('Request'); + app.use( + morgan('tiny', { + stream: { + write: (message) => logger.log(message.replace('\n', '')), + }, + }), + ); await app.listen(process.env.PORT); } bootstrap(); diff --git a/server/providers/guards/roles.guard.ts b/server/providers/guards/roles.guard.ts new file mode 100644 index 0000000..e69de29 diff --git a/yarn.lock b/yarn.lock index 9de60e8..b3c2377 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1499,6 +1499,13 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bcrypt@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.0.1.tgz#f1a2c20f208e2ccdceea4433df0c8b2c54ecdf71" @@ -2054,6 +2061,11 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= +depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -4004,6 +4016,17 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4164,6 +4187,11 @@ on-finished@^2.3.0, on-finished@~2.3.0: dependencies: ee-first "1.1.1" +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"