diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e838b3c..e09274a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: pull_request: push: branches: - - main - master - dev - feature/** @@ -81,30 +80,3 @@ jobs: - name: Test run: dotnet test ./JobFlow.API/JobFlow.API.csproj -c Release --no-build - api-e2e-playwright: - name: API E2E (Playwright) - runs-on: ubuntu-latest - if: ${{ secrets.JOBFLOW_API_BASE_URL != '' && secrets.JOBFLOW_API_BEARER_TOKEN != '' }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: npm - cache-dependency-path: tests/e2e/package-lock.json - - - name: Install E2E dependencies - working-directory: ./tests/e2e - run: npm ci - - - name: Run API Playwright tests - working-directory: ./tests/e2e - env: - API_BASE_URL: ${{ secrets.JOBFLOW_API_BASE_URL }} - JOBFLOW_API_BEARER_TOKEN: ${{ secrets.JOBFLOW_API_BEARER_TOKEN }} - JOBFLOW_ORGANIZATION_ID: ${{ secrets.JOBFLOW_ORGANIZATION_ID }} - run: npm run test:e2e diff --git a/.github/workflows/master_jobflow-api.yml b/.github/workflows/master_jobflow-api.yml index 65aa19c..1270b03 100644 --- a/.github/workflows/master_jobflow-api.yml +++ b/.github/workflows/master_jobflow-api.yml @@ -4,9 +4,6 @@ name: Build and deploy ASP.Net Core app to Azure Web App - jobflow-api on: - push: - branches: - - master workflow_dispatch: jobs: diff --git a/.github/workflows/staging_jobflow-api.yml b/.github/workflows/staging_jobflow-api.yml new file mode 100644 index 0000000..95efaa8 --- /dev/null +++ b/.github/workflows/staging_jobflow-api.yml @@ -0,0 +1,95 @@ +name: Build and deploy ASP.Net Core app to Azure Web App - jobflow-api-staging + +on: + push: + branches: + - master + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up .NET Core + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Build with dotnet + run: dotnet build JobFlow.API/JobFlow.API.csproj --configuration Release + + - name: dotnet publish + run: dotnet publish JobFlow.API/JobFlow.API.csproj --configuration Release --output ${{env.DOTNET_ROOT}}/myapp + + - name: Upload artifact for deployment job + uses: actions/upload-artifact@v4 + with: + name: .net-app + path: ${{env.DOTNET_ROOT}}/myapp + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: 'Staging' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write + contents: read + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: .net-app + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_15A959992577476DBE8A0461C48B98E2 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_0D7B12FDA6784264AD6F6B11A9FD4D54 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_9763D46E278F4180A2B5D8CC4B8B9E88 }} + + - name: Deploy to Azure Web App + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'jobflow-api-staging' + slot-name: 'Production' + package: . + + deploy-prod: + runs-on: ubuntu-latest + needs: deploy + environment: + name: 'Production' + url: ${{ steps.deploy-to-webapp.outputs.webapp-url }} + permissions: + id-token: write + contents: read + + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v4 + with: + name: .net-app + + - name: Login to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_15A959992577476DBE8A0461C48B98E2 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_0D7B12FDA6784264AD6F6B11A9FD4D54 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_9763D46E278F4180A2B5D8CC4B8B9E88 }} + + - name: Deploy to Azure Web App (prod) + id: deploy-to-webapp + uses: azure/webapps-deploy@v3 + with: + app-name: 'jobflow-api' + slot-name: 'Production' + package: . diff --git a/JobFlow.API/Program.cs b/JobFlow.API/Program.cs index 4a6d3d7..b0fb398 100644 --- a/JobFlow.API/Program.cs +++ b/JobFlow.API/Program.cs @@ -79,27 +79,42 @@ // FIREBASE INITIALIZATION // ============================================================ -var firebaseFilePath = Path.Combine(env.ContentRootPath, "job-flow-firebase-adminsdk.json"); - -if (!System.IO.File.Exists(firebaseFilePath)) - throw new InvalidOperationException($"Firebase service account file not found: {firebaseFilePath}"); - +var firebaseAdminSdkJson = builder.Configuration[ConfigConstants.FIREBASE_ADMIN_SDK]; string firebaseProjectId; -using (var doc = JsonDocument.Parse(System.IO.File.ReadAllText(firebaseFilePath))) +GoogleCredential firebaseCredential; + +if (!string.IsNullOrWhiteSpace(firebaseAdminSdkJson)) { + using var doc = JsonDocument.Parse(firebaseAdminSdkJson); firebaseProjectId = doc.RootElement.GetProperty("project_id").GetString() ?? ""; + var credential = CredentialFactory.FromJson(firebaseAdminSdkJson); + firebaseCredential = credential.ToGoogleCredential(); +} +else +{ + var firebaseFilePath = Path.Combine(env.ContentRootPath, "job-flow-firebase-adminsdk.json"); + + if (!System.IO.File.Exists(firebaseFilePath)) + throw new InvalidOperationException( + $"Firebase admin credentials were not found. Configure '{ConfigConstants.FIREBASE_ADMIN_SDK}' in Key Vault or provide local file: {firebaseFilePath}"); + + var firebaseJson = System.IO.File.ReadAllText(firebaseFilePath); + + using var doc = JsonDocument.Parse(firebaseJson); + firebaseProjectId = doc.RootElement.GetProperty("project_id").GetString() ?? ""; + var credential = CredentialFactory.FromFile(firebaseFilePath); + firebaseCredential = credential.ToGoogleCredential(); } if (string.IsNullOrWhiteSpace(firebaseProjectId)) - throw new InvalidOperationException("Firebase project_id is missing in job-flow-firebase-adminsdk.json"); + throw new InvalidOperationException("Firebase project_id is missing in configured Firebase admin credentials."); // Create the Firebase Admin default app instance so FirebaseAuth.DefaultInstance is available. if (FirebaseApp.DefaultInstance is null) { - var credential = CredentialFactory.FromFile(firebaseFilePath); FirebaseApp.Create(new AppOptions { - Credential = credential.ToGoogleCredential() + Credential = firebaseCredential }); } @@ -119,7 +134,7 @@ }) .AddJwtBearer("ClientPortalJwt", options => { - var signingKey = builder.Configuration["Auth:ClientPortal:SigningKey"]; + var signingKey = builder.Configuration["Auth-ClientPortal-SigningKey"]; if (string.IsNullOrWhiteSpace(signingKey)) throw new InvalidOperationException("Missing configuration: Auth:ClientPortal:SigningKey"); @@ -259,6 +274,8 @@ return host == "localhost" || host == "gojobflow.com" || host == "www.gojobflow.com" + || host == "jobflow-ui-web-staging.web.app" + || host == "jobflow-ui-web-staging.firebaseapp.com" || host.EndsWith(".gojobflow.app") || host.EndsWith(".gojobflow.com"); }) diff --git a/JobFlow.API/appsettings.Staging.json b/JobFlow.API/appsettings.Staging.json new file mode 100644 index 0000000..69d6d4b --- /dev/null +++ b/JobFlow.API/appsettings.Staging.json @@ -0,0 +1,16 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "KeyVaultUri": "https://jobflow-staging.vault.azure.net/", + "Frontend": { + "BaseUrl": "https://staging.gojobflow.com" + }, + "Backend": { + "BaseUrl": "https://api.staging.gojobflow.com" + } +} \ No newline at end of file diff --git a/JobFlow.API/job-flow-firebase-adminsdk.json b/JobFlow.API/job-flow-firebase-adminsdk.json index 977d90a..7810b0d 100644 --- a/JobFlow.API/job-flow-firebase-adminsdk.json +++ b/JobFlow.API/job-flow-firebase-adminsdk.json @@ -1,13 +1,13 @@ { "type": "service_account", - "project_id": "jobflow-ui-web", - "private_key_id": "e9c3df7ce12c0500cc61cc285934a73ed8565480", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC66EWCPmbUwl25\nzROu+2tcDnbQlzMJ386pcx+YRGee5w/Q1al1zEls4/f8C7gOkTG4Ki9P9GCBOWGZ\nI41l+J3KiMsFVPb8amRtjyyLHjwD4tcbyl3DE5RGD2fS3LMNeWMxtwehKtV+6asc\nBEtMeswbrBC3vXQCe2AjDCYYsyxzOU+Qp2JQT2GNr3brZaow1nPqu6IAz7f92DBC\n44ezV3IJC0vYhIQq4bBngB97cC2hYI+f7ll3blYljccsDKQXxaNbQUJ1K+QJIlhg\nSSJ11SKcMk7a2l7KVknUligLrdllS/m652vV609nITmxlAGKUmjgle8YNLs0srFW\nSbjLZk53AgMBAAECggEAQPxScqMEuPPth5UUw3HaVbMXv5XaopPE9Ki48wXRq2+2\nUYuAdJs3altnFSTz9WipS1mrgpa62SNc2lSArNRA9LMUN8HfcEsDqQ4vVB2Ki2Vb\nGmgFqraLhsKDfE7NGKG8igQT7IcKnSpcmoypq6lEf1iXpXMDO3uvJPBr7ImbqmHK\nOJi+xO7z85fT//7gRh5I0I1RROJeH8go6OssnIVrgxSs8EI7Ai7HGMv1yxPlmGv1\n5naVvHpFRq18KrzU8LmirIwMByQC9IKJKPZtMmgNdPYc6YYKgfYIHhSXmJiBNPq3\nQQU/0bc3MTzUDNnnbIPwtj3San5jhRoiNVVX/rYEFQKBgQDc0wOaFJsoc82yn9RH\nwjNNHjyXllf6h4iNgcAYT8K7NVLg0KDppI64dFMw0ZKLd506QLevNFG/Kjcf9JgQ\ndQ2IkpGb4lK3vh8+lSdVK8Ngne35g6mPCTgNYUDbApb7KDf2rnwLcRh4WQTHP7w5\nljAXuzIN2wAyI+4onz84R8HJlQKBgQDYriig7qGdMNNfwfSHJEYmukWk/f61VQfX\n/rK48MXfDv4fq0YUHQvBDmQYAFN3hhbfCwa8Sw33Bbu49Zjydi8R+r7eBT7Vvv/e\nbeID1fYZdJQ5kotS3/IcYqck1ecd/X2SG9sc/0RVkeII1KcdYiVeyeCZr+zXQ8+a\nNTTUrERs2wKBgQDECCNjbjWLRLpvfwmRJmoqZNQ/cbzqb9UeYffo3S2uyZiocSzY\nHTiBsOqFJRal7urJ4tftllGXld9X4+f2fCMmgY73xoPOD95mzTwclPwd0jWHUoV8\nsB9taU+M3RCxJ7P+rkj6U0z40XW3d/IdYSGSf6DgwfC7kkADGdOin7j9vQKBgGR5\nFWPSY2RVQJ5VfIKxwkmw9BxWnqYMwK9abhstokMVW6bpr3wiH9IsTyOF+y4gIjjY\njw3+q4IQyYQxdfNv89GdeKXQvts0TscgIr5ul0gkc5ripfIO3+Bjqmd9PEb+xRxc\nCFVA1LntBGfd24PXf8adS6VYGzWSPxCdfVrkanIjAoGAHhrAzZqNJSQBISFWhk3G\n97EV8+AUSelIObsXQhQoEtlpEggffdKx0nC+i1nggkiB/WMa8ZxTCNB+ri3IfPeV\nrpjnDROjcWyZlHTp/4LPEDVIh4QvZWdial+A/v09/xErhDILyAlIrWEVCX9IcJm2\nJJ5pfEGgcIVSEGMV4cKPxAc=\n-----END PRIVATE KEY-----\n", - "client_email": "firebase-adminsdk-fbsvc@jobflow-ui-web.iam.gserviceaccount.com", - "client_id": "115912080472474571179", + "project_id": "jobflow-ui-web-staging", + "private_key_id": "9708ddf5828958edeb9a3fe8b2d2bb2c8be9933f", + "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJlI2Plx+znQi7\nrsq0ZOZeoLTqo5pbczPj42DwLf+YNEXvyRVrVruyqXhffKUxDGHmR3zxslUsT1f/\nKjWh09y2fnRXH1su2f90Kax4Lff1chhqMH6/J/CNK6KulBnjCcWks1F5GBCwsEw8\nCA44cJmVYnGAyqrQvNkELBYgWiIblE8kWFlPviZyyB9krEEJo7U8T9cA/sc3NI1j\nRKfx7S97koNgIsMht+TY92hDwClGVGWL7v1cgkeKmll6NhEEJpUr8amtrTplSjJB\nykq1FjmPmv6cao2JOGPNQRv2rt0xHKNCNQjPMgQetlX3HGauNDQk+PqPLxeq1b4S\npMqPMFPrAgMBAAECggEAE2uU5yBbmlhxfBy/BE8eOeqd/hGsms0yqBdYAlD5frE2\niv836Y3tv6zVCc9T3jGqXYmbRMZ2BIf7sHu2trRXCgaNGyGhuBX3fDpRlohzNY8m\n7AASY0SBR9B24qkmWfmvNAq0jt0zKm/pqvTpuHqcnp2L1X5w+Mg2LiaN1n291c3c\nTcak9qN1Fnfw/uhVuRr4Dws2+xOEVTwLEfJpzPa/hJnGhNUAdCR61GRgG/PQ1Xqw\n1BnP8w8dRjah5aeefUziUyiao/g3mXh2XtyXuA5OBEfGAFf01yWBkbB2Z6CEuP+U\nPtLcVSnmhQW0+MCeFovltokkjShpdis7KdjmBHg8EQKBgQDy0TRPM/6QTh7B9dOg\nRUNh6zpnOFC0Or0NXbFPJObzqnljeKjS1MPFJ5HtzEDerxfsyy8HVux91k8BkdrG\npFSrCtRyr2VC1cjK21sA2S8uAvQ+dVZIDY8ozhpKGuZMwiK03BF/n13Wk+MMRuAq\n9quOpHj6hKkCOQh3IRkOT/xP+wKBgQDUhjeZlQLGf0fMq4XzGs45ppIGw9dyu1yj\nvIF7rpzqnAgtqI4VzLkhcskEvDm+WdCgnjSvEKGbZq4Dm5v9Obm5T3RGZdIhkLSx\ns2taL6tbznq4SDGBGgMzJl8rAHXX2P+SHRyCIzrYnz6ZfAB2KffoTBmsHMjPikbq\nMsnWPEKY0QKBgAtjqLJ2W+Bk6ahrYWvJE+oJ4Ilq6M4rWya/WEvADV0sh9kUlcad\n2DjtLDkdNYW8bMDcnu4XM6yLWtVWBA8BMj97mI9wjq1d3bc2JsSZa08bMF2ln1Bt\n4mMll7IWJOtAx+P31pJH5VzlPucag/U/8LgWGt6VTmAeULlVwhkbw1f1AoGBALUS\nfgDO4wR4oZYSdhhBOIAKGdTFu6U3WaDwFWppxaxmsNkmCZktSnbjM75jGNfD8mtH\nICAgjXC4NX9Bb9B7BHCM78ajLjwG7M2Szt6SSu/3pruoVvVmUl+cS+15gO4dJvM4\n9ncyyQqT82QWMNZ8v4oefKkWBUo+yFj2WN29jghhAoGAVKAE8JxD20qiAbWk1RKm\n6T2TyDiZU/nG4MGPkRuR0OmtEszoLkk5o968IVEdK4v2y9XLCV+ke2PTgfVNEcKg\nLRBFitx6yVlzUddDTkdSnyYtl6S4LRX1XKk2Rur8fCgHd59bEb1CLxiFm9e7B5zW\ntHB937uJRztg2Dy8GnipZQw=\n-----END PRIVATE KEY-----\n", + "client_email": "firebase-adminsdk-fbsvc@jobflow-ui-web-staging.iam.gserviceaccount.com", + "client_id": "115002767484169273887", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40jobflow-ui-web.iam.gserviceaccount.com", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40jobflow-ui-web-staging.iam.gserviceaccount.com", "universe_domain": "googleapis.com" } \ No newline at end of file