Skip to content

Commit

Permalink
chore(backend): setup and implement rest resources
Browse files Browse the repository at this point in the history
  • Loading branch information
tericcabrel committed Jun 3, 2024
1 parent 4c09641 commit b6d931b
Show file tree
Hide file tree
Showing 98 changed files with 283 additions and 2,911 deletions.
1 change: 0 additions & 1 deletion apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ ADMIN_PASSWORD=nwHSvXuQxjey43Bp
CONVERTKIT_API_KEY=convertKitApiKey
CONVERTKIT_FORM_ID=formId
CONVERTKIT_TAG_ID=tagId
REQUEST_TIMEOUT=30000
GITHUB_CLIENT_ID=<github_client_id_here>
GITHUB_CLIENT_SECRET=<github_client_secret_here>
WEB_APP_URL=http:https://localhost:7500
Expand Down
13 changes: 0 additions & 13 deletions apps/backend/src/app.controller.ts

This file was deleted.

4 changes: 2 additions & 2 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { DomainModule } from '@snipcode/domain';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { EnvironmentVariables, validate } from './configs/environment';
import { AppController } from './features/app/app.controller';
import { AppService } from './features/app/app.service';
import { AuthFeatureModule } from './features/auth/auth.module';
import { FolderFeatureModule } from './features/folders/folder.module';
import { SnippetFeatureModule } from './features/snippets/snippet.module';
Expand Down
27 changes: 0 additions & 27 deletions apps/backend/src/app.service.ts

This file was deleted.

7 changes: 7 additions & 0 deletions apps/backend/src/configs/environment.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { z } from 'zod';

const EnvironmentVariablesSchema = z.object({
ADMIN_PASSWORD: z.string(),
APP_VERSION: z.string(),
CONVERTKIT_API_KEY: z.string(),
CONVERTKIT_FORM_ID: z.string(),
CONVERTKIT_TAG_ID: z.string(),
DATABASE_URL: z.string(),
GITHUB_CLIENT_ID: z.string(),
GITHUB_CLIENT_SECRET: z.string(),
HOST: z.string(),
INTROSPECTION_ENABLED: z.boolean({ coerce: true }),
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
PORT: z.number({ coerce: true }).min(7000).max(8000),
SENTRY_DSN: z.string(),
SENTRY_ENABLED: z.boolean({ coerce: true }),
SESSION_LIFETIME: z.number({ coerce: true }).min(1),
SNIPPET_RENDERER_API_URL: z.string(),
WEB_APP_URL: z.string(),
WEB_AUTH_ERROR_URL: z.string(),
WEB_AUTH_SUCCESS_URL: z.string(),
});

export type EnvironmentVariables = z.infer<typeof EnvironmentVariablesSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ describe('AppController', () => {
});

describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
it('should return "Hello from Snipcode!"', () => {
expect(appController.helloWorld()).toBe('Hello from Snipcode!');
});
});
});
11 changes: 11 additions & 0 deletions apps/backend/src/features/app/app.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Controller, Get } from '@nestjs/common';

@Controller()
export class AppController {
constructor() {}

@Get()
async helloWorld(): Promise<string> {
return 'Hello from Snipcode!';
}
}
28 changes: 28 additions & 0 deletions apps/backend/src/features/app/app.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { RoleService, UserService } from '@snipcode/domain';

import { EnvironmentVariables } from '../../configs/environment';

@Injectable()
export class AppService implements OnModuleInit {
constructor(
private readonly configService: ConfigService<EnvironmentVariables, true>,
private readonly userService: UserService,
private readonly roleService: RoleService,
) {}

async onModuleInit(): Promise<void> {
await this.roleService.loadRoles();

const adminRole = await this.roleService.findByName('admin');

if (!adminRole) {
throw new Error('[Data Loader]: Role administrator not found');
}

const adminPassword = this.configService.get<string>('ADMIN_PASSWORD');

await this.userService.loadAdminUser(adminRole, adminPassword);
}
}
4 changes: 3 additions & 1 deletion apps/backend/src/features/auth/auth.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';

import { AuthResolvers } from './graphql/auth.resolvers';
import { AuthController } from './rest/auth.controller';
import { GithubService } from './services/github.service';

@Module({
providers: [AuthResolvers],
providers: [AuthResolvers, GithubService, AuthController],
})
export class AuthFeatureModule {}
87 changes: 87 additions & 0 deletions apps/backend/src/features/auth/rest/auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Controller, Get, Logger, Query, Res } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
CreateSessionInput,
CreateUserRootFolderInput,
FolderService,
RoleService,
SessionService,
UserService,
} from '@snipcode/domain';
import { addDayToDate, errors } from '@snipcode/utils';
import { Response } from 'express';

import { EnvironmentVariables } from '../../../configs/environment';
import { AUTH_SUCCESS_URL } from '../../../utils/constants';
import { GithubService } from '../services/github.service';

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

constructor(
private readonly configService: ConfigService<EnvironmentVariables, true>,
private readonly githubService: GithubService,
private readonly userService: UserService,
private readonly roleService: RoleService,
private readonly folderService: FolderService,
private readonly sessionService: SessionService,
) {}

@Get('github/callback')
async authenticateWithGitHub(@Query('code') requestToken: string, @Res() res: Response): Promise<void> {
const sessionLifetime = this.configService.get<number>('SESSION_LIFETIME');
const webAuthSuccessUrl = this.configService.get<string>('WEB_AUTH_SUCCESS_URL');
const webAuthErrorUrl = this.configService.get<string>('WEB_AUTH_ERROR_URL');

const authResponse = await this.githubService.requestAccessTokenFromCode(requestToken);

const { access_token } = authResponse.data;

const userResponse = await this.githubService.retrieveGitHubUserData(access_token);

const userExist = await this.userService.findByEmail(userResponse.data.email);

if (userExist) {
const sessionInput = new CreateSessionInput({
expireDate: addDayToDate(sessionLifetime),
userId: userExist.id,
});
const session = await this.sessionService.create(sessionInput);

const updateUserInput = this.githubService.generateUserUpdateInputFromGitHubData(userExist, userResponse.data);

await this.userService.update(userExist, updateUserInput);

return res.redirect(AUTH_SUCCESS_URL(webAuthSuccessUrl, session.token));
}

const roleUser = await this.roleService.findByName('user');

if (!roleUser) {
this.logger.error(`GitHub Authentication: ${errors.ROLE_USER_NOT_FOUND}`);

return res.redirect(webAuthErrorUrl);
}

const createUserInput = this.githubService.generateUserRegistrationInputFromGitHubData(
userResponse.data,
roleUser.id,
);

const createdUser = await this.userService.create(createUserInput);

const createUserRootFolderInput = new CreateUserRootFolderInput(createdUser.id);

await this.folderService.createUserRootFolder(createUserRootFolderInput);

const sessionInput = new CreateSessionInput({
expireDate: addDayToDate(sessionLifetime),
userId: createdUser.id,
});

const session = await this.sessionService.create(sessionInput);

return res.redirect(AUTH_SUCCESS_URL(webAuthSuccessUrl, session.token));
}
}
75 changes: 75 additions & 0 deletions apps/backend/src/features/auth/services/github.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CreateUserInput, UpdateUserInput, User } from '@snipcode/domain';
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';

import { EnvironmentVariables } from '../../../configs/environment';
import { GitHubUserResponse } from '../types';

const GITHUB_AUTH_URL = 'https://github.com/login/oauth/access_token';
const GITHUB_API_USER_PROFILE_URL = 'https://api.github.com/user';

@Injectable()
export class GithubService {
private httpClient: AxiosInstance = axios.create();

constructor(private readonly configService: ConfigService<EnvironmentVariables, true>) {}

async requestAccessTokenFromCode(code: string) {
const authQueryObject = {
client_id: this.configService.get('GITHUB_CLIENT_ID'),
client_secret: this.configService.get('GITHUB_CLIENT_SECRET'),
code,
};
const requestConfig: AxiosRequestConfig = {
headers: {
accept: 'application/json',
},
};
const requestBody = {};

const authQueryString = new URLSearchParams(Object.entries(authQueryObject)).toString();

return this.httpClient.post(`${GITHUB_AUTH_URL}?${authQueryString}`, requestBody, requestConfig);
}

async retrieveGitHubUserData(accessToken: string) {
const requestConfig: AxiosRequestConfig = {
headers: {
Authorization: `token ${accessToken}`,
},
};

return this.httpClient.get<GitHubUserResponse>(GITHUB_API_USER_PROFILE_URL, requestConfig);
}

generateUserRegistrationInputFromGitHubData = (data: GitHubUserResponse, roleId: string): CreateUserInput => {
const { avatar_url, email, login, name } = data;

const createUserInput = new CreateUserInput({
email,
name,
oauthProvider: 'github',
pictureUrl: avatar_url,
roleId,
timezone: null,
username: login,
});

createUserInput.isEnabled = true;

return createUserInput;
};

generateUserUpdateInputFromGitHubData = (user: User, data: GitHubUserResponse): UpdateUserInput => {
const { avatar_url, name } = data;

return new UpdateUserInput({
name,
oauthProvider: 'github',
pictureUrl: avatar_url,
roleId: user.roleId,
timezone: user.timezone,
});
};
}
File renamed without changes.
28 changes: 28 additions & 0 deletions apps/backend/src/features/snippets/rest/snippet.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Controller, Get, Param } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { SnippetService } from '@snipcode/domain';
import { OEmbedResult, generateOembedMetadata } from '@snipcode/embed';

import { EnvironmentVariables } from '../../../configs/environment';

@Controller('snippets')
export class SnippetController {
constructor(
private readonly configService: ConfigService<EnvironmentVariables, true>,
private readonly snippetService: SnippetService,
) {}

@Get(':id/oembed')
async generateOembed(@Param('id') id: string): Promise<OEmbedResult> {
const snippet = await this.snippetService.findById(id);

return generateOembedMetadata({
snippet: {
id: snippet.id,
name: snippet.name,
},
snippetRendererURL: this.configService.get('SNIPPET_RENDERER_API_URL'),
webAppURL: this.configService.get('WEB_APP_URL'),
});
}
}
3 changes: 2 additions & 1 deletion apps/backend/src/features/snippets/snippet.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Module } from '@nestjs/common';

import { SnippetResolvers } from './graphql/snippet.resolvers';
import { SnippetController } from './rest/snippet.controller';

@Module({
providers: [SnippetResolvers],
providers: [SnippetResolvers, SnippetController],
})
export class SnippetFeatureModule {}
9 changes: 8 additions & 1 deletion apps/backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as Sentry from '@sentry/node';
import { AppModule } from './app.module';
import { ApplicationExceptionFilter } from './configs/exception.filter';
import { EnvironmentVariables } from './types/common';
import { CORS_APOLLO_STUDIO_URL } from './utils/constants';

const bootstrap = async () => {
const app = await NestFactory.create(AppModule);
Expand All @@ -16,9 +17,15 @@ const bootstrap = async () => {

Sentry.setupNestErrorHandler(app, new ApplicationExceptionFilter(httpAdapter));

const configService = app.get(ConfigService<EnvironmentVariables, true>);

app.enableCors({
credentials: true,
origin: [configService.get('WEB_APP_URL'), CORS_APOLLO_STUDIO_URL],
});

app.enableShutdownHooks();

const configService = app.get(ConfigService<EnvironmentVariables, true>);
const logger = new Logger('NestApplication');

const port = configService.get<number>('PORT');
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ export const NEWSLETTER_SUBSCRIBE_SUCCESS = 'Subscribed to the newsletter succes

export const AUTH_USER_NOT_FOUND = 'The authenticated user not found';
export const AUTH_USER_NOT_FOUND_CODE = 'AUTH_USER_NOT_FOUND';

export const AUTH_SUCCESS_URL = (webAuthSuccessUrl: string, sessionToken: string): string => {
return `${webAuthSuccessUrl}?token=${sessionToken}`;
};
Loading

0 comments on commit b6d931b

Please sign in to comment.