Skip to content

Commit

Permalink
feat: 🔥 support secrets
Browse files Browse the repository at this point in the history
support secrets
  • Loading branch information
Tal Rofe committed Jun 3, 2022
1 parent 256cfa2 commit 4d5e6e4
Show file tree
Hide file tree
Showing 54 changed files with 578 additions and 85 deletions.
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"start": "dotenv -e ./envs/.env.production -e ./prisma/.env.production nest start",
"start:dev": "dotenv -e ./envs/.env.development -e ./prisma/.env.development nest start --watch",
"start:prod": "dotenv -e ./envs/.env.production -e ./prisma/.env.production node dist/main",
"prisma-format": "prisma format",
"prisma-gen:dev": "prisma generate --schema ./prisma/schema.prisma",
"prisma-gen:prod": "prisma generate --schema ./prisma/schema.prisma",
"prisma-push:dev": "dotenv -e ./prisma/.env.development prisma db push --schema ./prisma/schema.prisma",
Expand Down
24 changes: 21 additions & 3 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,17 @@ model User {
id String @id @default(auto()) @map("_id") @db.ObjectId
name String
email String @unique
email String @unique
passwordHash String?
authType AuthType
authType AuthType
externalToken String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
RefreshToken RefreshToken[]
refreshTokens RefreshToken[]
clientSecrets ClientSecret[]
}

model RefreshToken {
Expand All @@ -40,3 +41,20 @@ model RefreshToken {
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model ClientSecret {
id String @id @default(auto()) @map("_id") @db.ObjectId
secret String
userId String @db.ObjectId
label String
expiration DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique(fields: [userId, label], name: "unique_user_labels")
@@unique(fields: [userId, secret], name: "unique_user_secrets")
}
19 changes: 18 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_GUARD, RouterModule } from '@nestjs/core';

import { validate } from './config/env.validation';
import { appRoutes } from './app.routes';
import EnvConfiguration from './config/configuration';
import { AccessTokenGuard } from './guards/access-token.guard';
import { DatabaseModule } from './modules/database/database.module';
import { UserModule } from './modules/user/user.module';
import { AccessTokenStrategy } from './strategies/access-token.strategy';

@Module({
imports: [DatabaseModule, RouterModule.register(appRoutes), UserModule],
imports: [
DatabaseModule,
RouterModule.register(appRoutes),
UserModule,
ConfigModule.forRoot({
load: [EnvConfiguration],
isGlobal: true,
cache: true,
validate,
validationOptions: {
allowUnknown: false,
abortEarly: true,
},
}),
],
providers: [
{
provide: APP_GUARD,
Expand Down
6 changes: 6 additions & 0 deletions apps/backend/src/app.routes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { Routes } from '@nestjs/core';

import { AuthModule } from './modules/user/modules/auth/auth.module';
import { SecretsModule } from './modules/user/modules/secrets/secrets.module';

export const appRoutes: Routes = [
{
path: 'user',
module: AuthModule,
},
{
path: 'user',
module: SecretsModule,
},
];
5 changes: 5 additions & 0 deletions apps/backend/src/decorators/is-nullable.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ValidationOptions, ValidateIf } from 'class-validator';

export function IsNullable(validationOptions?: ValidationOptions) {
return ValidateIf((_object, value) => value !== null, validationOptions);
}
34 changes: 34 additions & 0 deletions apps/backend/src/modules/database/client-secret.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Injectable } from '@nestjs/common';

import { PrismaService } from './prisma.service';

@Injectable()
export class DBClientSecretService {
constructor(private prisma: PrismaService) {}

public async doesSecretBelongUser(userId: string, secretId: string) {
const secretDB = await this.prisma.clientSecret.findFirst({ where: { userId, id: secretId } });

return secretDB !== null;
}

public async deleteSecret(secretId: string) {
await this.prisma.clientSecret.delete({ where: { id: secretId } });
}

public async revokeAllSecrets(userId: string) {
await this.prisma.clientSecret.deleteMany({ where: { userId } });
}

public async refreshSecret(secretId: string, newSecret: string) {
await this.prisma.clientSecret.update({ where: { id: secretId }, data: { secret: newSecret } });
}

public async editSecretLabel(secretId: string, newLabel: string) {
await this.prisma.clientSecret.update({ where: { id: secretId }, data: { label: newLabel } });
}

public async createSecret(userId: string, secret: string, label: string, expiration: Date | null) {
await this.prisma.clientSecret.create({ data: { secret, userId, label, expiration } });
}
}
5 changes: 3 additions & 2 deletions apps/backend/src/modules/database/database.module.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Global, Module } from '@nestjs/common';
import { DBClientSecretService } from './client-secret.service';

import { PrismaService } from './prisma.service';
import { DBUserService } from './user.service';

@Global()
@Module({
providers: [PrismaService, DBUserService],
exports: [DBUserService],
providers: [PrismaService, DBUserService, DBClientSecretService],
exports: [DBUserService, DBClientSecretService],
})
export class DatabaseModule {}
12 changes: 8 additions & 4 deletions apps/backend/src/modules/database/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { Prisma, User } from '@prisma/client';
import { Prisma } from '@prisma/client';

import { MAX_JWT_REFRESH_TOKENS } from '@/models/jwt-token';

Expand All @@ -16,17 +16,21 @@ export class DBUserService {
});
}

public findByEmail(email: string, select: Partial<Record<keyof User, boolean>>) {
public findByEmail(email: string, select: Prisma.UserSelect) {
return this.prisma.user.findUnique({
where: { email },
select,
});
}

public findGoogleUserByEmail(email: string) {
public findExternalUserByEmail(email: string) {
return this.prisma.user.findUnique({
where: { email },
select: { id: true, authType: true },
select: {
id: true,
authType: true,
clientSecrets: { select: { createdAt: true, expiration: true, id: true, label: true } },
},
});
}

Expand Down
20 changes: 1 addition & 19 deletions apps/backend/src/modules/user/modules/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ import { Module } from '@nestjs/common';
import { CqrsModule } from '@nestjs/cqrs';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';

import EnvConfiguration from '@/config/configuration';
import { validate } from '@/config/env.validation';

import { AuthService } from './auth.service';
import { LocalStrategy } from './strategies/local.strategy';
Expand All @@ -25,21 +21,7 @@ import { GithubController } from './github.controller';
import { DeleteController } from './delete.controller';

@Module({
imports: [
CqrsModule,
PassportModule,
ConfigModule.forRoot({
load: [EnvConfiguration],
isGlobal: true,
cache: true,
validate,
validationOptions: {
allowUnknown: false,
abortEarly: true,
},
}),
JwtModule.register({}),
],
imports: [CqrsModule, PassportModule, JwtModule.register({})],
controllers: [
LoginController,
RegisterController,
Expand Down
14 changes: 14 additions & 0 deletions apps/backend/src/modules/user/modules/auth/auth.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const Routes = {
CONTROLLER: 'auth',
DELETE: 'delete',
LOGIN: 'login',
AUTO_LOGIN: 'auto-login',
REGISTER: 'register',
REFRESH_TOKEN: 'refresh-token',
GITHUB_AUTH: 'github-auth',
GITHUB_REDIRECT: 'github-redirect',
GOOGLE_AUTH: 'google-auth',
GOOGLE_REDIRECT: 'google-redirect',
};

export default Routes;
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { CommandBus } from '@nestjs/cqrs';

import { CurrentUserId } from '@/decorators/current-user-id.decorator';
import { DeleteUserContract } from './commands/contracts/delete-user.contract';
import Routes from './auth.routes';

@Controller('auth')
@Controller(Routes.CONTROLLER)
export class DeleteController {
private readonly logger = new Logger(DeleteController.name);

constructor(private readonly commandBus: CommandBus) {}

@Delete('delete')
@Delete(Routes.DELETE)
@HttpCode(HttpStatus.OK)
public async delete(@CurrentUserId() userId: string): Promise<void> {
this.logger.log(`Will try to delete a user with an Id: "${userId}"`);
Expand Down
17 changes: 11 additions & 6 deletions apps/backend/src/modules/user/modules/auth/github.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import {
UseGuards,
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { User } from '@prisma/client';

import { Public } from '@/decorators/public.decorator';
import { ExternalAuthUser } from '@/decorators/external-auth-user.decorator';
import { IExternalAuthUser } from '@/interfaces/external-auth-user';

import { AuthService } from './auth.service';
import { AddRefreshTokenContract } from './commands/contracts/add-refresh-token.contract';
Expand All @@ -22,8 +20,11 @@ import { IGithubRedirectResponse } from './interfaces/responses';
import { GetGithubUserContract } from './queries/contracts/get-github-user.contract';
import { CreateGithubUserContract } from './queries/contracts/create-github-user.contract';
import { UpdateExternalTokenContract } from './commands/contracts/update-external-token.contract';
import Routes from './auth.routes';
import { IExternalAuthUser } from './interfaces/external-auth-user';
import { IExternalLoggedUser } from './interfaces/user';

@Controller('auth')
@Controller(Routes.CONTROLLER)
export class GithubController {
private readonly logger = new Logger(GithubController.name);

Expand All @@ -35,14 +36,14 @@ export class GithubController {

@Public()
@UseGuards(GithubAuthGuard)
@Get('github-auth')
@Get(Routes.GITHUB_AUTH)
public githubAuth() {
return;
}

@Public()
@UseGuards(GithubAuthGuard)
@Get('github-redirect')
@Get(Routes.GITHUB_REDIRECT)
@HttpCode(HttpStatus.OK)
public async githubRedirect(
@ExternalAuthUser() user: IExternalAuthUser,
Expand All @@ -51,7 +52,7 @@ export class GithubController {
`User with an email "${user.email}" tries to login. Will check if already exists in DB`,
);

const githubUser = await this.queryBus.execute<GetGithubUserContract, Pick<User, 'id' | 'authType'>>(
const githubUser = await this.queryBus.execute<GetGithubUserContract, IExternalLoggedUser>(
new GetGithubUserContract(user.email),
);

Expand Down Expand Up @@ -85,6 +86,8 @@ export class GithubController {
accessToken,
refreshToken,
name: user.name,
id: createdGithubUserId,
clientSecrets: [],
};
}

Expand Down Expand Up @@ -127,6 +130,8 @@ export class GithubController {
accessToken,
refreshToken,
name: user.name,
id: githubUser.id,
clientSecrets: githubUser.clientSecrets,
};
}
}
17 changes: 11 additions & 6 deletions apps/backend/src/modules/user/modules/auth/google.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@ import {
UseGuards,
} from '@nestjs/common';
import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { User } from '@prisma/client';

import { Public } from '@/decorators/public.decorator';
import { ExternalAuthUser } from '@/decorators/external-auth-user.decorator';
import { IExternalAuthUser } from '@/interfaces/external-auth-user';

import { AuthService } from './auth.service';
import { IGoogleRedirectResponse } from './interfaces/responses';
Expand All @@ -22,8 +20,11 @@ import { AddRefreshTokenContract } from './commands/contracts/add-refresh-token.
import { RemoveOldRefreshTokensContract } from './commands/contracts/remove-old-refresh-tokens.contract';
import { CreateGoogleUserContract } from './queries/contracts/create-google-user.contract';
import { UpdateExternalTokenContract } from './commands/contracts/update-external-token.contract';
import Routes from './auth.routes';
import { IExternalAuthUser } from './interfaces/external-auth-user';
import { IExternalLoggedUser } from './interfaces/user';

@Controller('auth')
@Controller(Routes.CONTROLLER)
export class GoogleController {
private readonly logger = new Logger(GoogleController.name);

Expand All @@ -35,14 +36,14 @@ export class GoogleController {

@Public()
@UseGuards(GoogleAuthGuard)
@Get('google-auth')
@Get(Routes.GOOGLE_AUTH)
public googleAuth() {
return;
}

@Public()
@UseGuards(GoogleAuthGuard)
@Get('google-redirect')
@Get(Routes.GOOGLE_REDIRECT)
@HttpCode(HttpStatus.OK)
public async googleRedirect(
@ExternalAuthUser() user: IExternalAuthUser,
Expand All @@ -51,7 +52,7 @@ export class GoogleController {
`User with an email "${user.email}" tries to login. Will check if already exists in DB`,
);

const googleUser = await this.queryBus.execute<GetGoogleUserContract, Pick<User, 'id' | 'authType'>>(
const googleUser = await this.queryBus.execute<GetGoogleUserContract, IExternalLoggedUser>(
new GetGoogleUserContract(user.email),
);

Expand Down Expand Up @@ -93,6 +94,8 @@ export class GoogleController {
accessToken,
refreshToken,
name: user.name,
id: createdGoogleUserId,
clientSecrets: [],
};
}

Expand Down Expand Up @@ -135,6 +138,8 @@ export class GoogleController {
accessToken,
refreshToken,
name: user.name,
id: googleUser.id,
clientSecrets: googleUser.clientSecrets,
};
}
}
Loading

0 comments on commit 4d5e6e4

Please sign in to comment.