Refactoring

This commit is contained in:
2025-01-19 08:02:58 -06:00
parent 1794527bc5
commit 2d2182a5ae
23 changed files with 230 additions and 295 deletions

View File

@@ -3,69 +3,20 @@ import {
Get, Get,
Headers, Headers,
Query, Query,
Res,
StreamableFile StreamableFile
} from '@nestjs/common'; } from '@nestjs/common';
import { Client as MsGraphClient } from '@microsoft/microsoft-graph-client'; import { Client as MsGraphClient } from '@microsoft/microsoft-graph-client';
import { MsGraphService, Public } from '@noahspan/noahspan-modules'; import { MsGraphService, Public } from '@noahspan/noahspan-modules';
import { FeatureFlagValue } from '@azure/app-configuration';
import { Person } from '@microsoft/microsoft-graph-types'; import { Person } from '@microsoft/microsoft-graph-types';
import { AppService } from './app.service'; 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() @Controller()
export class AppController { export class AppController {
constructor( constructor(
private readonly appService: AppService, private readonly appService: AppService,
private readonly daprService: DaprService,
private readonly msGraphService: MsGraphService 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<void> {
try {
// 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');
console.log('====== Config ======: ' + config);
} catch (error) {
console.log(`======= ERROR ======: ${error}`);
return error;
}
}
@Get('userPhoto') @Get('userPhoto')
async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> { async getProfilePhoto(@Headers() headers: any): Promise<StreamableFile> {
try { try {

View File

@@ -4,81 +4,42 @@ import { AppService } from './app.service';
import { AuthGuard, AuthModule } from '@noahspan/noahspan-modules'; import { AuthGuard, AuthModule } from '@noahspan/noahspan-modules';
import { MsGraphModule } from '@noahspan/noahspan-modules'; import { MsGraphModule } from '@noahspan/noahspan-modules';
import { APP_GUARD } from '@nestjs/core'; import { APP_GUARD } from '@nestjs/core';
import { FeatureFlagModule } from './featureFlag/feature-flag.module'
import { LogModule } from './log/log.module'; import { LogModule } from './log/log.module';
import { PilotModule } from './pilot/pilot.module'; import { PilotModule } from './pilot/pilot.module';
import { APP_FILTER } from '@nestjs/core'; import { APP_FILTER } from '@nestjs/core';
import { HttpExceptionFilter } from './filters/http-exception.filter'; import { HttpExceptionFilter } from './filters/http-exception.filter';
import { DaprModule, DaprService } from '@noahspan/noahspan-modules'; import { ConfigModule, ConfigService } from '@nestjs/config';
import { AzureTableStorageModule } from '@noahspan/azure-database'; import configuration from './config/configuration';
const secretStoreName = 'key-vault';
@Module({ @Module({
imports: [ imports: [
AuthModule.registerAsync({ AuthModule.registerAsync({
imports: [DaprModule], imports: [ConfigModule],
useFactory: async (daprService: DaprService) => { useFactory: async (configService: ConfigService) => {
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 { return {
clientId: clientIdSecret['client-id'].toString(), clientId: configService.get<string>('clientId'),
clientSecret: clientSecret['client-secret'].toString(), clientSecret: configService.get<string>('clientSecret'),
tenantId: tenantIdSecret['tenant-id'].toString() tenantId: configService.get<string>('tenantId')
}; };
}, },
inject: [DaprService] inject: [ConfigService]
}), }),
AzureTableStorageModule.forRootAsync({ ConfigModule.forRoot({
imports: [DaprModule], load: [configuration]
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]
}), }),
DaprModule, FeatureFlagModule,
LogModule, LogModule,
MsGraphModule.registerAsync({ MsGraphModule.registerAsync({
imports: [DaprModule], imports: [ConfigModule],
useFactory: async (daprService: DaprService) => { useFactory: async (configService: ConfigService) => {
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 { return {
clientId: clientIdSecret['client-id'].toString(), clientId: configService.get<string>('clientId'),
clientSecret: clientSecret['client-secret'].toString(), clientSecret: configService.get<string>('clientSecret'),
tenantId: tenantIdSecret['tenant-id'].toString() tenantId: configService.get<string>('tenantId')
}; };
}, },
inject: [DaprService] inject: [ConfigService]
}), }),
PilotModule PilotModule
], ],

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,40 @@
import {
Controller,
Get,
HttpException,
Param,
} from '@nestjs/common';
import { FeatureFlagService } from './feature-flag.service';
import { CustomError, Public } from '@noahspan/noahspan-modules';
@Controller('featureFlags')
export class FeatureFlagController {
constructor(private readonly featureFlagService: FeatureFlagService) {}
@Get(':partitionKey/:rowKey')
@Public()
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()
@Public()
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 response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>(); const request = ctx.getRequest<Request>();
const status = excpetion.getStatus(); const status = excpetion.getStatus();
console.log(excpetion.cause);
response.status(status).json({ response.status(status).json({
name: excpetion.cause, name: excpetion.cause,

View File

@@ -48,7 +48,7 @@ export class LogController {
const log = new Log(); const log = new Log();
Object.assign(log, logDto); Object.assign(log, logDto);
console.log(log);
return await this.logService.create(log); return await this.logService.create(log);
} catch (error) { } catch (error) {
const customError = error as CustomError; const customError = error as CustomError;

View File

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

View File

@@ -5,8 +5,6 @@ import { v4 as uuidv4 } from 'uuid';
@Injectable() @Injectable()
export class LogService { export class LogService {
private readonly tableName = 'Logbook';
constructor( constructor(
@InjectRepository(Log) private readonly logRepository: Repository<Log> @InjectRepository(Log) private readonly logRepository: Repository<Log>
) {} ) {}

View File

@@ -1,32 +1,25 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { DaprModule, DaprService } from '@noahspan/noahspan-modules';
import { PilotController } from './pilot.controller'; import { PilotController } from './pilot.controller';
import { PilotService } from './pilot.service'; import { PilotService } from './pilot.service';
import { AzureTableStorageModule } from '@noahspan/azure-database'; import { AzureTableStorageModule } from '@noahspan/azure-database';
import { Pilot } from './pilot.entity'; import { Pilot } from './pilot.entity';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({ @Module({
imports: [ imports: [
AzureTableStorageModule.forRootAsync({ AzureTableStorageModule.forRootAsync({
imports: [DaprModule], imports: [ConfigModule],
useFactory: async (daprService: DaprService) => { useFactory: async (configService: ConfigService) => {
const connectionString = await daprService.daprClient.secret.get(
'key-vault',
'azure-storage-connection-string'
);
return { return {
connectionString: connectionString: configService.get<string>('azureStorageConnectionString')
connectionString['azure-storage-connection-string'].toString()
}; };
}, },
inject: [DaprService] inject: [ConfigService]
}), }),
AzureTableStorageModule.forFeature(Pilot, { AzureTableStorageModule.forFeature(Pilot, {
createTableIfNotExists: false, createTableIfNotExists: false,
table: 'pilots' table: 'pilots'
}), }),
DaprModule
], ],
controllers: [PilotController], controllers: [PilotController],
providers: [PilotService] providers: [PilotService]

View File

@@ -15,12 +15,17 @@ const App: React.FC<unknown> = () => {
useEffect(() => { useEffect(() => {
const getFeatureFlags = async () => { const getFeatureFlags = async () => {
try { try {
const featureFlagKeys: string = 'flying-pilots'; const featureFlags: { key: string; enabled: boolean }[] = []
const response: AxiosResponse = await httpClient.get( const response: AxiosResponse = await httpClient.get(
`api/featureFlags?keys=${featureFlagKeys}&label=${import.meta.env.MODE}` 'api/featureFlags'
); );
console.log(response);
const featureFlags: { key: string; enabled: boolean }[] = response.data; for (const featureFlag of response.data) {
featureFlags.push({
key: featureFlag.rowKey,
enabled: featureFlag.active === 'true' ? true : false
})
}
if (featureFlags.length > 0) { if (featureFlags.length > 0) {
appContext.dispatch({ appContext.dispatch({
@@ -40,7 +45,7 @@ const App: React.FC<unknown> = () => {
<div> <div>
<SiteNav /> <SiteNav />
<Routes> <Routes>
{useFeatureFlag('flying-pilots')?.enabled && ( {useFeatureFlag('pilots')?.enabled && (
<Route path="/pilots" element={<Pilots />} /> <Route path="/pilots" element={<Pilots />} />
)} )}
<Route path="/" element={<Logbook />} /> <Route path="/" element={<Logbook />} />

View File

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

View File

@@ -3,7 +3,6 @@ locals {
dev = "flying-app-dev" dev = "flying-app-dev"
} }
container_app_app_container_image = { container_app_app_container_image = {
dev = "noahspan/flying-app:v0.0.1" dev = "noahspan/flying-app:v0.0.1"
} }
@@ -32,6 +31,10 @@ locals {
dev = "flying-dev" dev = "flying-dev"
} }
logbook_feature_flag_active = {
dev = "true"
}
key_vault_name = { key_vault_name = {
dev = "noahspanflyingkeyvault" dev = "noahspanflyingkeyvault"
} }
@@ -40,11 +43,11 @@ locals {
dev = "flying-log-analytics-workspace-dev" dev = "flying-log-analytics-workspace-dev"
} }
pilots_feature_flag_active = {
dev = "true"
}
storage_account_name = { storage_account_name = {
dev = "noahspanflyingdev" dev = "noahspanflyingdev"
} }
user_assigned_identity_name = {
dev = "noahspanflyingdevuser"
}
} }

View File

@@ -34,14 +34,18 @@ output "key_vault_name" {
value = local.key_vault_name[var.environment] value = local.key_vault_name[var.environment]
} }
output "logbook_feature_flag_active" {
value = local.logbook_feature_flag_active[var.environment]
}
output "log_analytics_workspace_name" { output "log_analytics_workspace_name" {
value = local.log_analytics_workspace_name[var.environment] 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" { output "storage_account_name" {
value = local.storage_account_name[var.environment] value = local.storage_account_name[var.environment]
} }
output "user_assigned_identity_name" {
value = local.user_assigned_identity_name[var.environment]
}

View File

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

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

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

@@ -42,6 +42,26 @@ resource "azurerm_container_app" "container_app_api" {
image = module.environment.container_app_api_container_image image = module.environment.container_app_api_container_image
cpu = 0.25 cpu = 0.25
memory = "0.5Gi" 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"
}
} }
} }
@@ -62,6 +82,26 @@ resource "azurerm_container_app" "container_app_api" {
value = var.DOCKER_IO_PASSWORD value = var.DOCKER_IO_PASSWORD
} }
secret {
name = "azure-storage-connection-string"
value = var.AZURE_STORAGE_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 { lifecycle {
ignore_changes = [ template[0].container[0].image ] ignore_changes = [ template[0].container[0].image ]
} }

View File

@@ -4,11 +4,6 @@ resource "azurerm_storage_account" "storage_account" {
location = data.azurerm_resource_group.resource_group.location location = data.azurerm_resource_group.resource_group.location
account_tier = "Standard" account_tier = "Standard"
account_replication_type = "LRS" account_replication_type = "LRS"
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.user_assigned_identity.id]
}
} }
resource "azurerm_storage_table" "logs_table" { resource "azurerm_storage_table" "logs_table" {
@@ -20,3 +15,30 @@ resource "azurerm_storage_table" "pilot_table" {
name = "pilots" name = "pilots"
storage_account_name = azurerm_storage_account.storage_account.name 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,6 +6,10 @@ variable "APP_SUBDOMAIN_NAME" {
type = string type = string
} }
variable "AZURE_STORAGE_CONNECTION_STRING" {
type = string
}
variable "CLIENT_ID" { variable "CLIENT_ID" {
type = string type = string
} }