Feature/33 api fails when no pilots or logbook entries #35

Merged
noahspannbauer merged 85 commits from feature/33-api-fails-when-no-pilots-or-logbook-entries into main 2025-02-02 18:24:48 -05:00
68 changed files with 3201 additions and 2616 deletions
Showing only changes of commit ab56211602 - Show all commits

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -5,19 +5,14 @@ on:
calling_workflow:
required: true
type: string
deployment_environment:
version_number:
required: true
type: string
jobs:
build-and-deploy:
build:
runs-on: ubuntu-latest
environment: ${{ inputs.calling_workflow }}
env:
VITE_API_URL: ${{ secrets.VITE_API_URL }}
VITE_CLIENT_ID: ${{ secrets.VITE_CLIENT_ID }}
VITE_TENANT_ID: ${{ secrets.VITE_TENANT_ID }}
VITE_REDIRECT_URL: ${{ secrets.VITE_REDIRECT_URL }}
steps:
- uses: actions/checkout@v4
with:
@@ -36,13 +31,38 @@ jobs:
run: |
npm run build -w app
- name: Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into Docker Hub
uses: docker/login-action@v3
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
repo_token: ${{ secrets.GITHUB_TOKEN }}
action: 'upload'
skip_app_build: true
app_location: ${{ vars.APP_LOCATION }}
deployment_environment: ${{ inputs.deployment_environment }}
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push to registry
uses: docker/build-push-action@v6
with:
push: true
tags: noahspan/flying-app:${{ inputs.version_number }}
file: ./app/Dockerfile
context: ./app
build-args: |
VITE_API_URL=${{ secrets.VITE_API_URL }}
VITE_CLIENT_ID=${{ secrets.VITE_CLIENT_ID }}
VITE_TENANT_ID=${{ secrets.VITE_TENANT_ID }}
VITE_REDIRECT_URL=${{ secrets.VITE_REDIRECT_URL }}
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.calling_workflow }}
needs: build
steps:
- name: Log in to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Update Container App
run: |
az containerapp update --name flying-app-${{ inputs.calling_workflow }} --revision-suffix ${{ inputs.version_number }} --resource-group noahspan --image docker.io/noahspan/flying-app:${{ inputs.version_number }}

View File

@@ -48,5 +48,5 @@ jobs:
uses: ./.github/workflows/app.yaml
with:
calling_workflow: dev
deployment_environment: development
version_number: ${{ github.run_id }}
secrets: inherit

View File

@@ -7,31 +7,20 @@ on:
- main
jobs:
# build-deploy-api:
# if: github.event_name == 'pull_request' && github.event.action != 'closed'
# name: build-deploy-api
# uses: ./.github/workflows/api.yaml
# with:
# calling_workflow: dev
# secrets: inherit
# build-deploy-app:
# if: github.event_name == 'pull_request' && github.event.action != 'closed'
# name: build-deploy-app
# uses: ./.github/workflows/app.yaml
# with:
# calling_workflow: dev
# deployment_environment: staging
# secrets: inherit
close-pull-request-job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
build-deploy-api:
if: github.event_name == 'pull_request' && github.event.action != 'closed'
name: build-deploy-api
uses: ./.github/workflows/api.yaml
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: 'close'
calling_workflow: pull_request
version_number: ${{ github.run_id }}
secrets: inherit
build-deploy-app:
if: github.event_name == 'pull_request' && github.event.action != 'closed'
name: build-deploy-app
uses: ./.github/workflows/app.yaml
with:
calling_workflow: pull_request
version_number: ${{ github.run_id }}
secrets: inherit

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@ node_modules
**/.env
**/.env.*
.DS_Store
.dapr
**/**/secrets.json

View File

@@ -1,7 +0,0 @@
*.js.map
*.ts
.git*
.vscode
local.settings.json
test
tsconfig.json

View File

@@ -1,11 +1,8 @@
# To enable ssh & remote debugging on app service change the base image to the one below
# FROM mcr.microsoft.com/azure-functions/node:4-node18-appservice
FROM mcr.microsoft.com/azure-functions/node:4-node18
FROM --platform=linux/amd64 node:18-alpine
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
COPY ./node_modules ./node_modules
COPY ./dist ./dist
COPY . /home/site/wwwroot
EXPOSE 3000
RUN cd /home/site/wwwroot && \
npm install
CMD ["node", "dist/main.js"]

View File

@@ -1,3 +0,0 @@
{
"version": "2.0"
}

View File

@@ -1,17 +0,0 @@
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "{*segments}"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/main/index.js"
}

View File

@@ -1,7 +0,0 @@
import { Context, HttpRequest } from '@azure/functions';
import { AzureHttpAdapter } from '@nestjs/azure-func-http';
import { createApp } from '../src/main.azure';
export default function(context: Context, req: HttpRequest): void {
AzureHttpAdapter.handle(createApp, context, req);
}

View File

@@ -1,3 +0,0 @@
{
"name": "Azure"
}

View File

@@ -1,5 +1,5 @@
{
"name": "api",
"name": "@noahspan/flying-api",
"version": "0.0.5",
"description": "",
"author": "",
@@ -16,23 +16,25 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"start:azure": "npm run build && func host start"
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@azure/app-configuration": "^1.8.0",
"@azure/data-tables": "^13.2.2",
"@azure/functions": "^1.0.3",
"@azure/msal-node": "^2.16.2",
"@microsoft/microsoft-graph-client": "^3.0.7",
"@nestjs/axios": "^3.0.3",
"@nestjs/azure-database": "^3.0.0",
"@nestjs/azure-func-http": "^0.10.0",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.2",
"@nestjs/core": "^10.0.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.0.0",
"@noahspan/noahspan-modules": "^0.4.7",
"@noahspan/azure-database": "^3.1.0",
"@noahspan/noahspan-modules": "^0.5.2",
"@schematics/angular": "^17.3.7",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"passport-azure-ad": "^4.3.5",
"reflect-metadata": "0.1.13",
"rxjs": "^7.8.1",
"uuid": "^10.0.0"

View File

@@ -6,47 +6,62 @@ import {
Res,
StreamableFile
} from '@nestjs/common';
import {
AppConfigService,
MsGraphService,
MsGraphClient
} from '@noahspan/noahspan-modules';
import { Client as MsGraphClient } from '@microsoft/microsoft-graph-client';
import { MsGraphService, Public } from '@noahspan/noahspan-modules';
import { FeatureFlagValue } from '@azure/app-configuration';
import { Public } from '@noahspan/noahspan-modules';
import { Person } from '@microsoft/microsoft-graph-types';
import { AppService } from './app.service';
import { createReadStream } from 'fs';
import { join } from 'path';
import { arrayBuffer } from 'stream/consumers';
import type { Response } from 'express';
import { DaprService } from '@noahspan/noahspan-modules';
@Controller()
export class AppController {
constructor(
private readonly appService: AppService,
private readonly appConfigService: AppConfigService,
private readonly daprService: DaprService,
private readonly msGraphService: MsGraphService
) {}
// @Public()
// @Get('featureFlags')
// async getFeatureFlags(
// @Query() query: any
// ): Promise<{ key: string; enabled: boolean }[]> {
// try {
// const featureFlagKeys: string[] =
// query.keys && query.keys.toString().includes(';')
// ? query.keys.split(';')
// : [query.keys];
// const featureFlagLabel: string = query.label;
// const featureFlags: { key: string; enabled: boolean }[] =
// await this.appConfigService.getFeatureFlags(
// featureFlagKeys,
// featureFlagLabel
// );
// return featureFlags;
// } catch (error) {
// return error;
// }
// }
@Public()
@Get('featureFlags')
async getFeatureFlags(
@Query() query: any
): Promise<{ key: string; enabled: boolean }[]> {
async getFeatureFlags(@Query() query: any): Promise<void> {
try {
const featureFlagKeys: string[] =
query.keys && query.keys.toString().includes(';')
? query.keys.split(';')
: [query.keys];
const featureFlagLabel: string = query.label;
const featureFlags: { key: string; enabled: boolean }[] =
await this.appConfigService.getFeatureFlags(
featureFlagKeys,
featureFlagLabel
);
// const config = await this.daprService.daprClient.configuration.get('app-config', ['flying-logbook'], {
// 'label': 'dev'
// })
console.log('====== BLAH ======');
const config =
await this.daprService.daprClient.configuration.get('app-config');
return featureFlags;
console.log('====== Config ======: ' + config);
} catch (error) {
console.log(`======= ERROR ======: ${error}`);
return error;
}
}

View File

@@ -1,39 +1,85 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import {
AppConfigModule,
AuthModule,
AuthGuard,
MsGraphModule
} from '@noahspan/noahspan-modules';
import { ConfigModule } from '@nestjs/config';
import { AuthGuard, AuthModule } from '@noahspan/noahspan-modules';
import { MsGraphModule } from '@noahspan/noahspan-modules';
import { APP_GUARD } from '@nestjs/core';
import { LogbookModule } from './logbook/logbook.module';
import { LogModule } from './log/log.module';
import { PilotModule } from './pilot/pilot.module';
import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { DaprModule, DaprService } from '@noahspan/noahspan-modules';
import { AzureTableStorageModule } from '@noahspan/azure-database';
const secretStoreName = 'key-vault';
@Module({
imports: [
ConfigModule.forRoot(),
AppConfigModule.register({
url: process.env.APP_CONFIG_URL,
tenantId: process.env.TENANT_ID,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET
AuthModule.registerAsync({
imports: [DaprModule],
useFactory: async (daprService: DaprService) => {
const clientIdSecret = await daprService.daprClient.secret.get(
secretStoreName,
'client-id'
);
const clientSecret = await daprService.daprClient.secret.get(
secretStoreName,
'client-secret'
);
const tenantIdSecret = await daprService.daprClient.secret.get(
secretStoreName,
'tenant-id'
);
return {
clientId: clientIdSecret['client-id'].toString(),
clientSecret: clientSecret['client-secret'].toString(),
tenantId: tenantIdSecret['tenant-id'].toString()
};
},
inject: [DaprService]
}),
AuthModule.register({
tenantId: process.env.TENANT_ID,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET
AzureTableStorageModule.forRootAsync({
imports: [DaprModule],
useFactory: async (daprService: DaprService) => {
const connectionString = await daprService.daprClient.secret.get(
secretStoreName,
'azure-storage-connection-string'
);
return {
connectionString:
connectionString['azure-storage-connection-string'].toString()
};
},
inject: [DaprService]
}),
MsGraphModule.register({
tenantId: process.env.TENANT_ID,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET
DaprModule,
LogModule,
MsGraphModule.registerAsync({
imports: [DaprModule],
useFactory: async (daprService: DaprService) => {
const clientIdSecret = await daprService.daprClient.secret.get(
secretStoreName,
'client-id'
);
const clientSecret = await daprService.daprClient.secret.get(
secretStoreName,
'client-secret'
);
const tenantIdSecret = await daprService.daprClient.secret.get(
secretStoreName,
'tenant-id'
);
return {
clientId: clientIdSecret['client-id'].toString(),
clientSecret: clientSecret['client-secret'].toString(),
tenantId: tenantIdSecret['tenant-id'].toString()
};
},
inject: [DaprService]
}),
LogbookModule,
PilotModule
],
controllers: [AppController],

View File

@@ -1,5 +1,7 @@
import { Injectable } from '@nestjs/common';
import { MsGraphService, MsGraphClient } from '@noahspan/noahspan-modules';
// import { MsGraphService, MsGraphClient } from '@noahspan/noahspan-modules';
import { Client as MsGraphClient } from '@microsoft/microsoft-graph-client';
import { MsGraphService } from '@noahspan/noahspan-modules';
@Injectable()
export class AppService {

View File

@@ -1,9 +0,0 @@
export class CustomError extends Error {
statusCode: number;
constructor(message, name, statusCode) {
super(message);
this.name = name;
this.statusCode = statusCode;
}
}

View File

@@ -0,0 +1,92 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put
} from '@nestjs/common';
import { LogDto } from './log.dto';
import { Log } from './log.entity';
import { LogService } from './log.service';
import { CustomError } from '@noahspan/noahspan-modules';
@Controller('logs')
export class LogController {
constructor(private readonly logService: LogService) {}
@Get(':partitionKey/:rowKey')
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
): Promise<Log> {
try {
return await this.logService.find(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get()
async findAll(): Promise<Log[]> {
try {
return await this.logService.findAll();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Post()
async create(@Body() logDto: LogDto): Promise<Log> {
try {
const log = new Log();
Object.assign(log, logDto);
console.log(log);
return await this.logService.create(log);
} 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() logDto: LogDto
): Promise<Log> {
try {
const log = new Log();
Object.assign(log, logDto);
return await this.logService.update(partitionKey, rowKey, log);
} 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
): Promise<void> {
try {
return await this.logService.delete(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -1,4 +1,4 @@
export class LogbookDto {
export class LogDto {
pilotId: string;
pilotName: string;
date: string;

View File

@@ -1,4 +1,4 @@
export class LogbookEntity {
export class Log {
partitionKey: string;
rowKey: string;
pilotId: string;

34
api/src/log/log.module.ts Normal file
View File

@@ -0,0 +1,34 @@
import { Module } from '@nestjs/common';
import { DaprModule, DaprService } from '@noahspan/noahspan-modules';
import { LogController } from './log.controller';
import { LogService } from './log.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Log } from './log.entity';
@Module({
imports: [
AzureTableStorageModule.forRootAsync({
imports: [DaprModule],
useFactory: async (daprService: DaprService) => {
const connectionString = await daprService.daprClient.secret.get(
'key-vault',
'azure-storage-connection-string'
);
return {
connectionString:
connectionString['azure-storage-connection-string'].toString()
};
},
inject: [DaprService]
}),
AzureTableStorageModule.forFeature(Log, {
createTableIfNotExists: false,
table: 'logs'
}),
DaprModule
],
controllers: [LogController],
providers: [LogService]
})
export class LogModule {}

View File

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

View File

@@ -1,96 +0,0 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put
} from '@nestjs/common';
import { LogbookService } from './logbook.service';
import { LogbookDto } from './logbook.dto';
import { CustomError } from '../customError/CustomError';
import { TableInsertEntityHeaders } from '@azure/data-tables';
import { LogbookEntity } from './logbook.entity';
@Controller('logbook')
export class LogbookController {
constructor(private readonly logbookService: LogbookService) {}
@Get(':entryId')
async find(@Param() params: any): Promise<LogbookEntity> {
try {
const entry: LogbookEntity = await this.logbookService.find(
params.entryId
);
return entry;
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
@Get()
async findAll(): Promise<LogbookEntity[]> {
try {
const logbookEntries: LogbookEntity[] =
await this.logbookService.findAll();
return logbookEntries;
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
@Post()
async create(@Body() logbookData: LogbookDto): Promise<void> {
try {
const response: TableInsertEntityHeaders =
await this.logbookService.create(logbookData);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
@Put(':entryId')
async update(
@Param() params: any,
@Body() logbookData: LogbookDto
): Promise<void> {
try {
return await this.logbookService.update(logbookData);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
@Delete(':rowKey')
async delete(@Param() params: any): Promise<void> {
try {
await this.logbookService.delete(params.rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
}
}
}

View File

@@ -1,20 +0,0 @@
import { Module } from '@nestjs/common';
import { LogbookController } from './logbook.controller';
import { LogbookService } from './logbook.service';
import { TableModule } from '@noahspan/noahspan-modules';
@Module({
imports: [
TableModule.register({
accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME,
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
accountUrl: process.env.AZURE_STORAGE_ACCOUNT_URL,
allowInsecureConnection: Boolean(
process.env.AZURE_STORAGE_ALLOW_INSECURE_CONNECTION
)
})
],
controllers: [LogbookController],
providers: [LogbookService]
})
export class LogbookModule {}

View File

@@ -1,183 +0,0 @@
import { Injectable } from '@nestjs/common';
import { TableClient, TableService } from '@noahspan/noahspan-modules';
import { LogbookDto } from './logbook.dto';
import { LogbookEntity } from './logbook.entity';
import { RestError, TableInsertEntityHeaders } from '@azure/data-tables';
import { CustomError } from '../customError/CustomError';
import { v4 as uuidv4 } from 'uuid';
@Injectable()
export class LogbookService {
private readonly tableName = 'Logbook';
constructor(private readonly tableService: TableService) {}
async getLogbookEntries(filter: string): Promise<LogbookEntity[]> {
try {
const client: TableClient = await this.tableService.getTableClient(
this.tableName
);
const entities = await client.listEntities({
queryOptions: { filter: filter }
});
const logbookEntries: LogbookEntity[] = [];
for await (const entity of entities) {
const logbookEntry = {
partitionKey: entity.partitionKey.toString(),
rowKey: entity.rowKey.toString(),
pilotId: entity.pilotId.toString(),
pilotName: entity.pilotName.toString(),
date: entity.date.toString(),
aircraftMakeModel: entity.aircraftMakeModel.toString(),
aircraftIdentity: entity.aircraftIdentity.toString(),
routeFrom: entity.routeFrom.toString(),
routeTo: entity.routeTo.toString(),
durationOfFlight: Number(entity.durationOfFlight),
singleEngineLand: entity.singleEngineLand
? Number(entity.singleEngineLand)
: null,
simulatorAtd: entity.simulatorAtd
? Number(entity.simulatorAtd)
: null,
landingsDay: entity.landingsDay ? Number(entity.landingsDay) : null,
landingsNight: entity.landingsNight
? Number(entity.landingsNight)
: null,
groundTrainingReceived: entity.groundTrainingReceived
? Number(entity.groundTrainingReceived)
: null,
flightTrainingReceived: entity.flightTrainingReceived
? Number(entity.flightTrainingReceived)
: null,
crossCountry: entity.crossCountry
? Number(entity.crossCountry)
: null,
night: entity.night ? Number(entity.night) : null,
solo: entity.solo ? Number(entity.solo) : null,
pilotInCommand: entity.pilotInCommand
? Number(entity.pilotInCommand)
: null,
instrumentActual: entity.instrumentActual
? Number(entity.instrumentActual)
: null,
instrumentSimulated: entity.instrumentSimulated
? Number(entity.instrumentSimulated)
: null,
instrumentApproaches: entity.instrumentApproaches
? Number(entity.instrumentApproaches)
: null,
instrumentHolds: entity.instrumentHolds
? Number(entity.instrumentHolds)
: null,
instrumentNavTrack: entity.instrumentNavTrack
? Number(entity.instrumentNavTrack)
: null,
notes: entity.notes ? entity.notes.toString() : ''
};
logbookEntries.push(logbookEntry);
}
return logbookEntries;
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async find(entryId: string): Promise<LogbookEntity> {
try {
const filter: string = `PartitionKey eq 'entry' and RowKey eq '${entryId}'`;
const entries: LogbookEntity[] = await this.getLogbookEntries(filter);
return entries[0];
} catch (error) {
throw error;
}
}
async findAll(): Promise<LogbookEntity[]> {
try {
const filter: string = `PartitionKey eq 'entry'`;
const entries: LogbookEntity[] = await this.getLogbookEntries(filter);
return entries;
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async create(logbookData: LogbookDto): Promise<TableInsertEntityHeaders> {
const client: TableClient = await this.tableService.getTableClient(
this.tableName
);
const logbook: LogbookEntity = new LogbookEntity();
Object.assign(logbook, logbookData);
logbook.partitionKey = 'entry';
logbook.rowKey = uuidv4();
try {
return await client.createEntity(logbook);
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async update(logbookData: LogbookDto): Promise<void> {
try {
const client: TableClient = await this.tableService.getTableClient(
this.tableName
);
const logbook: LogbookEntity = new LogbookEntity();
Object.assign(logbook, logbookData);
await client.upsertEntity(logbook, 'Replace');
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async delete(rowKey: string): Promise<void> {
try {
const client: TableClient = await this.tableService.getTableClient(
this.tableName
);
await client.deleteEntity('entry', rowKey);
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
}

View File

@@ -1,11 +0,0 @@
import { INestApplication } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
export async function createApp(): Promise<INestApplication> {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
await app.init();
return app;
}

View File

@@ -1,3 +1,5 @@
if (process.env.NODE_ENV !== 'production') require('dotenv').config();
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpService } from '@nestjs/axios';
@@ -20,6 +22,7 @@ async function bootstrap() {
throw new InternalServerErrorException();
}
);
app.enableCors();
await app.listen(3000);
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { Repository, InjectRepository } from '@nestjs/azure-database';
import { Repository, InjectRepository } from '@noahspan/azure-database';
import { Certificate } from './certificate.entity';
@Injectable()

View File

@@ -1,4 +1,4 @@
import { InjectRepository, Repository } from '@nestjs/azure-database';
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { Injectable } from '@nestjs/common';
import { Endorsement } from './endorsement.entity';

View File

@@ -1,12 +0,0 @@
export class PilotInfoEntity {
partitionKey: string;
rowKey: string;
id: string;
name: string;
address?: string;
city?: string;
state?: string;
postalCode?: string;
email?: string;
phone?: string;
}

View File

@@ -1,118 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PilotInfoDto } from './pilot-info.dto';
import { PilotInfoEntity } from './pilot-info.entity';
import { TableClient, TableService } from '@noahspan/noahspan-modules';
import { odata, RestError, TableInsertEntityHeaders } from '@azure/data-tables';
import { CustomError } from '../../customError/CustomError';
@Injectable()
export class PilotInfoService {
private readonly partitionKey: string = 'info';
constructor(private readonly tableService: TableService) {}
async find(pilotId: string): Promise<PilotInfoEntity> {
try {
const client: TableClient =
await this.tableService.getTableClient('Pilots');
console.log(client);
const entities = await client.listEntities({
queryOptions: {
filter: odata`PartitionKey eq 'pilot' and RowKey eq '${pilotId}'`
}
});
let pilot: PilotInfoEntity;
console.log(entities);
for await (const entity of entities) {
pilot = {
partitionKey: entity.partitionKey,
rowKey: entity.rowKey,
id: entity.id.toString(),
name: entity.name.toString(),
address: entity.address.toString(),
city: entity.city.toString(),
state: entity.state.toString(),
postalCode: entity.postalCode.toString(),
email: entity.email.toString(),
phone: entity.phone.toString()
};
}
return pilot;
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async findAll(): Promise<PilotInfoEntity[]> {
try {
const client: TableClient =
await this.tableService.getTableClient('Pilots');
const entities = await client.listEntities({
queryOptions: { filter: odata`PartitionKey eq 'pilot'` }
});
const pilots: PilotInfoEntity[] = [];
for await (const entity of entities) {
const pilot: PilotInfoEntity = {
partitionKey: entity.partitionKey,
rowKey: entity.rowKey,
id: entity.id.toString(),
name: entity.name.toString()
};
pilots.push(pilot);
}
return pilots;
} catch (error) {
const restError: RestError = error as RestError;
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
async create(pilotInfoData: PilotInfoDto): Promise<TableInsertEntityHeaders> {
try {
const client: TableClient =
await this.tableService.getTableClient('Pilots');
const pilotInfo: PilotInfoEntity = new PilotInfoEntity();
Object.assign(pilotInfo, pilotInfoData);
pilotInfo.partitionKey = 'pilot';
pilotInfo.rowKey = pilotInfo.id;
console.log(pilotInfo);
return await client.createEntity(pilotInfo);
} catch (error) {
const restError: RestError = error as RestError;
console.log(restError);
throw new CustomError(
restError.details['odataError']['message']['value'],
restError.details['odataError']['code'],
restError.statusCode
);
}
}
// async update(rowKey: string, pilotInfo: PilotInfo): Promise<PilotInfo> {
// return await this.pilotInfoRepository.update(
// this.partitionKey,
// rowKey,
// pilotInfo
// );
// }
// async delete(rowKey: string): Promise<void> {
// await this.pilotInfoRepository.delete(this.partitionKey, rowKey);
// }
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { Repository, InjectRepository } from '@nestjs/azure-database';
import { Repository, InjectRepository } from '@noahspan/azure-database';
import { Medical } from './medical.entity';
@Injectable()

View File

@@ -1,71 +1,98 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put,
UseInterceptors
} from '@nestjs/common';
import { PilotInfoService } from './info/pilot-info.service';
import { PilotInfoDto } from './info/pilot-info.dto';
import { TableInsertEntityHeaders } from '@azure/data-tables';
import { CustomError } from '../customError/CustomError';
import { PilotInfoEntity } from './info/pilot-info.entity';
import { PilotInterceptor } from 'src/pilot/interceptors/pilot.interceptor';
import { Public } from '@noahspan/noahspan-modules';
import { PilotDto } from './pilot.dto';
import { Pilot } from './pilot.entity';
import { PilotService } from './pilot.service';
import { CustomError, Public } from '@noahspan/noahspan-modules';
import { PilotInterceptor } from './interceptors/pilot.interceptor';
@Controller('pilots')
export class PilotController {
constructor(private readonly pilotInfoService: PilotInfoService) {}
constructor(private readonly pilotService: PilotService) {}
@Get(':pilotId')
@Get(':partitionKey/:rowKey')
@Public()
@UseInterceptors(PilotInterceptor)
async find(@Param() params: any): Promise<PilotInfoEntity> {
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
try {
const pilot: PilotInfoEntity = await this.pilotInfoService.find(
params.pilotId
);
return pilot;
return await this.pilotService.find(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get()
@Public()
@UseInterceptors(PilotInterceptor)
async findAll(): Promise<PilotInfoEntity[]> {
async findAll() {
try {
const pilots: PilotInfoEntity[] = await this.pilotInfoService.findAll();
return pilots;
return await this.pilotService.findAll();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
throw new HttpException(customError.message, customError.statusCode);
}
}
@Post()
async create(@Body() pilotInfoData: PilotInfoDto): Promise<void> {
async create(@Body() pilotDto: PilotDto) {
try {
const response: TableInsertEntityHeaders =
await this.pilotInfoService.create(pilotInfoData);
const pilot = new Pilot();
Object.assign(pilot, pilotDto);
return await this.pilotService.create(pilot);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode, {
cause: customError.name
});
throw new HttpException(customError.message, customError.statusCode);
}
}
@Put(':partitionKey/:rowKey')
async update(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string,
@Body() pilotDto: PilotDto
) {
try {
const pilot = new Pilot();
Object.assign(pilot, pilotDto);
return await this.pilotService.update(partitionKey, rowKey, pilot);
} 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.pilotService.delete(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -1,4 +1,6 @@
export class PilotInfoDto {
export class PilotDto {
partitionKey: string;
rowKey: string;
id: string;
name: string;
address: string;

View File

@@ -0,0 +1,14 @@
import { EntityString } from '@noahspan/azure-database';
export class Pilot {
@EntityString() partitionKey: string;
@EntityString() rowKey: string;
@EntityString() id: string;
@EntityString() name: string;
@EntityString() address?: string;
@EntityString() city?: string;
@EntityString() state?: string;
@EntityString() postalCode?: string;
@EntityString() email?: string;
@EntityString() phone?: string;
}

View File

@@ -1,20 +1,34 @@
import { Module } from '@nestjs/common';
import { DaprModule, DaprService } from '@noahspan/noahspan-modules';
import { PilotController } from './pilot.controller';
import { TableModule } from '@noahspan/noahspan-modules';
import { PilotInfoService } from './info/pilot-info.service';
import { PilotService } from './pilot.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Pilot } from './pilot.entity';
@Module({
imports: [
TableModule.register({
accountName: process.env.AZURE_STORAGE_ACCOUNT_NAME,
accountKey: process.env.AZURE_STORAGE_ACCOUNT_KEY,
accountUrl: process.env.AZURE_STORAGE_ACCOUNT_URL,
allowInsecureConnection: Boolean(
process.env.AZURE_STORAGE_ALLOW_INSECURE_CONNECTION
)
})
AzureTableStorageModule.forRootAsync({
imports: [DaprModule],
useFactory: async (daprService: DaprService) => {
const connectionString = await daprService.daprClient.secret.get(
'key-vault',
'azure-storage-connection-string'
);
return {
connectionString:
connectionString['azure-storage-connection-string'].toString()
};
},
inject: [DaprService]
}),
AzureTableStorageModule.forFeature(Pilot, {
createTableIfNotExists: false,
table: 'pilots'
}),
DaprModule
],
controllers: [PilotController],
providers: [PilotInfoService]
providers: [PilotService]
})
export class PilotModule {}

View File

@@ -0,0 +1,40 @@
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { Injectable } from '@nestjs/common';
import { Pilot } from './pilot.entity';
@Injectable()
export class PilotService {
constructor(
@InjectRepository(Pilot)
private readonly pilotRepository: Repository<Pilot>
) {}
async find(partitionKey: string, rowKey: string): Promise<Pilot> {
return await this.pilotRepository.find(partitionKey, rowKey);
}
async findAll(): Promise<Pilot[]> {
return await this.pilotRepository.findAll();
}
async create(pilot: Pilot): Promise<Pilot> {
// try {
// return await this.pilotRepository.create(pilot);
// } catch (error) {
// throw new Error(error);
// }
return await this.pilotRepository.create(pilot);
}
async update(
partitionKey: string,
rowKey: string,
pilot: Pilot
): Promise<Pilot> {
return await this.pilotRepository.update(partitionKey, rowKey, pilot);
}
async delete(partitionKey: string, rowKey: string): Promise<void> {
await this.pilotRepository.delete(partitionKey, rowKey);
}
}

18
app/Dockerfile Normal file
View File

@@ -0,0 +1,18 @@
ARG VITE_API_URL
ARG VITE_CLIENT_ID
ARG VITE_TENANT_ID
ARG VITE_REDIRECT_URL
FROM --platform=linux/amd64 node:18-alpine
WORKDIR /app
COPY ./dist ./dist
ENV NODE_ENV=production
RUN npm i -g serve
EXPOSE 8080
CMD [ "serve", "-s", "dist", "-p", "8080" ]

View File

@@ -1,24 +1,20 @@
{
"name": "@noahspan/flying-client",
"name": "@noahspan/flying-app",
"private": true,
"version": "0.1.10",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
"preview": "vite preview",
"serve": "serve -s dist"
},
"dependencies": {
"@azure/msal-browser": "^3.17.0",
"@azure/msal-react": "^2.0.19",
"@fortawesome/fontawesome-svg-core": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.5.2",
"@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@noahspan/noahspan-components": "^0.8.9",
"axios": "^1.7.2",
"framer-motion": "^11.1.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.4",
@@ -29,9 +25,6 @@
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"typescript": "^5.2.2",
"vite": "^5.2.0"
},

View File

@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View File

@@ -19,6 +19,7 @@ const App: React.FC<unknown> = () => {
const response: AxiosResponse = await httpClient.get(
`api/featureFlags?keys=${featureFlagKeys}&label=${import.meta.env.MODE}`
);
console.log(response);
const featureFlags: { key: string; enabled: boolean }[] = response.data;
if (featureFlags.length > 0) {

View File

@@ -1,6 +1,6 @@
import { FormMode } from '../../enums/formMode';
export interface ILogbookEntryFormProps {
export interface ILogFormProps {
entryId?: string;
isDrawerOpen: boolean;
mode: FormMode;

View File

@@ -1,4 +1,4 @@
export interface ILogbookEntryFormState {
export interface ILogFormState {
error: string | undefined;
isDisabled: boolean;
isLoading: boolean;

View File

@@ -17,7 +17,7 @@ import {
XmarkIcon
} from '@noahspan/noahspan-components';
import { useForm, Controller, FormProvider } from 'react-hook-form';
import { ILogbookEntryFormProps } from './ILogbookEntryFormProps';
import { ILogFormProps } from './ILogFormProps';
import { initialState, reducer } from './reducer';
import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
@@ -26,7 +26,7 @@ import { useIsAuthenticated } from '@azure/msal-react';
import { FormMode } from '../../enums/formMode';
import { usePilots } from '../../hooks/pilots/UsePilots';
const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
const LogForm: React.FC<ILogFormProps> = ({
entryId,
isDrawerOpen,
mode,
@@ -37,8 +37,6 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated();
const defaultValues = {
partitionKey: '',
rowKey: '',
pilotId: '',
pilotName: '',
date: null,
@@ -80,13 +78,13 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
const accessToken: string = await getAccessToken();
if (!entryId) {
await httpClient.post(`api/logbook`, data, {
await httpClient.post(`api/logs`, data, {
headers: {
Authorization: accessToken
}
});
} else {
await httpClient.put(`api/logbook/${entryId}`, data, {
await httpClient.put(`api/logs/${entryId}`, data, {
headers: {
Authorization: accessToken
}
@@ -120,7 +118,7 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logbook/${entryId}`,
`api/logs/${entryId}`,
config
);
const entry = response.data;
@@ -939,4 +937,4 @@ const LogbookEntryForm: React.FC<ILogbookEntryFormProps> = ({
);
};
export default LogbookEntryForm;
export default LogForm;

View File

@@ -1,4 +1,4 @@
import { ILogbookEntryFormState } from './ILogbookEntryFormState';
import { ILogFormState } from './ILogFormState';
type Action =
| { type: 'SET_ERROR'; payload: string | undefined }
@@ -7,7 +7,7 @@ type Action =
| { type: 'SET_PILOT_OPTIONS'; payload: { label: string; value: string }[] }
| { type: 'SET_SELECTED_ENTRY_PILOT_NAME'; payload: string };
export const initialState: ILogbookEntryFormState = {
export const initialState: ILogFormState = {
error: undefined,
isDisabled: false,
isLoading: true,
@@ -16,9 +16,9 @@ export const initialState: ILogbookEntryFormState = {
};
export const reducer = (
state: ILogbookEntryFormState,
state: ILogFormState,
action: Action
): ILogbookEntryFormState => {
): ILogFormState => {
switch (action.type) {
case 'SET_ERROR': {
return {

View File

@@ -1,5 +1,5 @@
import { useEffect, useReducer, useState } from 'react';
import LogbookEntryForm from '../logbookEntryForm/LogbookEntryForm';
import { useEffect, useReducer } from 'react';
import LogForm from '../logForm/LogForm';
import {
Alert,
Box,
@@ -35,10 +35,7 @@ const Logbook: React.FC<unknown> = () => {
const config = isAuthenticated
? { headers: { Authorization: `${token}` } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/logbook`,
config
);
const response: AxiosResponse = await httpClient.get(`api/logs`, config);
dispatch({ type: 'SET_ENTRIES', payload: response.data });
} catch (error) {
@@ -98,7 +95,7 @@ const Logbook: React.FC<unknown> = () => {
? { headers: { Authorization: `${token}` } }
: {};
await httpClient.delete(`api/logbook/${state.selectedEntryId}`, config);
await httpClient.delete(`api/logs/${state.selectedEntryId}`, config);
dispatch({
type: 'SET_DELETE',
@@ -419,7 +416,7 @@ const Logbook: React.FC<unknown> = () => {
)}
</Grid>
{state.isFormOpen && (
<LogbookEntryForm
<LogForm
entryId={state.selectedEntryId}
isDrawerOpen={state.isFormOpen}
mode={state.formMode}

View File

@@ -13,7 +13,7 @@ import {
XmarkIcon
} from '@noahspan/noahspan-components';
import { IPilotFormProps } from './IPilotFormProps';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { AxiosError, AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from '../../hooks/httpClient/UseHttpClient';
import { useAccessToken } from '../../hooks/accessToken/UseAcessToken';
import { useIsAuthenticated } from '@azure/msal-react';
@@ -38,8 +38,8 @@ const PilotForm: React.FC<IPilotFormProps> = ({
const { getAccessToken } = useAccessToken();
const isAuthenticated = useIsAuthenticated();
const defaultValues = {
partitionKey: '',
rowKey: '',
partitionKey: 'pilot',
rowKey: 'noah@noahspannbauer.com',
id: '',
name: '',
address: '',
@@ -53,6 +53,7 @@ const PilotForm: React.FC<IPilotFormProps> = ({
defaultValues: defaultValues
});
const [isDisabled, setIsDisabled] = useState<boolean>(false);
const [isError, setIsError] = useState<boolean>(false);
const onPeoplePickerSearch = async (
_event: React.SyntheticEvent,
@@ -105,24 +106,19 @@ const PilotForm: React.FC<IPilotFormProps> = ({
setIsLoading(true);
const accessToken: string = await getAccessToken();
const response: AxiosResponse = await httpClient.post(
`api/pilots`,
data,
{
await httpClient.post(`api/pilots`, data, {
headers: {
Authorization: accessToken
}
}
);
});
if (response) console.log(response);
onOpenClose(FormMode.CANCEL);
} catch (error) {
if (axios.isAxiosError(error)) {
const errResp = error.response;
const axiosError = error as AxiosError;
const responseData = axiosError.response?.data as any;
console.log(errResp?.data.message);
} else {
}
console.log(responseData.message);
} finally {
setIsLoading(false);
}

View File

@@ -39,7 +39,25 @@ const Pilots: React.FC<unknown> = () => {
const [pilotFormMode, setPilotFormMode] = useState<FormMode>(FormMode.CANCEL);
const [selectedPilotId, setSelectedPilotId] = useState<string | undefined>();
const [pilots, setPilots] = useState<Pilot[]>([]);
const onOpenClosePilotForm = (mode: FormMode, pilotId?: string) => {
const getPilots = async (): Promise<Pilot[]> => {
try {
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots`,
config
);
const pilots: Pilot[] = response.data;
return pilots;
} catch (error) {
throw new Error('broken');
}
};
const onOpenClosePilotForm = async (mode: FormMode, pilotId?: string) => {
switch (mode) {
case FormMode.ADD:
case FormMode.EDIT:
@@ -49,6 +67,9 @@ const Pilots: React.FC<unknown> = () => {
setIsDrawerOpen(true);
break;
case FormMode.CANCEL:
const pilots = await getPilots();
setPilots(pilots);
setPilotFormMode(mode);
setSelectedPilotId(undefined);
setIsDrawerOpen(false);
@@ -73,6 +94,7 @@ const Pilots: React.FC<unknown> = () => {
const onCloseActionMenu = () => {
setAnchorElAction(null);
};
return (
<div>
<IconButton onClick={onOpenActionMenu}>
@@ -124,23 +146,17 @@ const Pilots: React.FC<unknown> = () => {
];
useEffect(() => {
const getPilots = async () => {
const loadPilots = async () => {
try {
const config = isAuthenticated
? { headers: { Authorization: await getAccessToken() } }
: {};
const response: AxiosResponse = await httpClient.get(
`api/pilots`,
config
);
console.log(response.data);
setPilots(response.data);
const pilots = await getPilots();
setPilots(pilots);
} catch (error) {
console.log(error);
}
};
getPilots();
loadPilots();
}, []);
return (

12
app/src/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -1,9 +0,0 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {}
},
darkMode: 'class',
plugins: []
};

View File

@@ -3,5 +3,14 @@ import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()]
plugins: [react()],
preview: {
port: 8080,
strictPort: true
},
server: {
port: 8080,
strictPort: true,
host: '0.0.0.0'
}
});

14
dapr.yaml Normal file
View File

@@ -0,0 +1,14 @@
version: 1
common:
resourcesPaths: ['./.dapr/components']
apps:
- appID: flying-api
appDirPath: ./api
appPort: 3000
command: ['npm', 'run', 'start']
- appID: flying-app
appDirPath: ./app
appPort: 5173
command: ['npm', 'run', 'dev']

View File

@@ -0,0 +1,20 @@
data "azurerm_app_configuration" "app_configuration" {
name = "noahspan-appconfig"
resource_group_name = data.azurerm_resource_group.resource_group.name
}
resource "azurerm_app_configuration_feature" "pilots_app_configuration_feature" {
configuration_store_id = data.azurerm_app_configuration.app_configuration.id
description = "Feature flag for pilots page"
name = "flying-pilots"
label = var.WORKSPACE
enabled = false
}
resource "azurerm_app_configuration_feature" "logbook_app_configuration_feature" {
configuration_store_id = data.azurerm_app_configuration.app_configuration.id
description = "Feature flag for logbook page"
name = "flying-logbook"
label = var.WORKSPACE
enabled = false
}

4
infrastructure/config.tf Normal file
View File

@@ -0,0 +1,4 @@
module "environment" {
source = "./config"
environment = var.WORKSPACE
}

View File

@@ -0,0 +1,50 @@
locals {
container_app_app_name = {
dev = "flying-app-dev"
}
container_app_app_container_image = {
dev = "noahspan/flying-app:v0.0.1"
}
container_app_app_container_name = {
dev = "flying-app-dev"
}
container_app_api_name = {
dev = "flying-api-dev"
}
container_app_api_container_image = {
dev = "noahspan/flying-api:v0.0.1"
}
container_app_api_container_name = {
dev = "flying-api-dev"
}
container_app_api_dapr_app_id = {
dev = "flyingapidev"
}
container_app_environment_name = {
dev = "flying-dev"
}
key_vault_name = {
dev = "noahspanflyingkeyvault"
}
log_analytics_workspace_name = {
dev = "flying-log-analytics-workspace-dev"
}
storage_account_name = {
dev = "noahspanflyingdev"
}
user_assigned_identity_name = {
dev = "noahspanflyingdevuser"
}
}

View File

@@ -0,0 +1,47 @@
output "container_app_app_name" {
value = local.container_app_app_name[var.environment]
}
output "container_app_app_container_image" {
value = local.container_app_app_container_image[var.environment]
}
output "container_app_app_container_name" {
value = local.container_app_app_container_name[var.environment]
}
output "container_app_api_name" {
value = local.container_app_api_name[var.environment]
}
output "container_app_api_container_image" {
value = local.container_app_api_container_image[var.environment]
}
output "container_app_api_container_name" {
value = local.container_app_api_container_name[var.environment]
}
output "container_app_api_dapr_app_id" {
value = local.container_app_api_dapr_app_id[var.environment]
}
output "container_app_environment_name" {
value = local.container_app_environment_name[var.environment]
}
output "key_vault_name" {
value = local.key_vault_name[var.environment]
}
output "log_analytics_workspace_name" {
value = local.log_analytics_workspace_name[var.environment]
}
output "storage_account_name" {
value = local.storage_account_name[var.environment]
}
output "user_assigned_identity_name" {
value = local.user_assigned_identity_name[var.environment]
}

View File

@@ -0,0 +1 @@
variable "environment" {}

View File

@@ -1,91 +0,0 @@
resource "azurerm_container_app" "container_app_dev" {
name = "flying-api-dev"
container_app_environment_id = azurerm_container_app_environment.container_app_environment_dev.id
resource_group_name = data.azurerm_resource_group.resource_group.name
revision_mode = "Single"
registry {
server = "index.docker.io"
username = var.DOCKER_IO_USERNAME
password_secret_name = "docker-io-password"
}
template {
min_replicas = 0
max_replicas = 5
container {
name = "flying-api-dev"
image = "noahspan/flying-api:v0.0.1"
cpu = 0.25
memory = "0.5Gi"
env {
name = "APP_CONFIG_URL"
value = "https://noahspann-appconfig.azconfig.io"
}
env {
name = "AZURE_STORAGE_ACCOUNT_KEY"
secret_name = "azure-storage-account-key"
}
env {
name = "AZURE_STORAGE_ACCOUNT_NAME"
value = "noahspanflyingdev"
}
env {
name = "AZURE_STROAGE_ACCOUNT_URL"
value = "https://noahspanflyingdev.table.core.windows.net/"
}
env {
name = "CLIENT_ID"
secret_name = "client-id"
}
env {
name = "CLIENT_SECRET"
secret_name = "client-secret"
}
env {
name = "TENANT_ID"
value = "0f23652e-4b15-420f-991e-3d6fc769a31d"
}
}
}
ingress {
allow_insecure_connections = false
external_enabled = true
target_port = 80
transport = "auto"
traffic_weight {
latest_revision = true
percentage = 100
}
}
secret {
name = "azure-storage-account-key"
value = var.AZURE_STORAGE_ACCOUNT_KEY
}
secret {
name = "client-id"
value = var.CLIENT_ID
}
secret {
name = "client-secret"
value = var.CLIENT_SECRET
}
secret {
name = "docker-io-password"
value = var.DOCKER_IO_PASSWORD
}
}

View File

@@ -1,14 +1,2 @@
resource "azurerm_log_analytics_workspace" "log_analytics_workspace" {
name = "flying-log-analytics-workspace"
location = data.azurerm_resource_group.resource_group.location
resource_group_name = data.azurerm_resource_group.resource_group.name
sku = "PerGB2018"
retention_in_days = 30
}
resource "azurerm_container_app_environment" "container_app_environment_dev" {
name = "flying"
location = data.azurerm_resource_group.resource_group.location
resource_group_name = data.azurerm_resource_group.resource_group.name
log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.id
}

66
infrastructure/dapr.tf Normal file
View File

@@ -0,0 +1,66 @@
resource "azurerm_container_app_environment_dapr_component" "table_storage_pilots_dapr_component" {
name = "table-storage-pilots"
container_app_environment_id = azurerm_container_app_environment.container_app_environment.id
component_type = "state.azure.tablestorage"
version = "v1"
metadata {
name = "accountName"
value = azurerm_storage_account.storage_account.name
}
metadata {
name = "azureClientId"
value = azurerm_user_assigned_identity.user_assigned_identity.id
}
metadata {
name = "tableName"
value = "Pilots"
}
scopes = [ azurerm_container_app.container_app_api.dapr[0].app_id ]
}
resource "azurerm_container_app_environment_dapr_component" "table_storage_logbook_dapr_component" {
name = "table-storage-logbook"
container_app_environment_id = azurerm_container_app_environment.container_app_environment.id
component_type = "state.azure.tablestorage"
version = "v1"
metadata {
name = "accountName"
value = azurerm_storage_account.storage_account.name
}
metadata {
name = "azureClientId"
value = azurerm_user_assigned_identity.user_assigned_identity.client_id
}
metadata {
name = "tableName"
value = "Logbook"
}
scopes = [ azurerm_container_app.container_app_api.dapr[0].app_id ]
}
resource "azurerm_container_app_environment_dapr_component" "key_vault_dapr_component" {
name = "key-vault"
container_app_environment_id = azurerm_container_app_environment.container_app_environment.id
component_type = "secretstores.azure.keyvault"
version = "v1"
metadata {
name = "vaultName"
value = azurerm_key_vault.key_vault.name
}
metadata {
name = "azureClientId"
value = azurerm_user_assigned_identity.user_assigned_identity.client_id
}
scopes = [ azurerm_container_app.container_app_api.dapr[0].app_id ]
}

View File

@@ -2,3 +2,14 @@ data "azurerm_dns_zone" "dns_zone" {
name = var.DOMAIN_NAME
resource_group_name = data.azurerm_resource_group.resource_group.name
}
# resource "azurerm_dns_txt_record" "dns_txt_record" {
# name = "asuid.${var.APP_SUBDOMAIN_NAME}"
# resource_group_name = data.azurerm_resource_group.resource_group.name
# zone_name = data.azurerm_dns_zone.dns_zone.name
# ttl = 14400
# record {
# value = azurerm_container_app.container_app_app.custom_domain_verification_id
# }
# }

View File

@@ -0,0 +1,5 @@
resource "azurerm_user_assigned_identity" "user_assigned_identity" {
name = module.environment.user_assigned_identity_name
resource_group_name = data.azurerm_resource_group.resource_group.name
location = data.azurerm_resource_group.resource_group.location
}

View File

@@ -0,0 +1,70 @@
resource "azurerm_key_vault" "key_vault" {
name = module.environment.key_vault_name
resource_group_name = data.azurerm_resource_group.resource_group.name
location = data.azurerm_resource_group.resource_group.location
tenant_id = data.azurerm_client_config.current.tenant_id
soft_delete_retention_days = 7
sku_name = "standard"
}
resource "azurerm_key_vault_access_policy" "key_vault_access_policy_cli" {
key_vault_id = azurerm_key_vault.key_vault.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
key_permissions = [
"Create",
"Get",
]
secret_permissions = [
"Set",
"Get",
"Delete",
"Purge",
"Recover"
]
}
resource "azurerm_key_vault_access_policy" "key_vault_access_policy_user_assigned_identity" {
key_vault_id = azurerm_key_vault.key_vault.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id
key_permissions = [
"Create",
"Get",
]
secret_permissions = [
"Set",
"Get",
"Delete",
"Purge",
"Recover"
]
}
resource "azurerm_key_vault_secret" "client_id_key_vault_secret" {
name = "client-id"
value = var.CLIENT_ID
key_vault_id = azurerm_key_vault.key_vault.id
depends_on = [ azurerm_key_vault_access_policy.key_vault_access_policy_cli ]
}
resource "azurerm_key_vault_secret" "client_secret_key_vault_secret" {
name = "client-secret"
value = var.CLIENT_SECRET
key_vault_id = azurerm_key_vault.key_vault.id
depends_on = [ azurerm_key_vault_access_policy.key_vault_access_policy_cli ]
}
resource "azurerm_key_vault_secret" "tenant_id_key_vault_secret" {
name = "tenant-id"
value = var.TENANT_ID
key_vault_id = azurerm_key_vault.key_vault.id
depends_on = [ azurerm_key_vault_access_policy.key_vault_access_policy_cli ]
}

View File

@@ -1,23 +1,124 @@
resource "azurerm_static_web_app" "static_web_app" {
name = var.STATIC_WEB_APP_NAME
data "azurerm_client_config" "current" {
}
resource "azurerm_log_analytics_workspace" "log_analytics_workspace" {
name = module.environment.log_analytics_workspace_name
location = data.azurerm_resource_group.resource_group.location
resource_group_name = data.azurerm_resource_group.resource_group.name
location = var.REGION
sku_size = "Free"
sku_tier = "Free"
sku = "PerGB2018"
retention_in_days = 30
}
resource "azurerm_dns_cname_record" "dns_cname_record_app" {
name = var.APP_SUBDOMAIN_NAME
zone_name = data.azurerm_dns_zone.dns_zone.name
resource "azurerm_container_app_environment" "container_app_environment" {
name = module.environment.container_app_environment_name
location = data.azurerm_resource_group.resource_group.location
resource_group_name = data.azurerm_resource_group.resource_group.name
ttl = 14400
record = azurerm_static_web_app.static_web_app.default_host_name
log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.id
}
resource "azurerm_static_web_app_custom_domain" "static_web_app_custom_domain" {
static_web_app_id = azurerm_static_web_app.static_web_app.id
domain_name = "${var.APP_SUBDOMAIN_NAME}.${var.DOMAIN_NAME}"
validation_type = "cname-delegation"
resource "azurerm_container_app" "container_app_api" {
name = module.environment.container_app_api_name
container_app_environment_id = azurerm_container_app_environment.container_app_environment.id
resource_group_name = data.azurerm_resource_group.resource_group.name
revision_mode = "Single"
depends_on = [ azurerm_dns_cname_record.dns_cname_record_app ]
registry {
server = "index.docker.io"
username = var.DOCKER_IO_USERNAME
password_secret_name = "docker-io-password"
}
dapr {
app_id = module.environment.container_app_api_dapr_app_id
app_port = 80
app_protocol = "http"
}
template {
min_replicas = 0
max_replicas = 2
container {
name = module.environment.container_app_api_container_name
image = module.environment.container_app_api_container_image
cpu = 0.25
memory = "0.5Gi"
}
}
ingress {
allow_insecure_connections = false
external_enabled = true
target_port = 80
transport = "auto"
traffic_weight {
latest_revision = true
percentage = 100
}
}
secret {
name = "docker-io-password"
value = var.DOCKER_IO_PASSWORD
}
lifecycle {
ignore_changes = [ template[0].container[0].image ]
}
}
resource "azurerm_container_app" "container_app_app" {
name = module.environment.container_app_app_name
container_app_environment_id = azurerm_container_app_environment.container_app_environment.id
resource_group_name = data.azurerm_resource_group.resource_group.name
revision_mode = "Single"
registry {
server = "index.docker.io"
username = var.DOCKER_IO_USERNAME
password_secret_name = "docker-io-password"
}
template {
min_replicas = 0
max_replicas = 2
container {
name = module.environment.container_app_app_container_name
image = module.environment.container_app_app_container_image
cpu = 0.25
memory = "0.5Gi"
}
}
ingress {
allow_insecure_connections = false
external_enabled = true
target_port = 80
transport = "auto"
traffic_weight {
latest_revision = true
percentage = 100
}
}
secret {
name = "docker-io-password"
value = var.DOCKER_IO_PASSWORD
}
lifecycle {
ignore_changes = [ template[0].container[0].image ]
}
}
# resource "azurerm_container_app_custom_domain" "custom_domain" {
# name = trimsuffix(trimprefix(azurerm_dns_txt_record.dns_txt_record.fqdn, "asuid."), ".")
# container_app_id = azurerm_container_app.container_app_app.id
# lifecycle {
# ignore_changes = [ certificate_binding_type, container_app_environment_certificate_id ]
# }
# }

View File

@@ -1,8 +1,8 @@
output "api_key" {
value = azurerm_static_web_app.static_web_app.api_key
sensitive = true
}
# output "api_key" {
# value = azurerm_static_web_app.static_web_app.api_key
# sensitive = true
# }
output "default_host_name" {
value = azurerm_static_web_app.static_web_app.default_host_name
}
# output "default_host_name" {
# value = azurerm_static_web_app.static_web_app.default_host_name
# }

View File

@@ -1,3 +1,8 @@
provider "azurerm" {
features {}
features {
key_vault {
purge_soft_deleted_certificates_on_destroy = true
recover_soft_deleted_secrets = true
}
}
}

View File

@@ -1,17 +1,22 @@
resource "azurerm_storage_account" "storage_account" {
name = "${var.STORAGE_ACCOUNT_NAME}"
name = module.environment.storage_account_name
resource_group_name = data.azurerm_resource_group.resource_group.name
location = data.azurerm_resource_group.resource_group.location
account_tier = "Standard"
account_replication_type = "LRS"
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.user_assigned_identity.id]
}
}
resource "azurerm_storage_table" "logbook_storage_table" {
name = "Logbook"
resource "azurerm_storage_table" "logs_table" {
name = "logs"
storage_account_name = azurerm_storage_account.storage_account.name
}
resource "azurerm_storage_table" "pilots_storage_table" {
name = "Pilots"
resource "azurerm_storage_table" "pilot_table" {
name = "pilots"
storage_account_name = azurerm_storage_account.storage_account.name
}

View File

@@ -6,11 +6,6 @@ variable "APP_SUBDOMAIN_NAME" {
type = string
}
variable "AZURE_STORAGE_ACCOUNT_KEY" {
type = string
sensitive = true
}
variable "CLIENT_ID" {
type = string
}
@@ -45,14 +40,6 @@ variable "RESOURCE_GROUP_NAME" {
type = string
}
variable "STATIC_WEB_APP_NAME" {
type = string
}
variable "STORAGE_ACCOUNT_NAME" {
type = string
}
variable "TENANT_ID" {
type = string
}

3926
package-lock.json generated

File diff suppressed because it is too large Load Diff