diff --git a/.vscode/api-tests.http b/.vscode/api-tests.http new file mode 100644 index 0000000..334b9c5 --- /dev/null +++ b/.vscode/api-tests.http @@ -0,0 +1,141 @@ +############################################################################### +# Luminous API Test Requests +############################################################################### +# Use with the REST Client VS Code extension (humao.rest-client) +# +# Usage: +# 1. Start the API (F5 or run "API: Debug" task) +# 2. Click "Send Request" above any request +# 3. View response in the split panel +# +# Environment variables are defined in .vscode/settings.json +############################################################################### + +@baseUrl = http://localhost:5000 +@contentType = application/json + +############################################################################### +# Health & Status +############################################################################### + +### Health Check +GET {{baseUrl}}/health + +### Check Dev Auth Status +GET {{baseUrl}}/api/devauth/status + +############################################################################### +# Development Authentication +############################################################################### + +### Get Default Dev Token +# @name getToken +POST {{baseUrl}}/api/devauth/token + +### Store token from response +@token = {{getToken.response.body.accessToken}} + +### Get Custom Dev Token (Owner role) +POST {{baseUrl}}/api/devauth/token/custom +Content-Type: {{contentType}} + +{ + "userId": "test-user-001", + "familyId": "test-family-001", + "email": "owner@luminous.local", + "role": "Owner", + "displayName": "Test Owner" +} + +### Get Custom Dev Token (Child role) +POST {{baseUrl}}/api/devauth/token/custom +Content-Type: {{contentType}} + +{ + "userId": "child-user-001", + "familyId": "test-family-001", + "email": "child@luminous.local", + "role": "Child", + "displayName": "Test Child" +} + +### Get Custom Dev Token (Caregiver role) +POST {{baseUrl}}/api/devauth/token/custom +Content-Type: {{contentType}} + +{ + "userId": "caregiver-001", + "familyId": "test-family-001", + "email": "nanny@example.com", + "role": "Caregiver", + "displayName": "Test Caregiver" +} + +############################################################################### +# Family API (requires authentication) +############################################################################### + +### Get Family Details +GET {{baseUrl}}/api/families +Authorization: Bearer {{token}} + +### Create Family +POST {{baseUrl}}/api/families +Content-Type: {{contentType}} +Authorization: Bearer {{token}} + +{ + "name": "Smith Family", + "timezone": "America/New_York" +} + +############################################################################### +# User API (requires authentication) +############################################################################### + +### Get Family Members +GET {{baseUrl}}/api/users +Authorization: Bearer {{token}} + +### Get Current User Profile +GET {{baseUrl}}/api/users/me +Authorization: Bearer {{token}} + +############################################################################### +# Events API (requires authentication) +############################################################################### + +### Get Events +GET {{baseUrl}}/api/events +Authorization: Bearer {{token}} + +### Get Events for Date Range +GET {{baseUrl}}/api/events?startDate=2025-01-01&endDate=2025-01-31 +Authorization: Bearer {{token}} + +############################################################################### +# Chores API (requires authentication) +############################################################################### + +### Get Chores +GET {{baseUrl}}/api/chores +Authorization: Bearer {{token}} + +############################################################################### +# Devices API (requires authentication) +############################################################################### + +### Generate Device Link Code +POST {{baseUrl}}/api/devices/link-code +Authorization: Bearer {{token}} + +### Get Linked Devices +GET {{baseUrl}}/api/devices +Authorization: Bearer {{token}} + +############################################################################### +# Swagger / OpenAPI +############################################################################### + +### Get OpenAPI Spec +GET {{baseUrl}}/swagger/v1/swagger.json diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..25d63f4 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,73 @@ +{ + "recommendations": [ + // ========================================================================== + // C# / .NET Development + // ========================================================================== + "ms-dotnettools.csdevkit", + "ms-dotnettools.csharp", + "ms-dotnettools.vscode-dotnet-runtime", + + // ========================================================================== + // Angular / TypeScript Development + // ========================================================================== + "angular.ng-template", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + + // ========================================================================== + // Tailwind CSS + // ========================================================================== + "bradlc.vscode-tailwindcss", + + // ========================================================================== + // Docker + // ========================================================================== + "ms-azuretools.vscode-docker", + + // ========================================================================== + // Azure / Bicep + // ========================================================================== + "ms-azuretools.vscode-bicep", + "ms-vscode.azure-account", + "ms-azuretools.vscode-azurefunctions", + + // ========================================================================== + // API Development + // ========================================================================== + "humao.rest-client", + "42crunch.vscode-openapi", + + // ========================================================================== + // Git + // ========================================================================== + "eamodio.gitlens", + "mhutchie.git-graph", + + // ========================================================================== + // Productivity + // ========================================================================== + "christian-kohler.path-intellisense", + "streetsidesoftware.code-spell-checker", + "usernamehw.errorlens", + "gruntfuggly.todo-tree", + + // ========================================================================== + // Markdown / Documentation + // ========================================================================== + "yzhang.markdown-all-in-one", + "bierner.markdown-mermaid", + + // ========================================================================== + // Testing + // ========================================================================== + "ms-vscode.test-adapter-converter", + "hbenl.vscode-test-explorer", + + // ========================================================================== + // Editor Enhancements + // ========================================================================== + "editorconfig.editorconfig", + "aaron-bond.better-comments" + ], + "unwantedRecommendations": [] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..b0345a2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,172 @@ +{ + "version": "0.2.0", + "configurations": [ + // ========================================================================== + // .NET API Debugging + // ========================================================================== + { + "name": "API: Debug", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "API: Build", + "program": "${workspaceFolder}/src/Luminous.Api/bin/Debug/net10.0/Luminous.Api.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Luminous.Api", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "http://localhost:5000" + }, + "console": "integratedTerminal", + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)", + "uriFormat": "%s/swagger" + } + }, + { + "name": "API: Debug (HTTPS)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "API: Build", + "program": "${workspaceFolder}/src/Luminous.Api/bin/Debug/net10.0/Luminous.Api.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Luminous.Api", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:5001;http://localhost:5000" + }, + "console": "integratedTerminal", + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)", + "uriFormat": "%s/swagger" + } + }, + { + "name": "API: Attach to Process", + "type": "coreclr", + "request": "attach", + "processId": "${command:pickProcess}" + }, + + // ========================================================================== + // .NET Tests Debugging + // ========================================================================== + { + "name": "Tests: Debug Domain", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Solution: Build All", + "program": "dotnet", + "args": [ + "test", + "${workspaceFolder}/tests/Luminous.Domain.Tests/Luminous.Domain.Tests.csproj", + "--no-build" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": "Tests: Debug Application", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Solution: Build All", + "program": "dotnet", + "args": [ + "test", + "${workspaceFolder}/tests/Luminous.Application.Tests/Luminous.Application.Tests.csproj", + "--no-build" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": "Tests: Debug API", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Solution: Build All", + "program": "dotnet", + "args": [ + "test", + "${workspaceFolder}/tests/Luminous.Api.Tests/Luminous.Api.Tests.csproj", + "--no-build" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "stopAtEntry": false + }, + { + "name": "Tests: Debug All", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "Solution: Build All", + "program": "dotnet", + "args": ["test", "--no-build"], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "stopAtEntry": false + }, + + // ========================================================================== + // Angular Web App Debugging + // ========================================================================== + { + "name": "Web: Debug in Chrome", + "type": "chrome", + "request": "launch", + "preLaunchTask": "Web: Start Dev Server", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}/clients/web/src", + "sourceMapPathOverrides": { + "webpack:/*": "${webRoot}/*" + } + }, + { + "name": "Web: Debug in Edge", + "type": "msedge", + "request": "launch", + "preLaunchTask": "Web: Start Dev Server", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}/clients/web/src", + "sourceMapPathOverrides": { + "webpack:/*": "${webRoot}/*" + } + }, + { + "name": "Web: Attach to Chrome", + "type": "chrome", + "request": "attach", + "port": 9222, + "webRoot": "${workspaceFolder}/clients/web/src" + } + ], + "compounds": [ + // ========================================================================== + // Full Stack Debugging + // ========================================================================== + { + "name": "Full Stack: API + Web (Chrome)", + "configurations": ["API: Debug", "Web: Debug in Chrome"], + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Full Stack", + "order": 1 + } + }, + { + "name": "Full Stack: API + Web (Edge)", + "configurations": ["API: Debug", "Web: Debug in Edge"], + "stopAll": true, + "presentation": { + "hidden": false, + "group": "Full Stack", + "order": 2 + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..de5bf3b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,217 @@ +{ + // ========================================================================== + // Editor Settings + // ========================================================================== + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "explicit" + }, + "editor.rulers": [100, 120], + "editor.bracketPairColorization.enabled": true, + "editor.guides.bracketPairs": true, + + // ========================================================================== + // File Settings + // ========================================================================== + "files.trimTrailingWhitespace": true, + "files.insertFinalNewline": true, + "files.exclude": { + "**/bin": true, + "**/obj": true, + "**/node_modules": true, + "**/.angular": true, + "**/dist": true, + "**/.git": true + }, + "files.watcherExclude": { + "**/bin/**": true, + "**/obj/**": true, + "**/node_modules/**": true, + "**/.angular/**": true, + "**/dist/**": true + }, + + // ========================================================================== + // Search Settings + // ========================================================================== + "search.exclude": { + "**/bin": true, + "**/obj": true, + "**/node_modules": true, + "**/.angular": true, + "**/dist": true, + "**/coverage": true, + "**/*.min.js": true, + "**/*.min.css": true + }, + + // ========================================================================== + // C# / .NET Settings + // ========================================================================== + "dotnet.defaultSolution": "Luminous.sln", + "omnisharp.enableEditorConfigSupport": true, + "omnisharp.enableRoslynAnalyzers": true, + "[csharp]": { + "editor.defaultFormatter": "ms-dotnettools.csharp", + "editor.tabSize": 4, + "editor.insertSpaces": true + }, + + // ========================================================================== + // TypeScript / Angular Settings + // ========================================================================== + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.updateImportsOnFileMove.enabled": "always", + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + "[html]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + "[scss]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + + // ========================================================================== + // Angular Settings + // ========================================================================== + "angular.enable-strict-mode-prompt": false, + + // ========================================================================== + // Tailwind CSS Settings + // ========================================================================== + "tailwindCSS.includeLanguages": { + "html": "html", + "typescript": "javascript" + }, + "tailwindCSS.experimental.classRegex": [ + ["class\\s*=\\s*['\"]([^'\"]*)['\"]", "([a-zA-Z0-9\\-:]+)"], + ["[a-zA-Z]+Class\\s*=\\s*['\"]([^'\"]*)['\"]", "([a-zA-Z0-9\\-:]+)"] + ], + + // ========================================================================== + // ESLint Settings + // ========================================================================== + "eslint.workingDirectories": ["./clients/web"], + "eslint.validate": ["javascript", "typescript", "html"], + + // ========================================================================== + // Prettier Settings + // ========================================================================== + "prettier.singleQuote": true, + "prettier.trailingComma": "es5", + + // ========================================================================== + // Bicep Settings + // ========================================================================== + "[bicep]": { + "editor.defaultFormatter": "ms-azuretools.vscode-bicep", + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + + // ========================================================================== + // JSON Settings + // ========================================================================== + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.tabSize": 2, + "editor.insertSpaces": true + }, + + // ========================================================================== + // Markdown Settings + // ========================================================================== + "[markdown]": { + "editor.wordWrap": "on", + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": false + } + }, + + // ========================================================================== + // Docker Settings + // ========================================================================== + "docker.containers.label": "ContainerName", + "docker.containers.sortBy": "Label", + + // ========================================================================== + // Git Settings + // ========================================================================== + "git.autofetch": true, + "git.confirmSync": false, + "git.enableSmartCommit": true, + "gitlens.currentLine.enabled": true, + + // ========================================================================== + // Testing Settings + // ========================================================================== + "dotnet-test-explorer.testProjectPath": "**/*Tests.csproj", + + // ========================================================================== + // Todo Tree Settings + // ========================================================================== + "todo-tree.general.tags": [ + "BUG", + "HACK", + "FIXME", + "TODO", + "XXX", + "[ ]", + "[x]" + ], + "todo-tree.highlights.defaultHighlight": { + "icon": "alert", + "type": "text", + "foreground": "#FF8C00", + "background": "#1e1e1e", + "opacity": 50, + "iconColour": "#FF8C00" + }, + + // ========================================================================== + // REST Client Settings + // ========================================================================== + "rest-client.environmentVariables": { + "$shared": {}, + "development": { + "baseUrl": "http://localhost:5000", + "token": "" + }, + "staging": { + "baseUrl": "https://staging-api.luminous.app", + "token": "" + } + }, + + // ========================================================================== + // Spell Checker Settings + // ========================================================================== + "cSpell.words": [ + "Azurite", + "bicep", + "blazor", + "cosmosdb", + "Luminous", + "mailhog", + "nanoid", + "passkey", + "passkeys", + "TOGAF", + "webauthn" + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..17b2731 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,320 @@ +{ + "version": "2.0.0", + "tasks": [ + // ========================================================================== + // Docker Services + // ========================================================================== + { + "label": "Docker: Start All Services", + "type": "shell", + "command": "docker compose up -d", + "problemMatcher": [], + "group": "none", + "presentation": { + "reveal": "always", + "panel": "shared" + } + }, + { + "label": "Docker: Stop All Services", + "type": "shell", + "command": "docker compose down", + "problemMatcher": [], + "group": "none" + }, + { + "label": "Docker: View Logs", + "type": "shell", + "command": "docker compose logs -f", + "problemMatcher": [], + "group": "none", + "isBackground": true + }, + { + "label": "Docker: Reset (Remove Volumes)", + "type": "shell", + "command": "docker compose down -v", + "problemMatcher": [], + "group": "none" + }, + + // ========================================================================== + // .NET API + // ========================================================================== + { + "label": "API: Build", + "type": "shell", + "command": "dotnet", + "args": ["build", "--no-restore"], + "options": { + "cwd": "${workspaceFolder}/src/Luminous.Api" + }, + "group": "build", + "problemMatcher": "$msCompile" + }, + { + "label": "API: Run", + "type": "shell", + "command": "dotnet", + "args": ["run", "--no-build"], + "options": { + "cwd": "${workspaceFolder}/src/Luminous.Api" + }, + "group": "none", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*Starting.*$", + "endsPattern": "^.*Now listening on.*$" + } + } + }, + { + "label": "API: Watch (Hot Reload)", + "type": "shell", + "command": "dotnet", + "args": ["watch", "run"], + "options": { + "cwd": "${workspaceFolder}/src/Luminous.Api" + }, + "group": "none", + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*dotnet watch.*$", + "endsPattern": "^.*Now listening on.*$" + } + } + }, + { + "label": "API: Restore Packages", + "type": "shell", + "command": "dotnet", + "args": ["restore"], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": "$msCompile" + }, + + // ========================================================================== + // Solution-wide + // ========================================================================== + { + "label": "Solution: Build All", + "type": "shell", + "command": "dotnet", + "args": ["build"], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Solution: Clean", + "type": "shell", + "command": "dotnet", + "args": ["clean"], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Solution: Test All", + "type": "shell", + "command": "dotnet", + "args": ["test", "--no-build"], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": "$msCompile" + }, + { + "label": "Solution: Test with Coverage", + "type": "shell", + "command": "dotnet", + "args": [ + "test", + "--collect:\"XPlat Code Coverage\"", + "--results-directory", + "${workspaceFolder}/coverage" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "problemMatcher": "$msCompile" + }, + + // ========================================================================== + // Angular Web App + // ========================================================================== + { + "label": "Web: Install Dependencies", + "type": "shell", + "command": "npm", + "args": ["install"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "problemMatcher": [] + }, + { + "label": "Web: Start Dev Server", + "type": "shell", + "command": "npm", + "args": ["start"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "isBackground": true, + "problemMatcher": { + "pattern": { + "regexp": "^$" + }, + "background": { + "activeOnStart": true, + "beginsPattern": "^.*Compiling.*$", + "endsPattern": "^.*Compiled successfully.*$" + } + } + }, + { + "label": "Web: Build", + "type": "shell", + "command": "npm", + "args": ["run", "build"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "group": "build", + "problemMatcher": [] + }, + { + "label": "Web: Build Production", + "type": "shell", + "command": "npm", + "args": ["run", "build:prod"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "group": "build", + "problemMatcher": [] + }, + { + "label": "Web: Test", + "type": "shell", + "command": "npm", + "args": ["test"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "group": "test", + "problemMatcher": [] + }, + { + "label": "Web: Test (CI Mode)", + "type": "shell", + "command": "npm", + "args": ["run", "test:ci"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "group": "test", + "problemMatcher": [] + }, + { + "label": "Web: Lint", + "type": "shell", + "command": "npm", + "args": ["run", "lint"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "problemMatcher": ["$eslint-stylish"] + }, + { + "label": "Web: Type Check", + "type": "shell", + "command": "npm", + "args": ["run", "typecheck"], + "options": { + "cwd": "${workspaceFolder}/clients/web" + }, + "problemMatcher": "$tsc" + }, + + // ========================================================================== + // Development Environment + // ========================================================================== + { + "label": "Dev: Start Full Stack", + "dependsOn": [ + "Docker: Start All Services", + "API: Watch (Hot Reload)", + "Web: Start Dev Server" + ], + "dependsOrder": "sequence", + "problemMatcher": [], + "group": "none" + }, + { + "label": "Dev: Get Auth Token", + "type": "shell", + "command": "curl", + "args": [ + "-s", + "-X", "POST", + "http://localhost:5000/api/devauth/token", + "|", + "jq", "." + ], + "problemMatcher": [], + "presentation": { + "reveal": "always", + "panel": "new" + } + }, + + // ========================================================================== + // Infrastructure + // ========================================================================== + { + "label": "Infra: Validate Bicep", + "type": "shell", + "command": "az", + "args": ["bicep", "build", "--file", "infra/bicep/main.bicep"], + "problemMatcher": [] + }, + { + "label": "Infra: Deploy (Dev)", + "type": "shell", + "command": "${workspaceFolder}/infra/scripts/deploy.sh", + "args": ["dev"], + "problemMatcher": [] + }, + { + "label": "Infra: What-If (Dev)", + "type": "shell", + "command": "${workspaceFolder}/infra/scripts/deploy.sh", + "args": ["dev", "--what-if"], + "problemMatcher": [] + } + ] +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..85c9fb7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,175 @@ +# ============================================================================= +# Luminous - Local Development Docker Compose +# ============================================================================= +# Provides local development services that emulate Azure cloud services. +# +# Usage: +# docker compose up -d # Start all services +# docker compose up -d cosmosdb redis # Start specific services +# docker compose down # Stop all services +# docker compose down -v # Stop and remove volumes +# +# Services: +# - cosmosdb: Azure Cosmos DB Emulator (port 8081) +# - azurite: Azure Storage Emulator (Blob: 10000, Queue: 10001, Table: 10002) +# - redis: Redis Cache (port 6379) +# - mailpit: Email testing server (SMTP: 1025, Web UI: 8025) +# +# ARM64 Support (Apple Silicon M1/M2/M3): +# All services are compatible with ARM64 architecture. +# See docs/DEVELOPMENT.md for platform-specific notes. +# ============================================================================= + +services: + # --------------------------------------------------------------------------- + # Azure Cosmos DB Emulator + # --------------------------------------------------------------------------- + # Documentation: https://learn.microsoft.com/en-us/azure/cosmos-db/emulator + # Default Key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw== + # + # ARM64 Note: The Linux emulator supports ARM64 natively as of late 2023. + # First startup may take 3-5 minutes on Apple Silicon. + # --------------------------------------------------------------------------- + cosmosdb: + image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest + container_name: luminous-cosmosdb + hostname: cosmosdb + # Platform auto-detected; explicitly set if needed: + # platform: linux/amd64 # Use Rosetta 2 emulation on Apple Silicon + # platform: linux/arm64 # Use native ARM64 (recommended for M1/M2/M3) + mem_limit: 3g + cpu_count: 2 + environment: + - AZURE_COSMOS_EMULATOR_PARTITION_COUNT=10 + - AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=true + - AZURE_COSMOS_EMULATOR_IP_ADDRESS_OVERRIDE=127.0.0.1 + ports: + - "8081:8081" # HTTPS endpoint + - "10250:10250" # Gateway port + - "10251:10251" # Data partition 1 + - "10252:10252" # Data partition 2 + - "10253:10253" # Data partition 3 + - "10254:10254" # Data partition 4 + - "10255:10255" # Data partition 5 + volumes: + - cosmosdb_data:/tmp/cosmos/appdata + networks: + - luminous-network + healthcheck: + test: ["CMD-SHELL", "curl -fks https://localhost:8081/_explorer/emulator.pem || exit 1"] + interval: 30s + timeout: 15s + retries: 15 + start_period: 180s # ARM64 may need longer startup time + + # --------------------------------------------------------------------------- + # Azurite - Azure Storage Emulator + # --------------------------------------------------------------------------- + # Documentation: https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite + # Connection String: DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1; + # + # ARM64 Note: Azurite has native ARM64 support. + # --------------------------------------------------------------------------- + azurite: + image: mcr.microsoft.com/azure-storage/azurite:latest + container_name: luminous-azurite + hostname: azurite + command: "azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0 --loose" + ports: + - "10000:10000" # Blob service + - "10001:10001" # Queue service + - "10002:10002" # Table service + volumes: + - azurite_data:/data + networks: + - luminous-network + + # --------------------------------------------------------------------------- + # Redis Cache + # --------------------------------------------------------------------------- + # Documentation: https://redis.io/docs/ + # Connection: localhost:6379 + # + # ARM64 Note: Redis Alpine has native ARM64 support. + # --------------------------------------------------------------------------- + redis: + image: redis:7-alpine + container_name: luminous-redis + hostname: redis + command: redis-server --appendonly yes + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - luminous-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + # --------------------------------------------------------------------------- + # Mailpit - Email Testing Server (replaces MailHog) + # --------------------------------------------------------------------------- + # Documentation: https://github.com/axllent/mailpit + # SMTP: localhost:1025 + # Web UI: http://localhost:8025 + # + # Note: Mailpit is a modern replacement for MailHog with better ARM64 support + # and active maintenance. API-compatible with MailHog. + # --------------------------------------------------------------------------- + mailpit: + image: axllent/mailpit:latest + container_name: luminous-mailpit + hostname: mailpit + environment: + - MP_SMTP_AUTH_ACCEPT_ANY=true + - MP_SMTP_AUTH_ALLOW_INSECURE=true + ports: + - "1025:1025" # SMTP server + - "8025:8025" # Web UI + networks: + - luminous-network + + # --------------------------------------------------------------------------- + # Redis Commander - Redis Web UI (Optional) + # --------------------------------------------------------------------------- + # Web UI: http://localhost:8082 + # + # ARM64 Note: May require Rosetta 2 emulation on Apple Silicon. + # --------------------------------------------------------------------------- + redis-commander: + image: rediscommander/redis-commander:latest + container_name: luminous-redis-commander + hostname: redis-commander + platform: linux/amd64 # Force x86_64 emulation if no ARM64 image + environment: + - REDIS_HOSTS=local:redis:6379 + ports: + - "8082:8081" + depends_on: + - redis + networks: + - luminous-network + profiles: + - tools + +# ============================================================================= +# Volumes +# ============================================================================= +volumes: + cosmosdb_data: + name: luminous-cosmosdb-data + azurite_data: + name: luminous-azurite-data + redis_data: + name: luminous-redis-data + +# ============================================================================= +# Networks +# ============================================================================= +networks: + luminous-network: + name: luminous-network + driver: bridge diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..7172819 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,651 @@ +# Luminous Local Development Guide + +> **Document Version:** 1.2.0 +> **Last Updated:** 2025-12-22 +> **Status:** Active + +This guide covers setting up and running the Luminous development environment locally. + +--- + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Quick Start](#quick-start) +3. [Docker Services](#docker-services) +4. [Running the API](#running-the-api) +5. [Running the Web App](#running-the-web-app) +6. [Development Authentication](#development-authentication) +7. [Cosmos DB Emulator](#cosmos-db-emulator) +8. [Troubleshooting](#troubleshooting) + +--- + +## Prerequisites + +### Required Software + +| Software | Version | Purpose | +|----------|---------|---------| +| **Docker Desktop** | Latest | Container runtime for local services | +| **.NET SDK** | 10.0+ | Backend API development | +| **Node.js** | 20+ | Frontend development | +| **npm** | 10+ | Package management | + +### Optional Tools + +| Software | Purpose | +|----------|---------| +| **Visual Studio Code** | Recommended IDE with extensions | +| **Azure Data Studio** | CosmosDB data management | +| **Redis Insight** | Redis data visualization | + +### System Requirements + +The Cosmos DB Emulator has specific requirements: + +- **RAM**: Minimum 3GB available for Docker +- **CPU**: 2+ cores recommended +- **Disk**: 10GB free space for containers and data + +### Apple Silicon (M1/M2/M3) Support + +All Docker services are compatible with ARM64 architecture: + +| Service | ARM64 Support | Notes | +|---------|---------------|-------| +| **Cosmos DB Emulator** | Native | First startup takes 3-5 minutes | +| **Azurite** | Native | No special configuration needed | +| **Redis** | Native | No special configuration needed | +| **Mailpit** | Native | Replaces MailHog for better ARM64 support | + +**Docker Desktop Settings for Apple Silicon:** + +1. Open Docker Desktop > Settings > General +2. Ensure "Use Virtualization framework" is enabled +3. Under Resources, allocate at least 4GB RAM (6GB recommended) +4. Enable "Use Rosetta for x86/amd64 emulation" for any x86 containers + +**First-Time Setup:** +```bash +# Pull images (may take a few minutes) +docker compose pull + +# Start services (Cosmos DB takes 3-5 minutes on first run) +docker compose up -d + +# Monitor startup progress +docker compose logs -f cosmosdb +``` + +--- + +## Quick Start + +### Using the Start Script + +The easiest way to start development is using the provided scripts: + +**macOS/Linux:** +```bash +# Start all Docker services +./scripts/dev-start.sh + +# In a new terminal, start the API +./scripts/dev-start.sh --api + +# In another terminal, start the web app +./scripts/dev-start.sh --web +``` + +**Windows (PowerShell):** +```powershell +# Start all Docker services +.\scripts\dev-start.ps1 + +# In a new terminal, start the API +.\scripts\dev-start.ps1 -Api + +# In another terminal, start the web app +.\scripts\dev-start.ps1 -Web +``` + +### Manual Setup + +If you prefer manual control: + +```bash +# 1. Start Docker services +docker compose up -d + +# 2. Start the .NET API +cd src/Luminous.Api +dotnet watch run + +# 3. Start the Angular app (new terminal) +cd clients/web +npm install +npm start +``` + +--- + +## Docker Services + +### Overview + +The `docker-compose.yml` file provides these local services: + +| Service | Port | Description | +|---------|------|-------------| +| **cosmosdb** | 8081 | Azure Cosmos DB Emulator | +| **azurite** | 10000, 10001, 10002 | Azure Storage Emulator | +| **redis** | 6379 | Redis Cache | +| **mailpit** | 1025 (SMTP), 8025 (UI) | Email testing server | + +### Starting Services + +```bash +# Start all services +docker compose up -d + +# Start specific services +docker compose up -d cosmosdb redis + +# View service status +docker compose ps + +# View logs +docker compose logs -f cosmosdb + +# Stop all services +docker compose down + +# Stop and remove volumes (reset data) +docker compose down -v +``` + +### Optional Tools Profile + +Additional development tools can be started with the `tools` profile: + +```bash +# Start with Redis Commander web UI +docker compose --profile tools up -d +``` + +This adds: +- **Redis Commander**: http://localhost:8082 - Redis web UI + +--- + +## Running the API + +### Configuration + +The API uses `appsettings.Development.json` for local settings: + +| Setting | Value | Description | +|---------|-------|-------------| +| `CosmosDb.AccountEndpoint` | `https://localhost:8081` | Cosmos DB Emulator | +| `Redis.ConnectionString` | `localhost:6379` | Local Redis | +| `Jwt.EnableLocalTokenGeneration` | `true` | Enable dev auth | + +### Starting the API + +```bash +cd src/Luminous.Api + +# Run with hot reload +dotnet watch run + +# Or run without hot reload +dotnet run +``` + +### API Endpoints + +Once running, the API is available at: + +| Endpoint | Description | +|----------|-------------| +| http://localhost:5000 | API base URL | +| http://localhost:5000/swagger | Swagger UI | +| http://localhost:5000/health | Health check | +| http://localhost:5000/api/devauth/status | Dev auth status | + +### Launch Profiles + +The API includes several launch profiles in `Properties/launchSettings.json`: + +| Profile | Description | +|---------|-------------| +| `http` | HTTP only (port 5000) | +| `https` | HTTPS + HTTP (ports 5001, 5000) | +| `Luminous.Api (Watch)` | With hot reload | +| `Luminous.Api (Docker)` | Docker container | + +--- + +## Running the Web App + +### Installation + +```bash +cd clients/web + +# Install dependencies +npm install +``` + +### Starting the App + +```bash +# Development server +npm start + +# Or with staging configuration +npm run start:staging +``` + +### Web App Endpoints + +| Endpoint | Description | +|----------|-------------| +| http://localhost:4200 | Angular development server | + +### Build Commands + +```bash +# Development build +npm run build + +# Staging build +npm run build:staging + +# Production build +npm run build:prod + +# Run tests +npm test + +# Lint code +npm run lint +``` + +--- + +## Development Authentication + +### Overview + +In development, you can use the local JWT token service to authenticate without setting up Azure AD or external identity providers. + +### Getting a Dev Token + +**Using curl:** +```bash +# Get a default dev token +curl -X POST http://localhost:5000/api/devauth/token + +# Response: +{ + "accessToken": "eyJhbGciOiJIUzI1NiIs...", + "tokenType": "Bearer", + "expiresIn": 3600, + "user": { + "userId": "dev-user-001", + "familyId": "dev-family-001", + "email": "dev@luminous.local", + "displayName": "Developer", + "role": "Owner" + } +} +``` + +**Custom token with specific user:** +```bash +curl -X POST http://localhost:5000/api/devauth/token/custom \ + -H "Content-Type: application/json" \ + -d '{ + "userId": "test-user-001", + "familyId": "test-family-001", + "email": "test@luminous.local", + "role": "Adult", + "displayName": "Test User" + }' +``` + +### Using the Token + +Add the token to your requests: + +```bash +curl -H "Authorization: Bearer " \ + http://localhost:5000/api/families +``` + +### Swagger UI + +The Swagger UI includes JWT authentication support: + +1. Open http://localhost:5000/swagger +2. Click "Authorize" +3. Enter your token (without "Bearer " prefix) +4. Click "Authorize" + +### Token Configuration + +Token settings in `appsettings.Development.json`: + +```json +{ + "Jwt": { + "SecretKey": "LuminousDevSecretKey-AtLeast32Characters-ForHMACSHA256!", + "Issuer": "https://luminous.local", + "Audience": "luminous-api", + "ExpirationMinutes": 60, + "EnableLocalTokenGeneration": true + } +} +``` + +> **Security Note**: Local token generation is only available when `EnableLocalTokenGeneration` is `true` AND the application is running in the Development environment. + +--- + +## Cosmos DB Emulator + +### Overview + +The Azure Cosmos DB Emulator provides a local environment for development. It supports the SQL API used by Luminous. + +### Connection Details + +| Property | Value | +|----------|-------| +| **Endpoint** | `https://localhost:8081` | +| **Account Key** | `C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==` | +| **Database** | `luminous` | + +### Data Explorer + +Access the Cosmos DB Data Explorer at: https://localhost:8081/_explorer/index.html + +> **Note**: You may need to accept the self-signed certificate warning in your browser. + +### SSL Certificate Setup + +The emulator uses a self-signed certificate. To avoid SSL errors: + +**macOS:** +```bash +# Download and trust the certificate +./scripts/dev-start.sh --install-cert + +# Or manually: +curl -k https://localhost:8081/_explorer/emulator.pem > ~/cosmos-emulator.crt +sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/cosmos-emulator.crt +``` + +**Windows (PowerShell):** +```powershell +.\scripts\dev-start.ps1 -InstallCert + +# Or manually: +Invoke-WebRequest -Uri "https://localhost:8081/_explorer/emulator.pem" -SkipCertificateCheck -OutFile cosmos-emulator.crt +Import-Certificate -FilePath .\cosmos-emulator.crt -CertStoreLocation Cert:\CurrentUser\Root +``` + +**Linux:** +```bash +curl -k https://localhost:8081/_explorer/emulator.pem > cosmos-emulator.crt +sudo cp cosmos-emulator.crt /usr/local/share/ca-certificates/ +sudo update-ca-certificates +``` + +### Startup Time + +The Cosmos DB Emulator can take **2-3 minutes** to start initially. The dev scripts wait for it automatically. + +### Containers + +The emulator will create these containers (defined in Bicep): + +| Container | Partition Key | Description | +|-----------|--------------|-------------| +| `families` | `/id` | Family (tenant) data | +| `users` | `/familyId` | User accounts | +| `events` | `/familyId` | Calendar events | +| `chores` | `/familyId` | Chores and tasks | +| `devices` | `/familyId` | Linked devices | +| `routines` | `/familyId` | Daily routines | +| `lists` | `/familyId` | Shopping/custom lists | +| `meals` | `/familyId` | Meal plans | +| `completions` | `/familyId` | Task completions | +| `invitations` | `/familyId` | Member invitations | +| `credentials` | `/userId` | WebAuthn credentials | + +--- + +## Troubleshooting + +### Docker Issues + +**Cosmos DB Emulator won't start:** +```bash +# Check if you have enough memory +docker stats + +# Increase Docker memory to at least 3GB +# Docker Desktop > Settings > Resources > Memory + +# Reset the emulator data +docker compose down -v +docker compose up -d cosmosdb +``` + +**Port already in use:** +```bash +# Find what's using the port +lsof -i :5000 # macOS/Linux +netstat -ano | findstr :5000 # Windows + +# Kill the process or use a different port +``` + +### API Issues + +**API can't connect to Cosmos DB:** +1. Ensure Docker services are running: `docker compose ps` +2. Wait for Cosmos DB to be ready (2-3 minutes on first start) +3. Check the SSL certificate is trusted +4. Verify `appsettings.Development.json` has correct settings + +**JWT token errors:** +1. Ensure `EnableLocalTokenGeneration` is `true` +2. Check you're running in Development environment +3. Verify the token hasn't expired (default 60 minutes) + +### Web App Issues + +**npm install fails:** +```bash +# Clear npm cache +npm cache clean --force + +# Delete node_modules and reinstall +rm -rf node_modules package-lock.json +npm install +``` + +**CORS errors:** +1. Ensure the API is running +2. Check `Cors.AllowedOrigins` includes `http://localhost:4200` +3. Verify you're using `http://localhost:4200` not `127.0.0.1` + +### Apple Silicon (ARM64) Issues + +**Cosmos DB Emulator won't start or is very slow:** +```bash +# Check Docker has enough resources (need 4GB+ RAM) +docker stats + +# Try pulling the latest image explicitly +docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest + +# Reset and try again +docker compose down -v +docker compose up -d cosmosdb + +# Watch the logs (startup can take 3-5 minutes) +docker compose logs -f cosmosdb +``` + +**"no matching manifest for linux/arm64" error:** +```bash +# Some images may need x86_64 emulation via Rosetta 2 +# Ensure Rosetta is enabled in Docker Desktop settings + +# For specific services, you can force the platform: +# Edit docker-compose.yml and add: +# platform: linux/amd64 +``` + +**Slow performance on Apple Silicon:** +1. Ensure Docker Desktop has "Use Virtualization framework" enabled +2. Allocate at least 4GB RAM (6GB recommended) in Docker settings +3. Enable "Use Rosetta for x86/amd64 emulation" for better x86 compatibility +4. Consider using native ARM64 images where available (all Luminous services support ARM64) + +### Reset Everything + +To completely reset the development environment: + +```bash +# Stop and remove all containers and volumes +docker compose down -v + +# Remove all Luminous data +docker volume rm luminous-cosmosdb-data luminous-azurite-data luminous-redis-data + +# Clean build artifacts +dotnet clean +rm -rf src/*/bin src/*/obj +rm -rf clients/web/node_modules clients/web/dist + +# Start fresh +./scripts/dev-start.sh +``` + +--- + +## VS Code Setup + +The repository includes complete VS Code configuration in the `.vscode/` folder. + +### Installing Recommended Extensions + +When you open the project, VS Code will prompt you to install recommended extensions. Alternatively: + +1. Open Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) +2. Run "Extensions: Show Recommended Extensions" +3. Click "Install All" in the Workspace Recommendations section + +### Key Extensions + +| Extension | Purpose | +|-----------|---------| +| **C# Dev Kit** | C# language support, debugging, testing | +| **Angular Language Service** | Angular template intellisense | +| **Tailwind CSS IntelliSense** | Tailwind class autocomplete | +| **Docker** | Container management | +| **Bicep** | Azure infrastructure files | +| **REST Client** | API testing from `.http` files | +| **GitLens** | Enhanced Git integration | + +### Tasks (Ctrl+Shift+B) + +Access via Command Palette > "Tasks: Run Task": + +| Task | Description | +|------|-------------| +| **Docker: Start All Services** | Start CosmosDB, Redis, Azurite, Mailpit | +| **Docker: Stop All Services** | Stop all Docker containers | +| **Solution: Build All** | Build entire .NET solution | +| **Solution: Test All** | Run all tests | +| **API: Watch (Hot Reload)** | Start API with hot reload | +| **Web: Start Dev Server** | Start Angular dev server | +| **Dev: Start Full Stack** | Start Docker + API + Web | +| **Dev: Get Auth Token** | Get a development JWT token | + +### Debugging (F5) + +Pre-configured launch configurations: + +| Configuration | Description | +|---------------|-------------| +| **API: Debug** | Debug .NET API (opens Swagger) | +| **API: Debug (HTTPS)** | Debug with HTTPS enabled | +| **Web: Debug in Chrome** | Debug Angular app in Chrome | +| **Web: Debug in Edge** | Debug Angular app in Edge | +| **Full Stack: API + Web (Chrome)** | Debug both API and Web together | +| **Tests: Debug All** | Debug all tests | + +### Compound Debugging + +For full-stack debugging: + +1. Select "Full Stack: API + Web (Chrome)" from the debug dropdown +2. Press F5 +3. Both API and Web will start with debuggers attached +4. Set breakpoints in both C# and TypeScript code + +### Keyboard Shortcuts + +| Shortcut | Action | +|----------|--------| +| `Ctrl+Shift+B` | Run default build task | +| `F5` | Start debugging | +| `Ctrl+Shift+D` | Open Debug panel | +| `Ctrl+Shift+P` | Command Palette | +| `Ctrl+`` ` | Toggle terminal | + +### REST Client Usage + +Create `.http` files to test API endpoints: + +```http +### Get Dev Token +POST http://localhost:5000/api/devauth/token + +### Get Family (with token) +GET http://localhost:5000/api/families +Authorization: Bearer {{token}} +``` + +Use the REST Client extension to send requests directly from VS Code. + +### Project Structure in Explorer + +The settings hide build artifacts (`bin/`, `obj/`, `node_modules/`) for cleaner navigation. To show hidden files, adjust `files.exclude` in settings. + +--- + +## Related Documentation + +- [Project Overview](./PROJECT-OVERVIEW.md) +- [Architecture](./ARCHITECTURE.md) +- [Roadmap](./ROADMAP.md) +- [Azure Infrastructure](./AZURE-INFRASTRUCTURE.md) +- [CLAUDE.md (Development Guidelines)](../CLAUDE.md) + +--- + +## Document History + +| Version | Date | Author | Changes | +|---------|------|--------|---------| +| 1.0.0 | 2025-12-22 | Luminous Team | Initial development guide | +| 1.1.0 | 2025-12-22 | Luminous Team | Added comprehensive VS Code configuration | +| 1.2.0 | 2025-12-22 | Luminous Team | Added ARM64/Apple Silicon support and Mailpit | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a7761a2..a1960c2 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,7 +1,7 @@ # Luminous Development Roadmap -> **Document Version:** 2.3.0 -> **Last Updated:** 2025-12-21 +> **Document Version:** 2.4.0 +> **Last Updated:** 2025-12-22 > **Status:** Active > **TOGAF Phase:** Phase E/F (Opportunities, Solutions & Migration Planning) @@ -102,7 +102,7 @@ Phase 6: Intelligence & Ecosystem | Phase | Name | Focus | Key Deliverables | Status | |-------|------|-------|------------------|--------| -| **0** | Foundation | Infrastructure | Azure IaC, .NET solution, Angular shell, Passwordless Auth | 🟡 In Progress (0.1, 0.2, 0.3 Complete) | +| **0** | Foundation | Infrastructure | Azure IaC, .NET solution, Angular shell, Passwordless Auth, Local Dev | 🟡 In Progress (0.1, 0.2, 0.3, 0.4 Complete) | | **1** | Core Platform | Multi-tenancy | Family sign-up, device linking, CosmosDB, web MVP | ⬜ Not Started | | **2** | Display & Calendar | Calendar visibility | Display app, calendar integration, SignalR sync | ⬜ Not Started | | **3** | Native Mobile | Mobile apps | iOS (Swift), Android (Kotlin), push notifications | ⬜ Not Started | @@ -192,13 +192,24 @@ Establish the Azure infrastructure, .NET backend, Angular frontend, and developm - [x] TypeScript models for User, Family, Auth entities - [x] HTTP interceptors for auth tokens and error handling -#### 0.4 Local Development Environment +#### 0.4 Local Development Environment ✅ COMPLETED -- [ ] **0.4.1** Create Docker Compose for local services -- [ ] **0.4.2** Document Cosmos DB Emulator setup -- [ ] **0.4.3** Create local development scripts (PowerShell/Bash) -- [ ] **0.4.4** Configure launch profiles for debugging -- [ ] **0.4.5** Set up local JWT issuer for development +- [x] **0.4.1** Create Docker Compose for local services + - *Implemented: docker-compose.yml with CosmosDB Emulator, Azurite, Redis, MailHog, and optional Redis Commander* +- [x] **0.4.2** Document Cosmos DB Emulator setup + - *Implemented: Comprehensive DEVELOPMENT.md with SSL certificate setup, troubleshooting, and configuration* +- [x] **0.4.3** Create local development scripts (PowerShell/Bash) + - *Implemented: scripts/dev-start.sh and scripts/dev-start.ps1 with service management, status checks, and certificate installation* +- [x] **0.4.4** Configure launch profiles for debugging + - *Implemented: Properties/launchSettings.json with http, https, Docker, and Watch profiles* +- [x] **0.4.5** Set up local JWT issuer for development + - *Implemented: LocalJwtTokenService, DevAuthController, JwtSettings configuration, and development token endpoints* + +**Additional deliverables:** +- [x] Full appsettings.Development.json with all local service configurations +- [x] Swagger UI with JWT authentication support +- [x] Authorization policies (FamilyMember, FamilyAdmin, FamilyOwner) +- [x] Development authentication endpoint (POST /api/devauth/token) #### 0.5 CI/CD Pipeline @@ -658,3 +669,4 @@ These can be developed in parallel after Phase 0: | 2.1.1 | 2025-12-21 | Luminous Team | Refactored to use AVMs directly from public registry | | 2.2.0 | 2025-12-21 | Luminous Team | Phase 0.2 .NET Solution Structure completed | | 2.3.0 | 2025-12-21 | Luminous Team | Phase 0.3 Angular Web Application completed | +| 2.4.0 | 2025-12-22 | Luminous Team | Phase 0.4 Local Development Environment completed | diff --git a/scripts/dev-start.ps1 b/scripts/dev-start.ps1 new file mode 100644 index 0000000..58e6789 --- /dev/null +++ b/scripts/dev-start.ps1 @@ -0,0 +1,402 @@ +<# +.SYNOPSIS + Luminous - Local Development Start Script (PowerShell) + +.DESCRIPTION + Starts all local development services and applications for Luminous. + +.PARAMETER Services + Start Docker services only + +.PARAMETER Api + Start .NET API only (assumes Docker services running) + +.PARAMETER Web + Start Angular web app only + +.PARAMETER Stop + Stop all Docker services + +.PARAMETER Status + Check service status + +.PARAMETER InstallCert + Install Cosmos DB Emulator certificate + +.EXAMPLE + .\dev-start.ps1 + Start all services + +.EXAMPLE + .\dev-start.ps1 -Services + Start only Docker services + +.EXAMPLE + .\dev-start.ps1 -Api -Web + Start API and web app (no Docker) + +.NOTES + Prerequisites: + - Docker Desktop with Docker Compose + - .NET SDK 10.0 + - Node.js 20+ and npm +#> + +[CmdletBinding()] +param( + [switch]$Services, + [switch]$Api, + [switch]$Web, + [switch]$Stop, + [switch]$Status, + [switch]$InstallCert +) + +# Script configuration +$ErrorActionPreference = "Stop" +$ScriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +$ProjectRoot = Split-Path -Parent $ScriptRoot + +# ============================================================================= +# Helper Functions +# ============================================================================= + +function Write-Header { + param([string]$Message) + Write-Host "" + Write-Host "=============================================================================" -ForegroundColor Blue + Write-Host " $Message" -ForegroundColor Blue + Write-Host "=============================================================================" -ForegroundColor Blue + Write-Host "" +} + +function Write-Success { + param([string]$Message) + Write-Host "✓ $Message" -ForegroundColor Green +} + +function Write-Warning { + param([string]$Message) + Write-Host "⚠ $Message" -ForegroundColor Yellow +} + +function Write-Error { + param([string]$Message) + Write-Host "✗ $Message" -ForegroundColor Red +} + +function Write-Info { + param([string]$Message) + Write-Host "ℹ $Message" -ForegroundColor Cyan +} + +function Test-Command { + param([string]$Command) + $null = Get-Command $Command -ErrorAction SilentlyContinue + return $? +} + +function Test-Port { + param([int]$Port) + $connection = Test-NetConnection -ComputerName localhost -Port $Port -WarningAction SilentlyContinue -ErrorAction SilentlyContinue + return $connection.TcpTestSucceeded +} + +# ============================================================================= +# Core Functions +# ============================================================================= + +function Test-Prerequisites { + Write-Header "Checking Prerequisites" + + $missing = $false + + # Check Docker + if (Test-Command "docker") { + Write-Success "Docker is installed" + } else { + Write-Error "Docker is not installed" + $missing = $true + } + + # Check Docker Compose + try { + $null = docker compose version 2>&1 + Write-Success "Docker Compose is installed" + } catch { + Write-Error "Docker Compose is not installed" + $missing = $true + } + + # Check .NET SDK + if (Test-Command "dotnet") { + $dotnetVersion = dotnet --version + Write-Success ".NET SDK $dotnetVersion is installed" + } else { + Write-Error ".NET SDK is not installed" + $missing = $true + } + + # Check Node.js + if (Test-Command "node") { + $nodeVersion = node --version + Write-Success "Node.js $nodeVersion is installed" + } else { + Write-Error "Node.js is not installed" + $missing = $true + } + + # Check npm + if (Test-Command "npm") { + $npmVersion = npm --version + Write-Success "npm $npmVersion is installed" + } else { + Write-Error "npm is not installed" + $missing = $true + } + + if ($missing) { + Write-Error "Missing prerequisites. Please install the required tools." + exit 1 + } + + Write-Host "" +} + +function Start-DockerServices { + Write-Header "Starting Docker Services" + + Push-Location $ProjectRoot + + try { + $running = docker compose ps --quiet 2>$null + if ($running) { + Write-Warning "Docker services are already running" + Write-Info "Use 'docker compose restart' to restart services" + } else { + Write-Info "Starting Docker Compose services..." + docker compose up -d + + Write-Info "Waiting for services to be ready..." + Start-Sleep -Seconds 5 + + # Check CosmosDB + Write-Info "Waiting for CosmosDB Emulator (this may take 2-3 minutes)..." + $maxAttempts = 30 + $attempt = 1 + while ($attempt -le $maxAttempts) { + try { + $null = Invoke-WebRequest -Uri "https://localhost:8081/_explorer/emulator.pem" -SkipCertificateCheck -TimeoutSec 5 -ErrorAction SilentlyContinue + Write-Success "CosmosDB Emulator is ready" + break + } catch { + if ($attempt -eq $maxAttempts) { + Write-Warning "CosmosDB Emulator may still be starting" + } + Start-Sleep -Seconds 5 + $attempt++ + } + } + + # Check Redis + try { + $pong = docker exec luminous-redis redis-cli ping 2>$null + if ($pong -eq "PONG") { + Write-Success "Redis is ready" + } + } catch { + Write-Warning "Redis may still be starting" + } + + Write-Success "Azurite is running" + } + + Write-Host "" + Write-Success "Docker services started" + Write-Host "" + Write-Host "Service endpoints:" + Write-Host " CosmosDB: https://localhost:8081" + Write-Host " Azurite: http://localhost:10000 (Blob)" + Write-Host " Redis: localhost:6379" + Write-Host " MailHog: http://localhost:8025 (Web UI)" + Write-Host "" + } finally { + Pop-Location + } +} + +function Stop-DockerServices { + Write-Header "Stopping Docker Services" + + Push-Location $ProjectRoot + + try { + docker compose down + Write-Success "Docker services stopped" + } finally { + Pop-Location + } +} + +function Start-Api { + Write-Header "Starting .NET API" + + $apiPath = Join-Path $ProjectRoot "src\Luminous.Api" + Push-Location $apiPath + + try { + # Restore packages if needed + if (-not (Test-Path (Join-Path $apiPath "bin"))) { + Write-Info "Restoring NuGet packages..." + dotnet restore + } + + Write-Info "Starting API with hot reload..." + Write-Info "API will be available at http://localhost:5000" + Write-Info "Swagger UI: http://localhost:5000/swagger" + Write-Host "" + + dotnet watch run --no-hot-reload + } finally { + Pop-Location + } +} + +function Start-Web { + Write-Header "Starting Angular Web App" + + $webPath = Join-Path $ProjectRoot "clients\web" + Push-Location $webPath + + try { + # Install dependencies if needed + if (-not (Test-Path (Join-Path $webPath "node_modules"))) { + Write-Info "Installing npm dependencies..." + npm install + } + + Write-Info "Starting Angular development server..." + Write-Info "Web app will be available at http://localhost:4200" + Write-Host "" + + npm start + } finally { + Pop-Location + } +} + +function Show-Status { + Write-Header "Service Status" + + Push-Location $ProjectRoot + + try { + Write-Host "Docker Services:" + docker compose ps 2>$null + if ($LASTEXITCODE -ne 0) { + Write-Host " Docker Compose not running" + } + + Write-Host "" + Write-Host "Port Usage:" + + $ports = @{ + 5000 = "API" + 4200 = "Web" + 8081 = "Cosmos" + 6379 = "Redis" + 10000 = "Blob" + } + + foreach ($port in $ports.Keys) { + $status = if (Test-Port $port) { "IN USE" } else { "FREE" } + Write-Host " Port $port ($($ports[$port])): $status" + } + + Write-Host "" + } finally { + Pop-Location + } +} + +function Install-CosmosCert { + Write-Header "Installing Cosmos DB Emulator Certificate" + + Write-Info "Downloading certificate from emulator..." + + $certPath = Join-Path $env:TEMP "cosmos-emulator.crt" + + try { + Invoke-WebRequest -Uri "https://localhost:8081/_explorer/emulator.pem" -SkipCertificateCheck -OutFile $certPath + Write-Success "Certificate downloaded to $certPath" + + Write-Info "Importing certificate to Trusted Root store..." + Write-Info "You may be prompted for administrator privileges..." + + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certPath) + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store("Root", "CurrentUser") + $store.Open("ReadWrite") + $store.Add($cert) + $store.Close() + + Write-Success "Certificate installed successfully" + } catch { + Write-Error "Failed to install certificate: $_" + Write-Info "Make sure the Cosmos DB Emulator is running first." + } +} + +# ============================================================================= +# Main +# ============================================================================= + +Write-Header "Luminous Local Development" +Write-Host "Project Root: $ProjectRoot" + +# Handle specific actions +if ($Stop) { + Stop-DockerServices + exit 0 +} + +if ($Status) { + Show-Status + exit 0 +} + +if ($InstallCert) { + Install-CosmosCert + exit 0 +} + +Test-Prerequisites + +# Determine what to start +$startAll = -not ($Services -or $Api -or $Web) + +if ($startAll) { + Start-DockerServices + + Write-Info "Starting API and Web app in separate terminals..." + Write-Info "Open two new PowerShell windows and run:" + Write-Host "" + Write-Host " Terminal 1 (API): cd $ProjectRoot; .\scripts\dev-start.ps1 -Api" + Write-Host " Terminal 2 (Web): cd $ProjectRoot; .\scripts\dev-start.ps1 -Web" + Write-Host "" + Write-Info "Or use the provided VS Code launch configurations" + exit 0 +} + +if ($Services) { + Start-DockerServices +} + +if ($Api) { + Start-Api +} + +if ($Web) { + Start-Web +} + +Write-Success "Development environment ready!" diff --git a/scripts/dev-start.sh b/scripts/dev-start.sh new file mode 100755 index 0000000..6919338 --- /dev/null +++ b/scripts/dev-start.sh @@ -0,0 +1,388 @@ +#!/bin/bash +# ============================================================================= +# Luminous - Local Development Start Script +# ============================================================================= +# Starts all local development services and applications. +# +# Usage: +# ./dev-start.sh # Start all services +# ./dev-start.sh --api # Start API only (assumes Docker services running) +# ./dev-start.sh --web # Start Web app only +# ./dev-start.sh --services # Start Docker services only +# ./dev-start.sh --stop # Stop all services +# ./dev-start.sh --status # Check service status +# +# Prerequisites: +# - Docker and Docker Compose installed +# - .NET SDK 10.0 installed +# - Node.js 20+ and npm installed +# ============================================================================= + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Script directory (resolve to project root) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" + +# ============================================================================= +# Functions +# ============================================================================= + +print_header() { + echo "" + echo -e "${BLUE}=============================================================================${NC}" + echo -e "${BLUE} $1${NC}" + echo -e "${BLUE}=============================================================================${NC}" + echo "" +} + +print_success() { + echo -e "${GREEN}✓ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠ $1${NC}" +} + +print_error() { + echo -e "${RED}✗ $1${NC}" +} + +print_info() { + echo -e "${CYAN}ℹ $1${NC}" +} + +usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --all Start all services (default)" + echo " --services Start Docker services only" + echo " --api Start .NET API only (assumes Docker services running)" + echo " --web Start Angular web app only" + echo " --stop Stop all services" + echo " --status Check service status" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 # Start everything" + echo " $0 --services # Start only Docker services" + echo " $0 --api --web # Start API and web app (no Docker)" + exit 0 +} + +check_prerequisites() { + print_header "Checking Prerequisites" + + local missing=0 + + # Check Docker + if ! command -v docker &> /dev/null; then + print_error "Docker is not installed" + missing=1 + else + print_success "Docker is installed" + fi + + # Check Docker Compose + if ! command -v docker &> /dev/null || ! docker compose version &> /dev/null; then + if ! command -v docker-compose &> /dev/null; then + print_error "Docker Compose is not installed" + missing=1 + else + print_success "Docker Compose (legacy) is installed" + fi + else + print_success "Docker Compose is installed" + fi + + # Check .NET SDK + if ! command -v dotnet &> /dev/null; then + print_error ".NET SDK is not installed" + missing=1 + else + local dotnet_version=$(dotnet --version) + print_success ".NET SDK $dotnet_version is installed" + fi + + # Check Node.js + if ! command -v node &> /dev/null; then + print_error "Node.js is not installed" + missing=1 + else + local node_version=$(node --version) + print_success "Node.js $node_version is installed" + fi + + # Check npm + if ! command -v npm &> /dev/null; then + print_error "npm is not installed" + missing=1 + else + local npm_version=$(npm --version) + print_success "npm $npm_version is installed" + fi + + if [ $missing -eq 1 ]; then + print_error "Missing prerequisites. Please install the required tools." + exit 1 + fi + + echo "" +} + +start_docker_services() { + print_header "Starting Docker Services" + + cd "$PROJECT_ROOT" + + if docker compose ps --quiet 2>/dev/null | grep -q .; then + print_warning "Docker services are already running" + print_info "Use 'docker compose restart' to restart services" + else + print_info "Starting Docker Compose services..." + docker compose up -d + + print_info "Waiting for services to be ready..." + sleep 5 + + # Check CosmosDB + local max_attempts=30 + local attempt=1 + while [ $attempt -le $max_attempts ]; do + if curl -fks https://localhost:8081/_explorer/emulator.pem > /dev/null 2>&1; then + print_success "CosmosDB Emulator is ready" + break + fi + if [ $attempt -eq $max_attempts ]; then + print_warning "CosmosDB Emulator may still be starting (can take 2-3 minutes)" + fi + sleep 5 + attempt=$((attempt + 1)) + done + + # Check Redis + if docker exec luminous-redis redis-cli ping 2>/dev/null | grep -q PONG; then + print_success "Redis is ready" + else + print_warning "Redis may still be starting" + fi + + # Check Azurite + if curl -s http://localhost:10000/devstoreaccount1 > /dev/null 2>&1; then + print_success "Azurite is ready" + else + print_success "Azurite is running" + fi + fi + + echo "" + print_success "Docker services started" + echo "" + echo "Service endpoints:" + echo " CosmosDB: https://localhost:8081" + echo " Azurite: http://localhost:10000 (Blob)" + echo " Redis: localhost:6379" + echo " MailHog: http://localhost:8025 (Web UI)" + echo "" +} + +stop_docker_services() { + print_header "Stopping Docker Services" + + cd "$PROJECT_ROOT" + + docker compose down + print_success "Docker services stopped" +} + +start_api() { + print_header "Starting .NET API" + + cd "$PROJECT_ROOT/src/Luminous.Api" + + # Restore packages if needed + if [ ! -d "$PROJECT_ROOT/src/Luminous.Api/bin" ]; then + print_info "Restoring NuGet packages..." + dotnet restore + fi + + print_info "Starting API with hot reload..." + print_info "API will be available at http://localhost:5000" + print_info "Swagger UI: http://localhost:5000/swagger" + echo "" + + dotnet watch run --no-hot-reload +} + +start_web() { + print_header "Starting Angular Web App" + + cd "$PROJECT_ROOT/clients/web" + + # Install dependencies if needed + if [ ! -d "node_modules" ]; then + print_info "Installing npm dependencies..." + npm install + fi + + print_info "Starting Angular development server..." + print_info "Web app will be available at http://localhost:4200" + echo "" + + npm start +} + +show_status() { + print_header "Service Status" + + cd "$PROJECT_ROOT" + + echo "Docker Services:" + docker compose ps 2>/dev/null || echo " Docker Compose not running" + + echo "" + echo "Port Usage:" + echo " Port 5000 (API): $(lsof -i :5000 2>/dev/null | grep LISTEN > /dev/null && echo "IN USE" || echo "FREE")" + echo " Port 4200 (Web): $(lsof -i :4200 2>/dev/null | grep LISTEN > /dev/null && echo "IN USE" || echo "FREE")" + echo " Port 8081 (Cosmos): $(lsof -i :8081 2>/dev/null | grep LISTEN > /dev/null && echo "IN USE" || echo "FREE")" + echo " Port 6379 (Redis): $(lsof -i :6379 2>/dev/null | grep LISTEN > /dev/null && echo "IN USE" || echo "FREE")" + echo " Port 10000 (Blob): $(lsof -i :10000 2>/dev/null | grep LISTEN > /dev/null && echo "IN USE" || echo "FREE")" + echo "" +} + +install_cosmos_cert() { + print_header "Installing Cosmos DB Emulator Certificate" + + print_info "Downloading certificate from emulator..." + + local cert_path="/tmp/cosmos-emulator.crt" + + # Download the certificate + curl -k https://localhost:8081/_explorer/emulator.pem > "$cert_path" 2>/dev/null + + if [ -f "$cert_path" ]; then + print_success "Certificate downloaded" + + # Check if running on macOS + if [[ "$OSTYPE" == "darwin"* ]]; then + print_info "On macOS, run:" + echo " sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain $cert_path" + elif [[ "$OSTYPE" == "linux-gnu"* ]]; then + print_info "On Linux (Ubuntu/Debian), run:" + echo " sudo cp $cert_path /usr/local/share/ca-certificates/cosmos-emulator.crt" + echo " sudo update-ca-certificates" + fi + else + print_error "Failed to download certificate. Is the emulator running?" + fi +} + +# ============================================================================= +# Main +# ============================================================================= + +# Parse arguments +START_ALL=true +START_SERVICES=false +START_API=false +START_WEB=false +STOP=false +STATUS=false + +if [ $# -eq 0 ]; then + START_ALL=true +else + START_ALL=false + while [[ $# -gt 0 ]]; do + case $1 in + --all) + START_ALL=true + shift + ;; + --services) + START_SERVICES=true + shift + ;; + --api) + START_API=true + shift + ;; + --web) + START_WEB=true + shift + ;; + --stop) + STOP=true + shift + ;; + --status) + STATUS=true + shift + ;; + --install-cert) + check_prerequisites + install_cosmos_cert + exit 0 + ;; + --help|-h) + usage + ;; + *) + print_error "Unknown option: $1" + usage + ;; + esac + done +fi + +# Execute based on arguments +print_header "Luminous Local Development" +echo "Project Root: $PROJECT_ROOT" + +if [ "$STOP" = true ]; then + stop_docker_services + exit 0 +fi + +if [ "$STATUS" = true ]; then + show_status + exit 0 +fi + +check_prerequisites + +if [ "$START_ALL" = true ]; then + start_docker_services + + print_info "Starting API and Web app in separate terminals..." + print_info "Open two new terminal windows and run:" + echo "" + echo " Terminal 1 (API): cd $PROJECT_ROOT && ./scripts/dev-start.sh --api" + echo " Terminal 2 (Web): cd $PROJECT_ROOT && ./scripts/dev-start.sh --web" + echo "" + print_info "Or use the provided VS Code launch configurations" + exit 0 +fi + +if [ "$START_SERVICES" = true ]; then + start_docker_services +fi + +if [ "$START_API" = true ]; then + start_api +fi + +if [ "$START_WEB" = true ]; then + start_web +fi + +print_success "Development environment ready!" diff --git a/src/Luminous.Api/Configuration/JwtSettings.cs b/src/Luminous.Api/Configuration/JwtSettings.cs new file mode 100644 index 0000000..9074ccf --- /dev/null +++ b/src/Luminous.Api/Configuration/JwtSettings.cs @@ -0,0 +1,44 @@ +namespace Luminous.Api.Configuration; + +/// +/// JWT authentication settings for the API. +/// +public class JwtSettings +{ + /// + /// Configuration section name in appsettings.json. + /// + public const string SectionName = "Jwt"; + + /// + /// Gets or sets the secret key used to sign JWT tokens (development only). + /// In production, tokens are validated against the external identity provider. + /// + public string SecretKey { get; set; } = string.Empty; + + /// + /// Gets or sets the JWT issuer. + /// + public string Issuer { get; set; } = "https://luminous.local"; + + /// + /// Gets or sets the JWT audience. + /// + public string Audience { get; set; } = "luminous-api"; + + /// + /// Gets or sets the token expiration time in minutes. + /// + public int ExpirationMinutes { get; set; } = 60; + + /// + /// Gets or sets the refresh token expiration time in days. + /// + public int RefreshExpirationDays { get; set; } = 7; + + /// + /// Gets or sets whether local JWT token generation is enabled. + /// Should only be true in development environments. + /// + public bool EnableLocalTokenGeneration { get; set; } = false; +} diff --git a/src/Luminous.Api/Controllers/DevAuthController.cs b/src/Luminous.Api/Controllers/DevAuthController.cs new file mode 100644 index 0000000..9fffdf0 --- /dev/null +++ b/src/Luminous.Api/Controllers/DevAuthController.cs @@ -0,0 +1,225 @@ +using Luminous.Api.Configuration; +using Luminous.Api.Services; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; + +namespace Luminous.Api.Controllers; + +/// +/// Development-only authentication controller for local JWT token generation. +/// This controller is only available when EnableLocalTokenGeneration is true. +/// +[ApiController] +[Route("api/[controller]")] +[AllowAnonymous] +public class DevAuthController : ControllerBase +{ + private readonly ILocalJwtTokenService _jwtService; + private readonly JwtSettings _jwtSettings; + private readonly IWebHostEnvironment _environment; + private readonly ILogger _logger; + + public DevAuthController( + ILocalJwtTokenService jwtService, + IOptions jwtSettings, + IWebHostEnvironment environment, + ILogger logger) + { + _jwtService = jwtService; + _jwtSettings = jwtSettings.Value; + _environment = environment; + _logger = logger; + } + + /// + /// Generates a development JWT token with default test user credentials. + /// Only available in development environments. + /// + /// A JWT access token for development use. + [HttpPost("token")] + [ProducesResponseType(typeof(DevTokenResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public IActionResult GetDevToken() + { + if (!CanGenerateTokens()) + { + _logger.LogWarning("Attempted to generate dev token in non-development environment"); + return Forbid(); + } + + var token = _jwtService.GenerateDevToken(); + return Ok(new DevTokenResponse + { + AccessToken = token, + TokenType = "Bearer", + ExpiresIn = _jwtSettings.ExpirationMinutes * 60, + User = new DevUserInfo + { + UserId = "dev-user-001", + FamilyId = "dev-family-001", + Email = "dev@luminous.local", + DisplayName = "Developer", + Role = "Owner" + } + }); + } + + /// + /// Generates a custom development JWT token with specified credentials. + /// Only available in development environments. + /// + /// The token request with user details. + /// A JWT access token for development use. + [HttpPost("token/custom")] + [ProducesResponseType(typeof(DevTokenResponse), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public IActionResult GetCustomDevToken([FromBody] DevTokenRequest request) + { + if (!CanGenerateTokens()) + { + _logger.LogWarning("Attempted to generate dev token in non-development environment"); + return Forbid(); + } + + if (!ModelState.IsValid) + { + return BadRequest(ModelState); + } + + var token = _jwtService.GenerateToken( + request.UserId, + request.FamilyId, + request.Email, + request.Role, + request.DisplayName); + + return Ok(new DevTokenResponse + { + AccessToken = token, + TokenType = "Bearer", + ExpiresIn = _jwtSettings.ExpirationMinutes * 60, + User = new DevUserInfo + { + UserId = request.UserId, + FamilyId = request.FamilyId, + Email = request.Email, + DisplayName = request.DisplayName, + Role = request.Role + } + }); + } + + /// + /// Checks if the development authentication is available. + /// + [HttpGet("status")] + [ProducesResponseType(typeof(DevAuthStatus), StatusCodes.Status200OK)] + public IActionResult GetStatus() + { + return Ok(new DevAuthStatus + { + IsEnabled = CanGenerateTokens(), + Environment = _environment.EnvironmentName, + Message = CanGenerateTokens() + ? "Development authentication is enabled. Use POST /api/devauth/token to get a token." + : "Development authentication is disabled. This feature is only available in development environments." + }); + } + + private bool CanGenerateTokens() + { + return _environment.IsDevelopment() && _jwtSettings.EnableLocalTokenGeneration; + } +} + +/// +/// Request model for custom development token generation. +/// +public class DevTokenRequest +{ + /// + /// The user ID to include in the token. + /// + public required string UserId { get; init; } + + /// + /// The family ID to include in the token. + /// + public required string FamilyId { get; init; } + + /// + /// The email to include in the token. + /// + public required string Email { get; init; } + + /// + /// The user's role (Owner, Admin, Adult, Teen, Child, Caregiver). + /// + public required string Role { get; init; } + + /// + /// The user's display name. + /// + public required string DisplayName { get; init; } +} + +/// +/// Response model for development token generation. +/// +public class DevTokenResponse +{ + /// + /// The JWT access token. + /// + public required string AccessToken { get; init; } + + /// + /// The token type (always "Bearer"). + /// + public required string TokenType { get; init; } + + /// + /// The token expiration time in seconds. + /// + public required int ExpiresIn { get; init; } + + /// + /// Information about the development user. + /// + public required DevUserInfo User { get; init; } +} + +/// +/// Information about the development user. +/// +public class DevUserInfo +{ + public required string UserId { get; init; } + public required string FamilyId { get; init; } + public required string Email { get; init; } + public required string DisplayName { get; init; } + public required string Role { get; init; } +} + +/// +/// Status of the development authentication feature. +/// +public class DevAuthStatus +{ + /// + /// Whether development authentication is enabled. + /// + public required bool IsEnabled { get; init; } + + /// + /// The current environment name. + /// + public required string Environment { get; init; } + + /// + /// A message describing the current status. + /// + public required string Message { get; init; } +} diff --git a/src/Luminous.Api/Program.cs b/src/Luminous.Api/Program.cs index 5a3b3ba..9a6729c 100644 --- a/src/Luminous.Api/Program.cs +++ b/src/Luminous.Api/Program.cs @@ -1,9 +1,13 @@ +using System.Text; using Luminous.Api.Configuration; using Luminous.Api.Middleware; using Luminous.Api.Services; using Luminous.Application; using Luminous.Application.Common.Interfaces; using Luminous.Infrastructure; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Microsoft.OpenApi.Models; using Serilog; var builder = WebApplication.CreateBuilder(args); @@ -20,9 +24,70 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); +// Configure JWT settings +builder.Services.Configure( + builder.Configuration.GetSection(JwtSettings.SectionName)); + +// Register local JWT token service (for development) +builder.Services.AddScoped(); + // Configure authentication -builder.Services.AddAuthentication().AddJwtBearer(); -builder.Services.AddAuthorization(); +var jwtSettings = builder.Configuration.GetSection(JwtSettings.SectionName).Get() + ?? new JwtSettings(); + +builder.Services.AddAuthentication(options => +{ + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; +}) +.AddJwtBearer(options => +{ + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings.Issuer, + ValidAudience = jwtSettings.Audience, + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtSettings.SecretKey)), + ClockSkew = TimeSpan.FromMinutes(5) + }; + + // For development, allow HTTP + if (builder.Environment.IsDevelopment()) + { + options.RequireHttpsMetadata = false; + } + + options.Events = new JwtBearerEvents + { + OnAuthenticationFailed = context => + { + if (context.Exception is SecurityTokenExpiredException) + { + context.Response.Headers.Append("Token-Expired", "true"); + } + return Task.CompletedTask; + } + }; +}); + +builder.Services.AddAuthorization(options => +{ + // Add authorization policies for different roles + options.AddPolicy("FamilyMember", policy => + policy.RequireClaim("family_id")); + + options.AddPolicy("FamilyAdmin", policy => + policy.RequireClaim("family_id") + .RequireRole("Owner", "Admin")); + + options.AddPolicy("FamilyOwner", policy => + policy.RequireClaim("family_id") + .RequireRole("Owner")); +}); // Configure controllers builder.Services.AddControllers(); @@ -31,11 +96,42 @@ builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { - options.SwaggerDoc("v1", new() + options.SwaggerDoc("v1", new OpenApiInfo { Title = "Luminous API", Version = "v1", - Description = "API for the Luminous Family Hub" + Description = "API for the Luminous Family Hub", + Contact = new OpenApiContact + { + Name = "Luminous Team", + Url = new Uri("https://github.com/trickpatty/Luminous") + } + }); + + // Add JWT authentication to Swagger + options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme + { + Name = "Authorization", + Type = SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "Enter your JWT token. In development, use POST /api/devauth/token to get a token." + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "Bearer" + } + }, + Array.Empty() + } }); }); @@ -62,12 +158,20 @@ if (app.Environment.IsDevelopment()) { app.UseSwagger(); - app.UseSwaggerUI(); + app.UseSwaggerUI(options => + { + options.SwaggerEndpoint("/swagger/v1/swagger.json", "Luminous API v1"); + options.DisplayRequestDuration(); + }); } app.UseSerilogRequestLogging(); -app.UseHttpsRedirection(); +// Only redirect to HTTPS in non-development environments +if (!app.Environment.IsDevelopment()) +{ + app.UseHttpsRedirection(); +} app.UseCors(); @@ -80,4 +184,14 @@ app.MapControllers(); app.MapHealthChecks("/health"); +// Log startup information +Log.Information("Luminous API started in {Environment} mode", app.Environment.EnvironmentName); +if (app.Environment.IsDevelopment()) +{ + Log.Information("Development features enabled:"); + Log.Information(" - Swagger UI: http://localhost:5000/swagger"); + Log.Information(" - Dev Auth: POST http://localhost:5000/api/devauth/token"); + Log.Information(" - Health Check: http://localhost:5000/health"); +} + app.Run(); diff --git a/src/Luminous.Api/Properties/launchSettings.json b/src/Luminous.Api/Properties/launchSettings.json new file mode 100644 index 0000000..f8829b7 --- /dev/null +++ b/src/Luminous.Api/Properties/launchSettings.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Luminous.Api (Docker)": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "publishAllPorts": true + }, + "Luminous.Api (Watch)": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_WATCH": "1" + } + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5000", + "sslPort": 5001 + } + } +} diff --git a/src/Luminous.Api/Services/LocalJwtTokenService.cs b/src/Luminous.Api/Services/LocalJwtTokenService.cs new file mode 100644 index 0000000..d044218 --- /dev/null +++ b/src/Luminous.Api/Services/LocalJwtTokenService.cs @@ -0,0 +1,99 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Luminous.Api.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace Luminous.Api.Services; + +/// +/// Service for generating JWT tokens for local development purposes. +/// This should NOT be used in production - tokens should come from the identity provider. +/// +public interface ILocalJwtTokenService +{ + /// + /// Generates a JWT token for a development user. + /// + /// The user ID to include in the token. + /// The family ID to include in the token. + /// The email to include in the token. + /// The role to include in the token. + /// The display name to include in the token. + /// A JWT access token. + string GenerateToken(string userId, string familyId, string email, string role, string displayName); + + /// + /// Generates a development user token with default test values. + /// + /// A JWT access token for a test user. + string GenerateDevToken(); +} + +/// +/// Implementation of local JWT token generation for development. +/// +public class LocalJwtTokenService : ILocalJwtTokenService +{ + private readonly JwtSettings _settings; + private readonly ILogger _logger; + + public LocalJwtTokenService(IOptions settings, ILogger logger) + { + _settings = settings.Value; + _logger = logger; + } + + public string GenerateToken(string userId, string familyId, string email, string role, string displayName) + { + if (!_settings.EnableLocalTokenGeneration) + { + throw new InvalidOperationException("Local JWT token generation is disabled. This feature is only available in development."); + } + + _logger.LogWarning( + "Generating local development JWT token for user {UserId} in family {FamilyId}. " + + "This should only happen in development environments.", + userId, familyId); + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_settings.SecretKey)); + var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var claims = new List + { + new(JwtRegisteredClaimNames.Sub, userId), + new(JwtRegisteredClaimNames.Email, email), + new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), + new(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64), + new(ClaimTypes.NameIdentifier, userId), + new(ClaimTypes.Email, email), + new(ClaimTypes.Role, role), + new(ClaimTypes.Name, displayName), + new("family_id", familyId), + new("display_name", displayName), + new("auth_method", "local_dev") + }; + + var token = new JwtSecurityToken( + issuer: _settings.Issuer, + audience: _settings.Audience, + claims: claims, + notBefore: DateTime.UtcNow, + expires: DateTime.UtcNow.AddMinutes(_settings.ExpirationMinutes), + signingCredentials: credentials); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + public string GenerateDevToken() + { + // Default development user credentials + return GenerateToken( + userId: "dev-user-001", + familyId: "dev-family-001", + email: "dev@luminous.local", + role: "Owner", + displayName: "Developer"); + } +} diff --git a/src/Luminous.Api/appsettings.Development.json b/src/Luminous.Api/appsettings.Development.json index 087ad98..114313f 100644 --- a/src/Luminous.Api/appsettings.Development.json +++ b/src/Luminous.Api/appsettings.Development.json @@ -2,7 +2,17 @@ "Logging": { "LogLevel": { "Default": "Debug", - "Microsoft.AspNetCore": "Information" + "Microsoft.AspNetCore": "Information", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "Serilog": { + "MinimumLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Information", + "System": "Warning" + } } }, "CosmosDb": { @@ -10,5 +20,45 @@ "AccountKey": "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==", "DatabaseName": "luminous", "UseManagedIdentity": false + }, + "BlobStorage": { + "ConnectionString": "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;", + "ContainerName": "luminous-files" + }, + "Redis": { + "ConnectionString": "localhost:6379", + "InstanceName": "luminous-dev:" + }, + "Jwt": { + "SecretKey": "LuminousDevSecretKey-AtLeast32Characters-ForHMACSHA256!", + "Issuer": "https://luminous.local", + "Audience": "luminous-api", + "ExpirationMinutes": 60, + "RefreshExpirationDays": 7, + "EnableLocalTokenGeneration": true + }, + "Email": { + "SmtpServer": "localhost", + "SmtpPort": 1025, + "UseSsl": false, + "FromAddress": "noreply@luminous.local", + "FromName": "Luminous Family Hub" + }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:4200", + "https://localhost:4200", + "http://localhost:4201" + ] + }, + "WebAuthn": { + "RelyingPartyId": "localhost", + "RelyingPartyName": "Luminous Family Hub (Development)", + "Origin": "http://localhost:4200" + }, + "FeatureFlags": { + "EnableDevAuth": true, + "EnableSwagger": true, + "EnableDetailedErrors": true } }