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

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* api no longer fails when no pilots or logbook entries

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* Refactoring

* refactoring

* refactoring

* refactoring

* refactoring

* adding test environment

* adding test environment
This commit was merged in pull request #35.
This commit is contained in:
2025-02-02 23:24:48 +00:00
committed by GitHub
parent 2199c6d016
commit 468837c985
97 changed files with 13360 additions and 27219 deletions

BIN
.DS_Store vendored

Binary file not shown.

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
node_modules
.git
.gitignore
*.md
infrastructure
test

View File

@@ -1,62 +0,0 @@
name: 'API Build and Deploy'
on:
workflow_call:
inputs:
calling_workflow:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
environment: ${{ inputs.calling_workflow }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: true
- name: 'Setup Node'
uses: actions/setup-node@v4
with:
node-version: ${{ vars.NODE_VERSION }}
- name: Install
run: |
npm install
- name: Build
shell: bash
run: |
npm run build -w api
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log into Docker Hub
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to registry
uses: docker/build-push-action@v6
with:
push: true
tags: noahspan/flying-api:${{ github.run_id }}
file: ./api/Dockerfile
context: ./api
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 container app update --name flying-api --resource-group noahspan --image noahspan-flying:${{ github.run_id }}

69
.github/workflows/api_build.yaml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: 'API Build and Deploy'
on:
workflow_call:
inputs:
environment_name:
required: true
type: string
version_number:
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
environment: ${{ inputs.environment_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
sparse-checkout: |
api
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
run_install: false
- name: Install Nest CLI
run: |
npm install -g @nestjs/cli
- name: 'Setup Node'
uses: actions/setup-node@v4
with:
node-version: ${{ vars.NODE_VERSION }}
- name: Install dependencies
run: |
pnpm install
- name: Build
run: |
pnpm --filter api build
- name: Deploy
if: ${{ github.event_name != 'pull_request' }}
run: |
pnpm --filter api --prod deploy ./.prod/api
- name: Log into Docker Hub
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Docker Buildx
if: ${{ github.event_name != 'pull_request' }}
uses: docker/setup-buildx-action@v3
- name: Build and push to registry
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v6
with:
push: true
tags: noahspan/flying-api:${{ inputs.version_number }}
context: .
target: api

View File

@@ -1,48 +0,0 @@
name: 'App Build and Deploy'
on:
workflow_call:
inputs:
calling_workflow:
required: true
type: string
deployment_environment:
required: true
type: string
jobs:
build-and-deploy:
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:
submodules: true
- name: 'Setup Node'
uses: actions/setup-node@v4
with:
node-version: ${{ vars.NODE_VERSION }}
- name: Install
run: |
npm install
- name: Build
run: |
npm run build -w app
- name: Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
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 }}

69
.github/workflows/app_build.yaml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: 'App Build and Deploy'
on:
workflow_call:
inputs:
calling_workflow:
required: true
type: string
version_number:
required: true
type: string
jobs:
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:
sparse-checkout: |
app
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
run_install: false
- name: 'Setup Node'
uses: actions/setup-node@v4
with:
node-version: ${{ vars.NODE_VERSION }}
- name: Install
run: |
pnpm install
- name: Build
run: |
pnpm --filter app build
- name: Deploy
if: ${{ github.event_name != 'pull_request' }}
run: |
pnpm --filter app --prod deploy ./.prod/app
- name: Setup Docker Buildx
if: ${{ github.event_name != 'pull_request' }}
uses: docker/setup-buildx-action@v3
- name: Log into Docker Hub
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to registry
if: ${{ github.event_name != 'pull_request' }}
uses: docker/build-push-action@v6
with:
push: true
tags: noahspan/flying-app:${{ inputs.version_number }}
context: .
target: app

27
.github/workflows/deploy.yaml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Deploy
on:
workflow_call:
inputs:
app_name:
required: true
type: string
environment_name:
required: true
type: string
version_number:
required: true
type: string
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment_name }}
steps:
- name: Log in to Azure
uses: azure/login@v2
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Update Container App
run: |
az containerapp update --name ${{ inputs.app_name }}}}-${{ inputs.environment_name }} --revision-suffix ${{ inputs.version_number }} --resource-group noahspan --image docker.io/noahspan/${{ inputs.app_name }}:${{ inputs.version_number }}

View File

@@ -1,21 +0,0 @@
name: Dev
on:
push:
branches:
- feature/**
jobs:
build-deploy-api:
name: build-deploy-api
uses: ./.github/workflows/api.yaml
with:
calling_workflow: dev
secrets: inherit
build-deploy-app:
name: build-deploy-app
uses: ./.github/workflows/app.yaml
with:
calling_workflow: dev
deployment_environment: development
secrets: inherit

42
.github/workflows/main.yaml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Main
on:
push:
branches:
- main
jobs:
build-api:
name: build-api
uses: ./.github/workflows/api_build.yaml
with:
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit
deploy-api:
name: deploy-api
needs: build-api
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying-app
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit
build-app:
name: build-app
uses: ./.github/workflows/app_build.yaml
with:
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit
deploy-app:
name: deploy-app
needs: build-app
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying-app
environment_name: test
version_number: ${{ github.run_id }}
secrets: inherit

View File

@@ -2,36 +2,20 @@ name: Pull Request
on:
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build-deploy-api:
if: github.event_name == 'pull_request' && github.event.action != 'closed'
name: build-deploy-api
uses: ./.github/workflows/api.yaml
build-api:
name: build-api
uses: ./.github/workflows/api_build.yaml
with:
calling_workflow: dev
environment_name: 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
build-app:
name: build-app
uses: ./.github/workflows/app_build.yaml
with:
calling_workflow: dev
deployment_environment: staging
environment_name: pull_request
version_number: ${{ github.run_id }}
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
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
action: 'close'

42
.github/workflows/tag.yaml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Tag
on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
jobs:
build-api:
name: build-api
uses: ./.github/workflows/api_build.yaml
with:
environment_name: prod
version_number: ${{ github.run_id }}
secrets: inherit
deploy-api:
name: deploy-api
needs: build-api
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying-api
environment_name: prod
version_number: ${{ github.run_id }}
secrets: inherit
build-app:
name: build-app
uses: ./.github/workflows/app_build.yaml
with:
environment_name: prod
version_number: ${{ github.run_id }}
secrets: inherit
deploy-app:
name: deploy-app
needs: build-app
uses: ./.github/workflows/deploy.yaml
with:
app_name: flying-app
environment_name: prod
version_number: ${{ github.run_id }}
secrets: inherit

3
.gitignore vendored
View File

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

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# . "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
# npx lint-staged

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
inject-workspace-packages=true

39
Dockerfile Normal file
View File

@@ -0,0 +1,39 @@
# FROM --platform=linux/amd64 node:18-alpine AS base
# ENV PNPM_HOME="/pnpm"
# ENV PATH="$PNPM_HOME:$PATH"
# RUN corepack enable
# FROM base AS api
# COPY /api/dist /app/dist
# COPY /api/node_modules /app/node_modules
# WORKDIR /app
# EXPOSE 3000
# # CMD ["node", "dist/main.js"]
# ENTRYPOINT ["tail", "-f", "/dev/null"]
# FROM base AS app
# COPY /app/dist /app/dist
# WORKDIR /app
# RUN npm i -g serve
# EXPOSE 8080
# CMD [ "serve", "-s", "/dist", "-p", "8080" ]
FROM node:18-slim AS base
FROM base AS api
WORKDIR api
COPY ./.prod/api .
EXPOSE 3000
CMD ["node", "dist/main.js"]
# ENTRYPOINT ["tail", "-f", "/dev/null"]
FROM base AS app
WORKDIR /app
COPY ./.prod/app .
RUN npm i -g serve
EXPOSE 8080
CMD [ "serve", "-s", "dist", "-p", "8080" ]

View File

@@ -1,3 +0,0 @@
local.settings.json
node_modules
test

View File

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

View File

@@ -1,11 +0,0 @@
# 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
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
AzureFunctionsJobHost__Logging__Console__IsEnabled=true
COPY . /home/site/wwwroot
RUN cd /home/site/wwwroot && \
npm install

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"
}

9146
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "api",
"version": "0.0.1",
"version": "0.0.22",
"description": "",
"author": "",
"private": true,
@@ -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.2",
"@schematics/angular": "^17.3.7",
"dotenv": "^16.4.5",
"dotenv": "^16.4.7",
"express-jwt": "^8.5.1",
"passport-azure-ad": "^4.3.5",
"reflect-metadata": "0.1.13",
"rxjs": "^7.8.1",
"uuid": "^10.0.0"
@@ -51,11 +53,13 @@
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"files": [
"dist"
],
"jest": {
"moduleFileExtensions": [
"js",

View File

@@ -3,54 +3,23 @@ import {
Get,
Headers,
Query,
Res,
StreamableFile
StreamableFile,
UseGuards
} from '@nestjs/common';
import {
AppConfigService,
MsGraphService,
MsGraphClient
} from '@noahspan/noahspan-modules';
import { FeatureFlagValue } from '@azure/app-configuration';
import { Public } from '@noahspan/noahspan-modules';
import { Client as MsGraphClient } from '@microsoft/microsoft-graph-client';
import { MsGraphService } from './msGraph/ms-graph.service'
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 { AuthGuard } from '@nestjs/passport';
@Controller()
@UseGuards(AuthGuard('azure-ad'))
export class AppController {
constructor(
private readonly appService: AppService,
private readonly appConfigService: AppConfigService,
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;
}
}
@Get('userPhoto')
async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> {
try {
@@ -106,7 +75,6 @@ export class AppController {
}
}
@Public()
@Get('hello')
async getHello(): Promise<string> {
return this.appService.getHello();

View File

@@ -1,47 +1,49 @@
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 { APP_GUARD } from '@nestjs/core';
import { LogbookModule } from './logbook/logbook.module';
import { AuthModule } from './auth/auth.module';
import { MsGraphModule } from './msGraph/ms-graph.module';
import { FeatureFlagModule } from './featureFlag/feature-flag.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 { ConfigModule, ConfigService } from '@nestjs/config';
import configuration from './config/configuration';
@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: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
clientId: configService.get<string>('clientId'),
clientSecret: configService.get<string>('clientSecret'),
tenantId: configService.get<string>('tenantId')
};
},
inject: [ConfigService]
}),
AuthModule.register({
tenantId: process.env.TENANT_ID,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET
ConfigModule.forRoot({
load: [configuration]
}),
MsGraphModule.register({
tenantId: process.env.TENANT_ID,
clientId: process.env.CLIENT_ID,
clientSecret: process.env.CLIENT_SECRET
FeatureFlagModule,
LogModule,
MsGraphModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
clientId: configService.get<string>('clientId'),
clientSecret: configService.get<string>('clientSecret'),
tenantId: configService.get<string>('tenantId')
};
},
inject: [ConfigService]
}),
LogbookModule,
PilotModule
],
controllers: [AppController],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard
},
{
provide: APP_FILTER,
useClass: HttpExceptionFilter

View File

@@ -1,12 +1,14 @@
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 './msGraph/ms-graph.service';
@Injectable()
export class AppService {
constructor(private readonly msGraphService: MsGraphService) {}
getHello(): string {
return 'Hello World!';
return JSON.stringify(process.env);
}
async getPersonSearchResults(

View File

@@ -0,0 +1,5 @@
export interface AuthModuleOptions {
tenantId: string;
clientId: string;
clientSecret: string;
}

View File

@@ -0,0 +1,4 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { AuthModuleOptions } from './auth.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<AuthModuleOptions>().build()

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { PassportModule } from '@nestjs/passport';
import { AzureAdStrategy } from './auth.strategy';
import { ConfigurableModuleClass } from './auth.module-definition';
@Module({
imports: [
PassportModule.register({
defaultStrategy: 'azure-ad'
})
],
providers: [AzureAdStrategy]
})
export class AuthModule extends ConfigurableModuleClass {}

View File

@@ -0,0 +1,26 @@
import { Inject, Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { AuthModuleOptions } from './auth.interface'
import { MODULE_OPTIONS_TOKEN } from "./auth.module-definition";
import { BearerStrategy } from 'passport-azure-ad'
@Injectable()
export class AzureAdStrategy extends PassportStrategy(
BearerStrategy,
'azure-ad'
) {
constructor(@Inject(MODULE_OPTIONS_TOKEN) authModuleOptions: AuthModuleOptions) {
console.log(authModuleOptions)
super({
identityMetadata: `https://login.microsoftonline.com/${authModuleOptions.tenantId}/.well-known/openid-configuration`,
clientID: authModuleOptions.clientId,
audience: `api://${authModuleOptions.clientId}`,
loggingLevel: 'info',
loggingNoPII: false
})
}
async validate(data: any): Promise<any> {
return data;
}
}

View File

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

View File

@@ -0,0 +1,41 @@
import {
Controller,
Get,
HttpException,
Param,
UseGuards,
} from '@nestjs/common';
import { FeatureFlagService } from './feature-flag.service';
import { CustomError } from '../error/customError';
import { AuthGuard } from '@nestjs/passport';
@Controller('featureFlags')
@UseGuards(AuthGuard('azure-ad'))
export class FeatureFlagController {
constructor(private readonly featureFlagService: FeatureFlagService) {}
@Get(':partitionKey/:rowKey')
async find(
@Param('partitionKey') partitionKey: string,
@Param('rowKey') rowKey: string
) {
try {
return await this.featureFlagService.find(partitionKey, rowKey);
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
@Get()
async findAll() {
try {
return await this.featureFlagService.findAll();
} catch (error) {
const customError = error as CustomError;
throw new HttpException(customError.message, customError.statusCode);
}
}
}

View File

@@ -0,0 +1,5 @@
export class FeatureFlagDto {
partitionKey: string;
rowKey: string;
active: string;
}

View File

@@ -0,0 +1,7 @@
import { EntityString } from '@noahspan/azure-database';
export class FeatureFlag {
@EntityString() partitionKey: string;
@EntityString() rowKey: string;
@EntityString() active: string;
}

View File

@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { FeatureFlagController } from './feature-flag.controller';
import { FeatureFlagService } from './feature-flag.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { FeatureFlag } from './feature-flag.entity';
@Module({
imports: [
AzureTableStorageModule.forRootAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(FeatureFlag, {
createTableIfNotExists: false,
table: 'featureFlags'
}),
],
controllers: [FeatureFlagController],
providers: [FeatureFlagService]
})
export class FeatureFlagModule {}

View File

@@ -0,0 +1,18 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository, Repository } from '@noahspan/azure-database';
import { FeatureFlag } from './feature-flag.entity';
@Injectable()
export class FeatureFlagService {
constructor(
@InjectRepository(FeatureFlag) private readonly featureFlagRepository: Repository<FeatureFlag>
) {}
async find(partitionKey: string, rowKey: string): Promise<FeatureFlag> {
return await this.featureFlagRepository.find(partitionKey, rowKey);
}
async findAll(): Promise<FeatureFlag[]> {
return await this.featureFlagRepository.findAll();
}
}

View File

@@ -13,7 +13,6 @@ export class HttpExceptionFilter implements ExceptionFilter {
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = excpetion.getStatus();
console.log(excpetion.cause);
response.status(status).json({
name: excpetion.cause,

View File

@@ -0,0 +1,97 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
Put,
UseGuards
} from '@nestjs/common';
import { LogDto } from './log.dto';
import { Log } from './log.entity';
import { LogService } from './log.service';
import { CustomError } from '../error/customError';
import { AuthGuard } from '@nestjs/passport';
@Controller('logs')
@UseGuards(AuthGuard('azure-ad'))
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);
}
}
// @Public()
@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);
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;

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

@@ -0,0 +1,27 @@
import { Module } from '@nestjs/common';
import { LogController } from './log.controller';
import { LogService } from './log.service';
import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Log } from './log.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(Log, {
createTableIfNotExists: false,
table: 'logs'
}),
],
controllers: [LogController],
providers: [LogService]
})
export class LogModule {}

View File

@@ -0,0 +1,34 @@
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 {
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

@@ -3,13 +3,16 @@ import { AppModule } from './app.module';
import { HttpService } from '@nestjs/axios';
import { HttpExceptionFilter } from './filters/http-exception.filter';
import { InternalServerErrorException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
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;
@@ -21,6 +24,7 @@ async function bootstrap() {
}
);
await app.listen(3000);
}
bootstrap();

View File

@@ -0,0 +1,5 @@
export interface MsGraphModuleOptions {
tenantId: string;
clientId: string;
clientSecret: string;
}

View File

@@ -0,0 +1,4 @@
import { ConfigurableModuleBuilder } from '@nestjs/common';
import { MsGraphModuleOptions } from './ms-graph.interface';
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder<MsGraphModuleOptions>().build()

View File

@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { MsGraphService } from './ms-graph.service';
import { ConfigurableModuleClass } from './ms-graph.module-definition';
@Module({
providers: [MsGraphService],
exports: [MsGraphService]
})
export class MsGraphModule extends ConfigurableModuleClass {}

View File

@@ -0,0 +1,41 @@
import { Inject, Injectable } from '@nestjs/common';
import { MsGraphModuleOptions } from './ms-graph.interface';
import { Client } from '@microsoft/microsoft-graph-client';
import { AuthenticationResult, ConfidentialClientApplication, OnBehalfOfRequest } from '@azure/msal-node';
import { MODULE_OPTIONS_TOKEN } from './ms-graph.module-definition';
@Injectable()
export class MsGraphService {
constructor(@Inject(MODULE_OPTIONS_TOKEN) private msGraphModuleOptions: MsGraphModuleOptions) {}
async getMsGraphAuth(accessToken: string, scopes: string[]): Promise<string> {
try {
const oboRequest: OnBehalfOfRequest = {
oboAssertion: accessToken,
scopes: scopes
}
const cca = new ConfidentialClientApplication({
auth: {
clientId: this.msGraphModuleOptions.clientId,
clientSecret: this.msGraphModuleOptions.clientSecret,
authority: `https://login.microsoftonline.com/${this.msGraphModuleOptions.tenantId}`
}
});
const authenticationResult: AuthenticationResult = await cca.acquireTokenOnBehalfOf(oboRequest);
return authenticationResult.accessToken
} catch (error) {
return error
}
}
async getMsGraphClientDelegated(accessToken): Promise<Client> {
const client = await Client.init({
authProvider: (done) => {
done(null, accessToken);
}
});
return client;
}
}

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,117 +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');
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,95 @@
import {
Body,
Controller,
Delete,
Get,
HttpException,
Param,
Post,
UseInterceptors
Put,
UseGuards,
} 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 } from '../error/customError';
import { AuthGuard } from '@nestjs/passport'
@Controller('pilots')
@UseGuards(AuthGuard('azure-ad'))
export class PilotController {
constructor(private readonly pilotInfoService: PilotInfoService) {}
constructor(private readonly pilotService: PilotService) {}
@Get(':pilotId')
@Public()
@UseInterceptors(PilotInterceptor)
async find(@Param() params: any): Promise<PilotInfoEntity> {
@Get(':partitionKey/:rowKey')
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,27 @@
import { Module } from '@nestjs/common';
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';
import { ConfigModule, ConfigService } from '@nestjs/config';
@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: [ConfigModule],
useFactory: async (configService: ConfigService) => {
return {
connectionString: configService.get<string>('azureStorageConnectionString')
};
},
inject: [ConfigService]
}),
AzureTableStorageModule.forFeature(Pilot, {
createTableIfNotExists: false,
table: 'pilots'
}),
],
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);
}
}

View File

@@ -1,26 +1,23 @@
{
"name": "@noahspan/flying-client",
"name": "app",
"private": true,
"version": "0.1.0",
"version": "0.0.19",
"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",
"@azure/msal-browser": "^4.0.1",
"@azure/msal-react": "^3.0.1",
"@noahspan/noahspan-components": "^0.8.9",
"axios": "^1.7.2",
"framer-motion": "^11.1.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"dotenv": "^16.4.7",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.51.4",
"react-router-dom": "^6.23.0"
},
@@ -29,13 +26,13 @@
"@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"
},
"engines": {
"node": ">=18.0.0"
}
},
"files": [
"dist"
]
}

View File

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

View File

@@ -7,8 +7,14 @@ import { AxiosInstance, AxiosResponse } from 'axios';
import { useHttpClient } from './hooks/httpClient/UseHttpClient';
import { useFeatureFlag } from './hooks/featureFlag/UseFeatureFlag';
import SiteNav from './components/siteNav/SiteNav';
import { IPublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
const App: React.FC<unknown> = () => {
interface AppProps {
pca: IPublicClientApplication
}
const App: React.FC<AppProps> = ({ pca }: AppProps) => {
const httpClient: AxiosInstance = useHttpClient();
const appContext = useAppContext();
@@ -36,7 +42,7 @@ const App: React.FC<unknown> = () => {
// }, []);
return (
<div>
<MsalProvider instance={pca}>
<SiteNav />
<Routes>
{/* {useFeatureFlag('flying-pilots')?.enabled && ( */}
@@ -44,7 +50,7 @@ const App: React.FC<unknown> = () => {
{/* )} */}
<Route path="/" element={<Logbook />} />
</Routes>
</div>
</MsalProvider>
);
};

View File

@@ -0,0 +1,70 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { LogLevel } from '@azure/msal-browser';
/**
* Configuration object to be passed to MSAL instance on creation.
* For a full list of MSAL.js configuration parameters, visit:
* https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md
*/
export const msalConfig = {
auth: {
clientId: import.meta.env.VITE_CLIENT_ID, // This is the ONLY mandatory field that you need to supply.
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`, // Replace the placeholder with your tenant subdomain
redirectUri: import.meta.env.VITE_REDIRECT_URL, // Points to window.location.origin. You must register this URI on Microsoft Entra admin center/App Registration.
postLogoutRedirectUri: '/', // Indicates the page to navigate after logout.
navigateToLoginRequestUrl: false, // If "true", will navigate back to the original request location before processing the auth code response.
},
cache: {
cacheLocation: 'sessionStorage', // Configures cache location. "sessionStorage" is more secure, but "localStorage" gives you SSO between tabs.
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
},
system: {
loggerOptions: {
loggerCallback: (level: any, message: any, containsPii: any) => {
if (containsPii) {
return;
}
switch (level) {
case LogLevel.Error:
console.error(message);
return;
case LogLevel.Info:
console.info(message);
return;
case LogLevel.Verbose:
console.debug(message);
return;
case LogLevel.Warning:
console.warn(message);
return;
default:
return;
}
},
},
},
};
/**
* Scopes you add here will be prompted for user consent during sign-in.
* By default, MSAL.js will add OIDC scopes (openid, profile, email) to any login request.
* For more information about OIDC scopes, visit:
* https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#openid-connect-scopes
*/
export const loginRequest = {
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`],
};
/**
* An optional silentRequest object can be used to achieve silent SSO
* between applications by providing a "login_hint" property.
*/
// export const silentRequest = {
// scopes: ["openid", "profile"],
// loginHint: "example@domain.net"
// };

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,11 +35,8 @@ 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);
console.log(response)
dispatch({ type: 'SET_ENTRIES', payload: response.data });
} catch (error) {
const axiosError = error as AxiosError;
@@ -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',
@@ -418,12 +415,15 @@ const Logbook: React.FC<unknown> = () => {
</>
)}
</Grid>
<LogbookEntryForm
{state.isFormOpen && (
<LogForm
entryId={state.selectedEntryId}
isDrawerOpen={state.isFormOpen}
mode={state.formMode}
onOpenClose={(mode) => onOpenCloseEntryForm(mode)}
/>
)}
{state.isConfirmDialogOpen && (
<ConfirmationDialog
contentText="Are you sure you want to delete the logbook entry?"
isLoading={state.isConfirmDialogLoading}
@@ -432,6 +432,7 @@ const Logbook: React.FC<unknown> = () => {
onConfirm={onConfirmationDialogConfirm}
title="Confirm Delete"
/>
)}
</Box>
);
};

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 (

View File

@@ -43,8 +43,8 @@ const SiteNav: React.FC<unknown> = () => {
url: '/pilots'
}
];
const handleSignIn = async () => {
await instance.loginRedirect({
const handleSignIn = () => {
instance.loginRedirect({
scopes: [`api://${import.meta.env.VITE_CLIENT_ID}/user_impersonation`]
});
};
@@ -52,6 +52,7 @@ const SiteNav: React.FC<unknown> = () => {
instance.logoutRedirect();
};
const getUserProfile = async (accessToken: string): Promise<User> => {
console.log(accessToken)
try {
const response: AxiosResponse = await httpClient.get(`api/userProfile`, {
headers: {
@@ -66,6 +67,7 @@ const SiteNav: React.FC<unknown> = () => {
}
};
const getUserPhoto = async (accessToken: string): Promise<string> => {
console.log(accessToken)
try {
const response: AxiosResponse = await httpClient.get(`api/userPhoto`, {
headers: {
@@ -98,40 +100,44 @@ const SiteNav: React.FC<unknown> = () => {
);
};
useEffect(() => {
const callback = instance.addEventCallback(
async (message: EventMessage) => {
if (message.eventType === EventType.LOGIN_SUCCESS) {
try {
setLoading(true);
// useEffect(() => {
// const callback = instance.addEventCallback(
// async (message: EventMessage) => {
// if (message.eventType === EventType.LOGIN_SUCCESS) {
// try {
// setLoading(true);
const eventPayload: EventPayloadExtended =
message.payload as EventPayloadExtended;
const userProfile: User = await getUserProfile(
eventPayload.accessToken
);
const userPhoto = await getUserPhoto(eventPayload.accessToken);
// const eventPayload: EventPayloadExtended =
// message.payload as EventPayloadExtended;
// const userProfile: User = await getUserProfile(
// eventPayload.accessToken
// );
// const userPhoto = await getUserPhoto(eventPayload.accessToken);
appContext.dispatch({
type: 'SET_USER_PROFILE',
payload: userProfile
});
} catch (error) {
console.log(error);
} finally {
setLoading(false);
}
}
}
);
// appContext.dispatch({
// type: 'SET_USER_PROFILE',
// payload: userProfile
// });
// } catch (error) {
// console.log(error);
// } finally {
// setLoading(false);
// }
// }
// }
// );
return () => {
if (callback) {
instance.removeEventCallback(callback);
appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
}
};
}, []);
// instance.handleRedirectPromise().then((response) => {
// console.log(response)
// })
// return () => {
// if (callback) {
// instance.removeEventCallback(callback);
// appContext.dispatch({ type: 'SET_USER_PROFILE', payload: {} });
// }
// };
// }, []);
useEffect(() => {
const setUserProfile = async () => {

View File

@@ -5,6 +5,6 @@ export const useFeatureFlag = (featureFlagKey: string) => {
const featureFlag = appContext.state.featureFlags.find(
(featureFlag) => featureFlag.key === featureFlagKey
);
console.log(featureFlag)
return featureFlag;
};

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

@@ -5,27 +5,34 @@ import AppContextProvider from './context/appContext/AppContextProvider.tsx';
import { BrowserRouter } from 'react-router-dom';
import './index.css';
import '@noahspan/noahspan-components/noahspan-components.css';
import { PublicClientApplication } from '@azure/msal-browser';
import { AuthenticationResult, EventMessage, EventType, PublicClientApplication } from '@azure/msal-browser';
import { MsalProvider } from '@azure/msal-react';
import { msalConfig } from './auth/msalConfig';
const msalInstance: PublicClientApplication = new PublicClientApplication({
auth: {
clientId: import.meta.env.VITE_CLIENT_ID,
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_TENANT_ID}`,
redirectUri: import.meta.env.VITE_REDIRECT_URL
const msalInstance: PublicClientApplication = new PublicClientApplication(msalConfig);
msalInstance.initialize().then(() => {
const accounts = msalInstance.getAllAccounts();
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0]);
}
msalInstance.addEventCallback((event: EventMessage) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const payload = event.payload as AuthenticationResult;
const account = payload.account;
msalInstance.setActiveAccount(account);
}
});
msalInstance.initialize().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<MsalProvider instance={msalInstance}>
<AppContextProvider>
<BrowserRouter>
<App />
<App pca={msalInstance} />
</BrowserRouter>
</AppContextProvider>
</MsalProvider>
</React.StrictMode>
);
});
})

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

@@ -11,17 +11,23 @@ services:
command: 'azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --loose'
api:
container_name: api
container_name: flying-api
build:
context: ./api
dockerfile: Dockerfile
context: .
target: api
ports:
- '7071:3000'
- '3000:3000'
env_file:
- ./api/.env
# swa:
# image: swacli/static-web-apps-cli
# ports:
# - "4280:4280"
# command: "swa start "
app:
container_name: flying-app
build:
context: .
target: app
ports:
- '8080:8080'
# env_file:
# - ./app/.env

4
infrastructure/config.tf Normal file
View File

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

View File

@@ -0,0 +1,61 @@
locals {
container_app_app_name = {
test = "flying-app-test"
prod = "flying-app-prod"
}
container_app_app_container_image = {
test = "noahspan/flying-app:v0.0.1"
prod = "noahspan/flying-app:v0.0.1"
}
container_app_app_container_name = {
test = "flying-app-test"
prod = "flying-app-prod"
}
container_app_api_name = {
test = "flying-api-test"
prod = "flying-api-prod"
}
container_app_api_container_image = {
test = "noahspan/flying-api:v0.0.1"
prod = "noahspan/flying-api:v0.0.1"
}
container_app_api_container_name = {
test = "flying-api-test"
prod = "flying-api-prod"
}
container_app_api_dapr_app_id = {
test = "flyingapitest"
prod = "flyingpapiprod"
}
container_app_environment_name = {
test = "flying-test"
prod = "flying-prod"
}
logbook_feature_flag_active = {
test = "true"
prod = "true"
}
log_analytics_workspace_name = {
test = "flying-log-analytics-workspace-test"
prod = "flying-log-analytics-workspace-prod"
}
pilots_feature_flag_active = {
test = "true"
prod = "true"
}
storage_account_name = {
test = "noahspanflyingtest"
prod = "noahspanflyingprod"
}
}

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 "logbook_feature_flag_active" {
value = local.logbook_feature_flag_active[var.environment]
}
output "log_analytics_workspace_name" {
value = local.log_analytics_workspace_name[var.environment]
}
output "pilots_feature_flag_active" {
value = local.pilots_feature_flag_active[var.environment]
}
output "storage_account_name" {
value = local.storage_account_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"
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 = 1
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 +0,0 @@
resource "azurerm_log_analytics_workspace" "log_analytics_workspace" {
name = "noahspan-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-dev"
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
}

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

@@ -1,23 +1,158 @@
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"
}
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"
env {
name = "AZURE_STORAGE_CONNECTION_STRING"
secret_name = "azure-storage-connection-string"
}
env {
name = "CLIENT_ID"
secret_name = "client-id"
}
env {
name = "CLIENT_SECRET"
secret_name = "client-secret"
}
env {
name = "TENANT_ID"
secret_name = "tenant-id"
}
}
}
ingress {
allow_insecure_connections = false
external_enabled = true
target_port = 3000
transport = "auto"
traffic_weight {
latest_revision = true
percentage = 100
}
}
secret {
name = "docker-io-password"
value = var.DOCKER_IO_PASSWORD
}
secret {
name = "azure-storage-connection-string"
value = azurerm_storage_account.storage_account.primary_connection_string
}
secret {
name = "client-id"
value = var.CLIENT_ID
}
secret {
name = "client-secret"
value = var.CLIENT_SECRET
}
secret {
name = "tenant-id"
value = var.TENANT_ID
}
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 = 8080
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 +0,0 @@
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
}

View File

@@ -1,23 +1,44 @@
resource "azurerm_storage_account" "storage_account_dev" {
name = "${var.STORAGE_ACCOUNT_NAME}dev"
resource "azurerm_storage_account" "storage_account" {
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"
}
resource "azurerm_storage_account" "storage_account_staging" {
name = "${var.STORAGE_ACCOUNT_NAME}staging"
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"
resource "azurerm_storage_table" "logs_table" {
name = "logs"
storage_account_name = azurerm_storage_account.storage_account.name
}
resource "azurerm_storage_account" "storage_account_prod" {
name = "${var.STORAGE_ACCOUNT_NAME}prod"
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"
resource "azurerm_storage_table" "pilot_table" {
name = "pilots"
storage_account_name = azurerm_storage_account.storage_account.name
}
resource "azurerm_storage_table" "feature_flags_table" {
name = "featureFlags"
storage_account_name = azurerm_storage_account.storage_account.name
}
resource "azurerm_storage_table_entity" "logbook_feature_flag_entity" {
storage_table_id = azurerm_storage_table.feature_flags_table.id
partition_key = "featureFlag"
row_key = "logbook"
entity = {
active = module.environment.logbook_feature_flag_active
}
}
resource "azurerm_storage_table_entity" "pilots_feature_flag_entity" {
storage_table_id = azurerm_storage_table.feature_flags_table.id
partition_key = "featureFlag"
row_key = "pilots"
entity = {
active = module.environment.pilots_feature_flag_active
}
}

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
}
@@ -33,10 +28,6 @@ variable "DOMAIN_NAME" {
type = string
}
variable "CUSTOM_DOMAIN_NAME_COUNT" {
type = number
}
variable "REGION" {
type = string
}
@@ -45,14 +36,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
}

16963
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,13 @@
{
"name": "@noahspan/flying",
"version": "1.0.0",
"workspaces": [
"api",
"app",
"tests"
],
"scripts": {
"start": "concurrently 'npm run start:azure -w api' 'wait-on tcp:0.0.0.0:7071 && npm run dev -w app'",
"format": "prettier --write \"**/src/**/*.ts\" \"**/test/**/*.ts\"",
"lint": "npm run lint -w api && npm run lint -w app",
"prepare": "husky || true"
"prepare": "husky || true",
"preprod": "rimraf ./.prod && pnpm --filter api build && pnpm --filter app build",
"prod": "pnpm --filter api --prod deploy ./.prod/api && pnpm --filter app --prod deploy ./.prod/app"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.2.0",
@@ -23,6 +20,7 @@
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
"prettier": "3.2.5",
"rimraf": "^6.0.1",
"wait-on": "^7.2.0"
},
"lint-staged": {

11879
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
packages:
- 'api'
- 'app'