Skip to content

Commit 287cd47

Browse files
authored
Add user authentication (#1)
1 parent 6380896 commit 287cd47

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2827
-58
lines changed

.dockerignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ Dockerfile
33
node_modules
44
npm-debug.log
55
dist
6+
.env

.eslintrc.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,29 @@ module.exports = {
1212
node: true,
1313
jest: true,
1414
},
15-
ignorePatterns: ['.eslintrc.js'],
15+
ignorePatterns: ['.eslintrc.js', 'migrations/*', 'dist/*'],
1616
rules: {
1717
'@typescript-eslint/interface-name-prefix': 'off',
1818
'@typescript-eslint/explicit-function-return-type': 'off',
1919
'@typescript-eslint/explicit-module-boundary-types': 'off',
2020
'@typescript-eslint/ban-ts-comment': 'off',
2121
'@typescript-eslint/no-explicit-any': 'off',
22+
'@typescript-eslint/no-empty-interface': 'off',
2223
'import/order': [
2324
'error',
2425
{
2526
'newlines-between': 'always-and-inside-groups',
2627
'pathGroupsExcludedImportTypes': ['builtin'],
28+
'pathGroups': [
29+
{
30+
pattern: '@src/**/**',
31+
group: 'parent',
32+
},
33+
{
34+
pattern: '@lib/**/**',
35+
group: 'parent',
36+
},
37+
],
2738
'alphabetize': {
2839
order: 'asc',
2940
caseInsensitive: true,

.gitignore

+4-1
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ lerna-debug.log*
3232
!.vscode/settings.json
3333
!.vscode/tasks.json
3434
!.vscode/launch.json
35-
!.vscode/extensions.json
35+
!.vscode/extensions.json
36+
37+
# env
38+
.env

.prettierignore

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ lib
99
out
1010

1111
*.svg
12+
migrations/*

.prettierrc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"printWidth": 100,
3-
"trailingComma": "es5",
3+
"trailingComma": "all",
44
"tabWidth": 2,
55
"semi": true,
66
"singleQuote": true,

migrations/1659985471735-AuthClaim.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class AuthClaim1659985471735 implements MigrationInterface {
4+
name = 'AuthClaim1659985471735'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE "auth_claim" (
9+
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
10+
"nonce" character varying NOT NULL,
11+
"onBehalfOf" character varying NOT NULL,
12+
"created" TIMESTAMP NOT NULL DEFAULT now(),
13+
"deleted" TIMESTAMP,
14+
"updated" TIMESTAMP NOT NULL DEFAULT now(),
15+
CONSTRAINT "PK_b0d640283a501763c9f7e6e942d" PRIMARY KEY ("id")
16+
)
17+
`);
18+
}
19+
20+
public async down(queryRunner: QueryRunner): Promise<void> {
21+
await queryRunner.query(`
22+
DROP TABLE "auth_claim"
23+
`);
24+
}
25+
26+
}

migrations/1660000538731-Auth.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class Auth1660000538731 implements MigrationInterface {
4+
name = 'Auth1660000538731'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE "auth" (
9+
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
10+
"data" jsonb NOT NULL,
11+
"publicKeyStr" character varying NOT NULL,
12+
"created" TIMESTAMP NOT NULL DEFAULT now(),
13+
"deleted" TIMESTAMP,
14+
"updated" TIMESTAMP NOT NULL DEFAULT now(),
15+
CONSTRAINT "PK_7e416cf6172bc5aec04244f6459" PRIMARY KEY ("id")
16+
)
17+
`);
18+
}
19+
20+
public async down(queryRunner: QueryRunner): Promise<void> {
21+
await queryRunner.query(`
22+
DROP TABLE "auth"
23+
`);
24+
}
25+
26+
}

migrations/1660048272853-User.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class User1660048272853 implements MigrationInterface {
4+
name = 'User1660048272853'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`
8+
CREATE TABLE "user" (
9+
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
10+
"data" jsonb NOT NULL,
11+
"authId" character varying NOT NULL,
12+
"publicKeyStr" character varying NOT NULL,
13+
"created" TIMESTAMP NOT NULL DEFAULT now(),
14+
"deleted" TIMESTAMP,
15+
"updated" TIMESTAMP NOT NULL DEFAULT now(),
16+
CONSTRAINT "UQ_ad5065ee18a722baaa932d1c3c6" UNIQUE ("authId"),
17+
CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id")
18+
)
19+
`);
20+
}
21+
22+
public async down(queryRunner: QueryRunner): Promise<void> {
23+
await queryRunner.query(`
24+
DROP TABLE "user"
25+
`);
26+
}
27+
28+
}

orm-cli-config.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as path from 'path';
2+
3+
import * as dotenv from 'dotenv';
4+
import * as dotenvExpand from 'dotenv-expand';
5+
import { DataSource } from 'typeorm';
6+
7+
const env = dotenv.config({ path: '.env' });
8+
dotenvExpand.expand(env);
9+
10+
export default new DataSource({
11+
type: 'postgres',
12+
connectTimeoutMS: 3000,
13+
entities: [path.join(__dirname, '**', '*.entity.{ts,js}')],
14+
migrations: ['./migrations/*.ts'],
15+
migrationsTableName: 'typeorm_migrations',
16+
password: process.env.DATABASE_PASSWORD,
17+
ssl: process.env.DATABASE_USE_SSL === 'true' ? { rejectUnauthorized: false } : false,
18+
url: process.env.DATABASE_URL,
19+
username: process.env.DATABASE_USERNAME,
20+
});

package.json

+31-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
"scripts": {
99
"prebuild": "rimraf dist",
1010
"build": "nest build",
11+
"db:m:new": "ts-node -r tsconfig-paths/register --project ./tsconfig.json ./node_modules/typeorm/cli.js -d ./orm-cli-config.ts migration:generate -p ./migrations/${npm_config_name}",
12+
"db:m:revert": "ts-node -r tsconfig-paths/register --project ./tsconfig.json ./node_modules/typeorm/cli.js -d ./orm-cli-config.ts migration:revert",
13+
"db:m:run": "ts-node -r tsconfig-paths/register --project ./tsconfig.json ./node_modules/typeorm/cli.js -d ./orm-cli-config.ts migration:run",
14+
"db": "ts-node -r tsconfig-paths/register --project ./tsconfig.json ./node_modules/typeorm/cli.js -d ./orm-cli-config.ts",
1115
"fmt": "prettier --write '{*,**/*}.{js,ts,jsx,tsx,json}'",
1216
"lint": "eslint --ext .ts . && prettier --check '{*,**/*}.{js,ts,jsx,tsx,json}'",
1317
"lint:fix": "eslint --fix --ext .ts . && yarn fmt",
@@ -25,38 +29,60 @@
2529
"@nestjs/common": "^9.0.0",
2630
"@nestjs/config": "^2.2.0",
2731
"@nestjs/core": "^9.0.0",
32+
"@nestjs/graphql": "^10.0.21",
33+
"@nestjs/jwt": "^9.0.0",
34+
"@nestjs/mercurius": "^10.0.21",
35+
"@nestjs/passport": "^9.0.0",
2836
"@nestjs/platform-express": "^9.0.0",
37+
"@nestjs/platform-fastify": "^9.0.8",
38+
"@nestjs/typeorm": "^9.0.0",
39+
"@solana/web3.js": "^1.50.1",
40+
"altair-fastify-plugin": "^4.5.3",
41+
"date-fns": "^2.29.1",
42+
"fp-ts": "^2.12.2",
43+
"graphql": "^16.5.0",
44+
"graphql-upload": "13.0.0",
45+
"io-ts": "^2.2.17",
46+
"io-ts-types": "^0.5.16",
47+
"mercurius": "^9",
48+
"mercurius-upload": "^5.0.0",
49+
"passport": "^0.6.0",
50+
"passport-jwt": "^4.0.0",
51+
"pg": "^8.7.3",
2952
"reflect-metadata": "^0.1.13",
3053
"rimraf": "^3.0.2",
31-
"rxjs": "^7.2.0"
54+
"rxjs": "^7.2.0",
55+
"tweetnacl": "^1.0.3",
56+
"typeorm": "^0.3.7"
3257
},
3358
"devDependencies": {
3459
"@babel/eslint-parser": "^7.18.9",
3560
"@nestjs/cli": "^9.0.0",
3661
"@nestjs/schematics": "^9.0.0",
3762
"@nestjs/testing": "^9.0.0",
38-
"@types/eslint-plugin-prettier": "^3.1.0",
3963
"@types/eslint": "^8.4.5",
64+
"@types/eslint-plugin-prettier": "^3.1.0",
4065
"@types/express": "^4.17.13",
66+
"@types/graphql-upload": "^8.0.11",
4167
"@types/jest": "28.1.4",
4268
"@types/node": "^16.0.0",
4369
"@types/prettier": "^2.7.0",
4470
"@types/supertest": "^2.0.11",
4571
"@typescript-eslint/eslint-plugin": "^5.32.0",
4672
"@typescript-eslint/parser": "^5.32.0",
4773
"babel-eslint": "^10.1.0",
74+
"eslint": "^8.21.0",
4875
"eslint-config-prettier": "^8.5.0",
4976
"eslint-plugin-import": "^2.26.0",
5077
"eslint-plugin-prettier": "^4.2.1",
51-
"eslint": "^8.21.0",
5278
"jest": "28.1.2",
5379
"prettier": "^2.3.2",
5480
"source-map-support": "^0.5.20",
5581
"supertest": "^6.1.3",
5682
"ts-jest": "28.0.5",
5783
"ts-loader": "^9.2.3",
58-
"ts-node": "^10.0.0",
59-
"tsconfig-paths": "4.0.0",
84+
"ts-node": "^10.9.1",
85+
"tsconfig-paths": "^4.1.0",
6086
"typescript": "^4.3.5"
6187
},
6288
"jest": {

src/app.module.ts

+44-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,51 @@
1+
import { join } from 'path';
2+
13
import { Module } from '@nestjs/common';
2-
import { ConfigModule } from '@nestjs/config';
4+
import { GraphQLModule } from '@nestjs/graphql';
5+
import { MercuriusDriver, MercuriusDriverConfig } from '@nestjs/mercurius';
6+
import { TypeOrmModule } from '@nestjs/typeorm';
7+
8+
// import { NonceScalar } from '@lib/scalars/Nonce';
9+
import { PublicKeyScalar } from '@lib/scalars/PublicKey';
10+
import { AppController } from '@src/app.controller';
11+
import { AppService } from '@src/app.service';
12+
import { AuthModule } from '@src/auth/auth.module';
13+
import { ConfigModule } from '@src/config/config.module';
14+
import { ConfigService } from '@src/config/config.service';
315

4-
import { AppController } from './app.controller';
5-
import { AppService } from './app.service';
16+
import { UserModule } from './user/user.module';
617

718
@Module({
8-
imports: [ConfigModule.forRoot()],
19+
imports: [
20+
ConfigModule,
21+
GraphQLModule.forRoot<MercuriusDriverConfig>({
22+
autoSchemaFile: true,
23+
buildSchemaOptions: {
24+
dateScalarMode: 'timestamp',
25+
},
26+
driver: MercuriusDriver,
27+
resolvers: {
28+
// Nonce: NonceScalar,
29+
PublicKey: PublicKeyScalar,
30+
},
31+
sortSchema: true,
32+
}),
33+
TypeOrmModule.forRootAsync({
34+
imports: [ConfigModule],
35+
useFactory: async (configService: ConfigService) => ({
36+
type: 'postgres',
37+
autoLoadEntities: true,
38+
entities: [join(__dirname, '/**/entity{.ts,.js}')],
39+
password: configService.get('database.password'),
40+
ssl: configService.get('database.useSsl') ? { rejectUnauthorized: true } : false,
41+
url: configService.get('database.url'),
42+
username: configService.get('database.username'),
43+
}),
44+
inject: [ConfigService],
45+
}),
46+
AuthModule,
47+
UserModule,
48+
],
949
controllers: [AppController],
1050
providers: [AppService],
1151
})

src/auth/auth.jwt.guard.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { ExecutionContext } from '@nestjs/common';
2+
import { Injectable } from '@nestjs/common';
3+
import { GqlExecutionContext } from '@nestjs/graphql';
4+
import { AuthGuard } from '@nestjs/passport';
5+
import type { Request } from 'express';
6+
7+
import type { User } from '@src/user/entities/User.entity';
8+
9+
@Injectable()
10+
export class AuthJwtGuard extends AuthGuard('authJwt') {
11+
getRequest(context: ExecutionContext) {
12+
const ctx = GqlExecutionContext.create(context);
13+
return ctx.getContext().req;
14+
}
15+
}
16+
17+
export interface GuardedReq extends Request {
18+
user: User;
19+
}

src/auth/auth.jwt.strategy.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { PassportStrategy } from '@nestjs/passport';
3+
import { PublicKey } from '@solana/web3.js';
4+
import * as FN from 'fp-ts/function';
5+
import * as TE from 'fp-ts/TaskEither';
6+
import { ExtractJwt, Strategy } from 'passport-jwt';
7+
8+
import * as errors from '@lib/errors/gql';
9+
import { ConfigService } from '@src/config/config.service';
10+
import { UserService } from '@src/user/user.service';
11+
12+
import { AuthService } from './auth.service';
13+
14+
@Injectable()
15+
export class AuthJwtStrategy extends PassportStrategy(Strategy, 'authJwt') {
16+
constructor(
17+
private readonly configService: ConfigService,
18+
private readonly authService: AuthService,
19+
private readonly userService: UserService,
20+
) {
21+
super({
22+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
23+
secretOrKey: configService.get('jwt.userSecret'),
24+
});
25+
}
26+
27+
validate(payload: { sub: string }) {
28+
return FN.pipe(
29+
this.userService.getUserById(payload.sub),
30+
TE.chainW(TE.fromOption(() => new errors.Unauthorized())),
31+
TE.matchW(
32+
(error) => {
33+
throw error;
34+
},
35+
(user) => ({ ...user.data, publicKey: new PublicKey(user.publicKeyStr) }),
36+
),
37+
)();
38+
}
39+
}

src/auth/auth.module.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Module } from '@nestjs/common';
2+
import { JwtModule } from '@nestjs/jwt';
3+
import { PassportModule } from '@nestjs/passport';
4+
import { TypeOrmModule } from '@nestjs/typeorm';
5+
6+
import { ConfigModule } from '@src/config/config.module';
7+
import { ConfigService } from '@src/config/config.service';
8+
import { UserModule } from '@src/user/user.module';
9+
10+
import { AuthJwtStrategy } from './auth.jwt.strategy';
11+
import { AuthResolver } from './auth.resolver';
12+
import { AuthService } from './auth.service';
13+
import { Auth } from './entities/Auth.entity';
14+
import { AuthClaim } from './entities/AuthClaim.entity';
15+
16+
@Module({
17+
imports: [
18+
ConfigModule,
19+
PassportModule,
20+
UserModule,
21+
JwtModule.registerAsync({
22+
imports: [ConfigModule],
23+
useFactory: (configService: ConfigService) => ({
24+
secret: configService.get('jwt.userSecret'),
25+
signOptions: { expiresIn: '7d' },
26+
}),
27+
inject: [ConfigService],
28+
}),
29+
TypeOrmModule.forFeature([Auth, AuthClaim]),
30+
],
31+
providers: [AuthResolver, AuthService, AuthJwtStrategy],
32+
exports: [AuthService],
33+
})
34+
export class AuthModule {}

0 commit comments

Comments
 (0)