Feature/1 add projects and about pages (#2)

* adding api

* adding projects page

* adding project page

* updating terraform

* updating terraform

* adding github workflows
This commit was merged in pull request #2.
This commit is contained in:
2025-02-23 20:45:04 +00:00
committed by GitHub
parent 2d9cd07358
commit 6deca4c7ea
95 changed files with 14643 additions and 1500 deletions

View File

@@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
describe('AppController', () => {
let appController: AppController;
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile();
appController = app.get<AppController>(AppController);
});
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
});
});
});

78
api/src/app.controller.ts Normal file
View File

@@ -0,0 +1,78 @@
import {
Controller,
Get,
Headers,
Query,
StreamableFile,
UseGuards
} from '@nestjs/common';
// import { Client as MsGraphClient } from '@microsoft/microsoft-graph-client';
import { MsGraphService } from '@noahspan/noahspan-modules'
@Controller()
export class AppController {
constructor(
private readonly msGraphService: MsGraphService
) {}
@Get('userPhoto')
async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> {
// try {
// const graphToken: string = await this.msGraphService.getMsGraphAuth(
// headers.authorization.replace('Bearer ', ''),
// ['user.read']
// );
// const client: MsGraphClient =
// await this.msGraphService.getMsGraphClientDelegated(graphToken);
// const blob: Blob = await client.api(`me/photos('48x48')/$value`).get();
// const arrayBuffer: ArrayBuffer = await blob.arrayBuffer();
// const buffer: Buffer = Buffer.from(arrayBuffer);
// return new StreamableFile(buffer, {
// type: 'application/json',
// disposition: `attachment; filename="user_photo.png"`
// });
// } catch (error) {
// return error;
// }
try {
const buffer = await this.msGraphService.getProfilePhoto(
headers.authorization.replace('Bearer ', ''),
['user.read']
)
return new StreamableFile(buffer, {
type: 'application/json',
disposition: `attachment; filename="user_photo.png"`
});
} catch (error) {
return error;
}
}
@Get('userProfile')
async getUserProfile(@Headers() headers: any): Promise<any> {
// try {
// const graphToken: string = await this.msGraphService.getMsGraphAuth(
// headers.authorization.replace('Bearer ', ''),
// ['user.read']
// );
// const client: MsGraphClient =
// await this.msGraphService.getMsGraphClientDelegated(graphToken);
// const userProfile = await client.api(`me`).get();
// return userProfile;
// } catch (error) {
// return error;
// }
try {
const userProfile = await this.msGraphService.getUserProfile(headers.authorization.replace('Bearer ', ''));
return userProfile;
} catch (error) {
return error;
}
}
}

51
api/src/app.module.ts Normal file
View File

@@ -0,0 +1,51 @@
import { Module } from '@nestjs/common';
import { APP_FILTER, APP_GUARD } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { ProjectModule } from './project/project.module';
import { AuthGuard, AuthModule, MsGraphModule } from '@noahspan/noahspan-modules'
import { ConfigModule, ConfigService } from '@nestjs/config';
import configuration from './config/configuration';
import { AppController } from './app.controller';
@Module({
imports: [
AuthModule.registerAsync({
inject: [ConfigService],
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
clientId: configService.get<string>('clientId'),
clientSecret: configService.get<string>('clientSecret'),
tenantId: configService.get<string>('tenantId')
}
}
}),
ConfigModule.forRoot({
load: [configuration]
}),
MsGraphModule.registerAsync({
inject: [ConfigService],
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
clientId: configService.get<string>('clientId'),
clientSecret: configService.get<string>('clientSecret'),
tenantId: configService.get<string>('tenantId')
}
}
}),
ProjectModule
],
controllers: [AppController],
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter
},
{
provide: APP_GUARD,
useClass: AuthGuard
}
]
})
export class AppModule {}

View File

@@ -0,0 +1,5 @@
export default () => ({
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET,
tenantId: process.env.TENANT_ID
})

View File

@@ -0,0 +1,25 @@
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(excpetion: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = excpetion.getStatus();
response.status(status).json({
name: excpetion.cause,
message: excpetion.message,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url
});
}
}

27
api/src/main.ts Normal file
View File

@@ -0,0 +1,27 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpService } from '@nestjs/axios';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { InternalServerErrorException } from '@nestjs/common';
async function bootstrap() {
const httpService = new HttpService();
const app = await NestFactory.create(AppModule);
app.enableCors();
app.setGlobalPrefix('api');
app.useGlobalFilters(new HttpExceptionFilter());
httpService.axiosRef.interceptors.response.use(
(response) => {
return response;
},
(error) => {
console.error('Internal server error exception', error);
throw new InternalServerErrorException();
}
);
await app.listen(3000);
}
bootstrap();

View File

@@ -0,0 +1,94 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put,
UseGuards
} from '@nestjs/common';
import { ProjectDto } from './project.dto';
import { Project } from './project.entity';
import { ProjectService } from './project.service';
import { CustomError, Public } from '@noahspan/noahspan-modules';
@Controller('projects')
export class ProjectController {
constructor(private readonly projectService: ProjectService) {}
@Get(':partitionKey/:rowKey')
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
try {
return await this.projectService.find(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Public()
@Get()
async findAll() {
try {
return await this.projectService.findAll();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Post()
async create(@Body() projectDto: ProjectDto) {
try {
const project = new Project();
Object.assign(project, projectDto);
return await this.projectService.create(project);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Put(':partitionKey/:rowKey')
async update(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string,
@Body() projectDto: ProjectDto
) {
try {
const project = new Project();
Object.assign(project, projectDto);
return await this.projectService.update(partitionKey, rowKey, project);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Delete(':partitionKey/:rowKey')
async delete(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
try {
return await this.projectService.delete(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -0,0 +1,6 @@
export class ProjectDto {
rowKey: string;
displayName: string;
icon: string;
summary: string;
}

View File

@@ -0,0 +1,7 @@
export class Project {
partitionKey: string;
rowKey: string;
displayName: string;
icon: string;
summary: string;
}

View File

@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { ProjectController } from './project.controller';
import { ProjectService } from './project.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Project } from './project.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(Project, {
createTableIfNotExists: true,
table: 'projects'
}),
],
controllers: [ProjectController],
providers: [ProjectService]
})
export class ProjectModule {}

View File

@@ -0,0 +1,38 @@
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { Injectable } from '@nestjs/common';
import { Project } from './project.entity';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class ProjectService {
constructor(
@InjectRepository(Project)
private readonly projectRepository: Repository<Project>
) {}
async find(partitionKey: string, rowKey: string): Promise<Project> {
return await this.projectRepository.find(partitionKey, rowKey);
}
async findAll(): Promise<Project[]> {
return await this.projectRepository.findAll();
}
async create(project: Project): Promise<Project> {
project.partitionKey = 'project';
return await this.projectRepository.create(project);
}
async update(
partitionKey: string,
rowKey: string,
project: Project
): Promise<Project> {
return await this.projectRepository.update(partitionKey, rowKey, project);
}
async delete(partitionKey: string, rowKey: string): Promise<void> {
await this.projectRepository.delete(partitionKey, rowKey);
}
}