mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 01:54:00 +00:00
move diagrams and documentation
This commit is contained in:
1
docs/website/diagrams-out/architecture.svg
Normal file
1
docs/website/diagrams-out/architecture.svg
Normal file
File diff suppressed because one or more lines are too long
1
docs/website/diagrams-out/authentication-flow.svg
Normal file
1
docs/website/diagrams-out/authentication-flow.svg
Normal file
File diff suppressed because one or more lines are too long
1
docs/website/diagrams-out/database-schema.svg
Normal file
1
docs/website/diagrams-out/database-schema.svg
Normal file
File diff suppressed because one or more lines are too long
1
docs/website/diagrams-out/deployment.svg
Normal file
1
docs/website/diagrams-out/deployment.svg
Normal file
File diff suppressed because one or more lines are too long
75
docs/website/diagrams-src/architecture.puml
Normal file
75
docs/website/diagrams-src/architecture.puml
Normal file
@@ -0,0 +1,75 @@
|
||||
@startuml architecture
|
||||
!theme plain
|
||||
skinparam backgroundColor #FFFFFF
|
||||
skinparam defaultFontName Arial
|
||||
skinparam packageStyle rectangle
|
||||
|
||||
title The Biergarten App - Layered Architecture
|
||||
|
||||
package "API Layer" #E3F2FD {
|
||||
[API.Core\nASP.NET Core Web API] as API
|
||||
note right of API
|
||||
- Controllers (Auth, User)
|
||||
- Swagger/OpenAPI
|
||||
- Middleware
|
||||
- Health Checks
|
||||
end note
|
||||
}
|
||||
|
||||
package "Service Layer" #F3E5F5 {
|
||||
[Service.Auth] as AuthSvc
|
||||
[Service.UserManagement] as UserSvc
|
||||
note right of AuthSvc
|
||||
- Business Logic
|
||||
- Validation
|
||||
- Orchestration
|
||||
end note
|
||||
}
|
||||
|
||||
package "Infrastructure Layer" #FFF3E0 {
|
||||
[Infrastructure.Repository] as Repo
|
||||
[Infrastructure.Jwt] as JWT
|
||||
[Infrastructure.PasswordHashing] as PwdHash
|
||||
[Infrastructure.Email] as Email
|
||||
}
|
||||
|
||||
package "Domain Layer" #E8F5E9 {
|
||||
[Domain.Entities] as Domain
|
||||
note right of Domain
|
||||
- UserAccount
|
||||
- UserCredential
|
||||
- UserVerification
|
||||
end note
|
||||
}
|
||||
|
||||
database "SQL Server" {
|
||||
[Stored Procedures] as SP
|
||||
[Tables] as Tables
|
||||
}
|
||||
|
||||
' Relationships
|
||||
API --> AuthSvc
|
||||
API --> UserSvc
|
||||
|
||||
AuthSvc --> Repo
|
||||
AuthSvc --> JWT
|
||||
AuthSvc --> PwdHash
|
||||
AuthSvc --> Email
|
||||
|
||||
UserSvc --> Repo
|
||||
|
||||
Repo --> SP
|
||||
Repo --> Domain
|
||||
SP --> Tables
|
||||
|
||||
AuthSvc --> Domain
|
||||
UserSvc --> Domain
|
||||
|
||||
' Notes
|
||||
note left of Repo
|
||||
SQL-first approach
|
||||
All queries via
|
||||
stored procedures
|
||||
end note
|
||||
|
||||
@enduml
|
||||
298
docs/website/diagrams-src/authentication-flow.puml
Normal file
298
docs/website/diagrams-src/authentication-flow.puml
Normal file
@@ -0,0 +1,298 @@
|
||||
@startuml authentication-flow
|
||||
!theme plain
|
||||
skinparam backgroundColor #FFFFFF
|
||||
skinparam defaultFontName Arial
|
||||
skinparam sequenceMessageAlign center
|
||||
skinparam maxMessageSize 200
|
||||
|
||||
title User Authentication Flow - Expanded
|
||||
|
||||
actor User
|
||||
participant "API\nController" as API
|
||||
box "Service Layer" #LightBlue
|
||||
participant "RegisterService" as RegSvc
|
||||
participant "LoginService" as LoginSvc
|
||||
participant "TokenService" as TokenSvc
|
||||
participant "EmailService" as EmailSvc
|
||||
end box
|
||||
box "Infrastructure Layer" #LightGreen
|
||||
participant "Argon2\nInfrastructure" as Argon2
|
||||
participant "JWT\nInfrastructure" as JWT
|
||||
participant "Email\nProvider" as SMTP
|
||||
participant "Template\nProvider" as Template
|
||||
end box
|
||||
box "Repository Layer" #LightYellow
|
||||
participant "AuthRepository" as AuthRepo
|
||||
participant "UserAccount\nRepository" as UserRepo
|
||||
end box
|
||||
database "SQL Server\nStored Procedures" as DB
|
||||
|
||||
== Registration Flow ==
|
||||
|
||||
User -> API: POST /api/auth/register\n{username, firstName, lastName,\nemail, dateOfBirth, password}
|
||||
activate API
|
||||
|
||||
note right of API
|
||||
FluentValidation runs:
|
||||
- Username: 3-64 chars, alphanumeric + [._-]
|
||||
- Email: valid format, max 128 chars
|
||||
- Password: min 8 chars, uppercase,\n lowercase, number, special char
|
||||
- DateOfBirth: must be 19+ years old
|
||||
end note
|
||||
|
||||
API -> API: Validate request\n(FluentValidation)
|
||||
|
||||
alt Validation fails
|
||||
API -> User: 400 Bad Request\n{errors: {...}}
|
||||
else Validation succeeds
|
||||
API -> RegSvc: RegisterAsync(userAccount, password)
|
||||
activate RegSvc
|
||||
|
||||
RegSvc -> AuthRepo: GetUserByUsernameAsync(username)
|
||||
activate AuthRepo
|
||||
AuthRepo -> DB: EXEC usp_GetUserAccountByUsername
|
||||
activate DB
|
||||
DB --> AuthRepo: null (user doesn't exist)
|
||||
deactivate DB
|
||||
deactivate AuthRepo
|
||||
|
||||
RegSvc -> AuthRepo: GetUserByEmailAsync(email)
|
||||
activate AuthRepo
|
||||
AuthRepo -> DB: EXEC usp_GetUserAccountByEmail
|
||||
activate DB
|
||||
DB --> AuthRepo: null (email doesn't exist)
|
||||
deactivate DB
|
||||
deactivate AuthRepo
|
||||
|
||||
alt User/Email already exists
|
||||
RegSvc -> API: throw ConflictException
|
||||
API -> User: 409 Conflict\n"Username or email already exists"
|
||||
else User doesn't exist
|
||||
|
||||
RegSvc -> Argon2: Hash(password)
|
||||
activate Argon2
|
||||
note right of Argon2
|
||||
Argon2id parameters:
|
||||
- Salt: 16 bytes (128-bit)
|
||||
- Memory: 64MB
|
||||
- Iterations: 4
|
||||
- Parallelism: CPU count
|
||||
- Hash output: 32 bytes
|
||||
end note
|
||||
Argon2 -> Argon2: Generate random salt\n(16 bytes)
|
||||
Argon2 -> Argon2: Hash password with\nArgon2id algorithm
|
||||
Argon2 --> RegSvc: "base64(salt):base64(hash)"
|
||||
deactivate Argon2
|
||||
|
||||
RegSvc -> AuthRepo: RegisterUserAsync(\n username, firstName, lastName,\n email, dateOfBirth, hash)
|
||||
activate AuthRepo
|
||||
|
||||
AuthRepo -> DB: EXEC USP_RegisterUser
|
||||
activate DB
|
||||
note right of DB
|
||||
Transaction begins:
|
||||
1. INSERT UserAccount
|
||||
2. INSERT UserCredential
|
||||
(with hashed password)
|
||||
Transaction commits
|
||||
end note
|
||||
DB -> DB: BEGIN TRANSACTION
|
||||
DB -> DB: INSERT INTO UserAccount\n(Username, FirstName, LastName,\nEmail, DateOfBirth)
|
||||
DB -> DB: OUTPUT INSERTED.UserAccountID
|
||||
DB -> DB: INSERT INTO UserCredential\n(UserAccountId, Hash)
|
||||
DB -> DB: COMMIT TRANSACTION
|
||||
DB --> AuthRepo: UserAccountId (GUID)
|
||||
deactivate DB
|
||||
|
||||
AuthRepo --> RegSvc: UserAccount entity
|
||||
deactivate AuthRepo
|
||||
|
||||
RegSvc -> TokenSvc: GenerateAccessToken(userAccount)
|
||||
activate TokenSvc
|
||||
TokenSvc -> JWT: GenerateJwt(userId, username, expiry)
|
||||
activate JWT
|
||||
note right of JWT
|
||||
JWT Configuration:
|
||||
- Algorithm: HS256
|
||||
- Expires: 1 hour
|
||||
- Claims:
|
||||
* sub: userId
|
||||
* unique_name: username
|
||||
* jti: unique token ID
|
||||
end note
|
||||
JWT -> JWT: Create JWT with claims
|
||||
JWT -> JWT: Sign with secret key
|
||||
JWT --> TokenSvc: Access Token
|
||||
deactivate JWT
|
||||
TokenSvc --> RegSvc: Access Token
|
||||
deactivate TokenSvc
|
||||
|
||||
RegSvc -> TokenSvc: GenerateRefreshToken(userAccount)
|
||||
activate TokenSvc
|
||||
TokenSvc -> JWT: GenerateJwt(userId, username, expiry)
|
||||
activate JWT
|
||||
note right of JWT
|
||||
Refresh Token:
|
||||
- Expires: 21 days
|
||||
- Same structure as access token
|
||||
end note
|
||||
JWT --> TokenSvc: Refresh Token
|
||||
deactivate JWT
|
||||
TokenSvc --> RegSvc: Refresh Token
|
||||
deactivate TokenSvc
|
||||
|
||||
RegSvc -> EmailSvc: SendRegistrationEmailAsync(\n createdUser, confirmationToken)
|
||||
activate EmailSvc
|
||||
|
||||
EmailSvc -> Template: RenderUserRegisteredEmailAsync(\n firstName, confirmationLink)
|
||||
activate Template
|
||||
note right of Template
|
||||
Razor Component:
|
||||
- Header with branding
|
||||
- Welcome message
|
||||
- Confirmation button
|
||||
- Footer
|
||||
end note
|
||||
Template -> Template: Render Razor component\nto HTML
|
||||
Template --> EmailSvc: HTML email content
|
||||
deactivate Template
|
||||
|
||||
EmailSvc -> SMTP: SendAsync(email, subject, body)
|
||||
activate SMTP
|
||||
note right of SMTP
|
||||
SMTP Configuration:
|
||||
- Host: from env (SMTP_HOST)
|
||||
- Port: from env (SMTP_PORT)
|
||||
- TLS: StartTLS
|
||||
- Auth: username/password
|
||||
end note
|
||||
SMTP -> SMTP: Create MIME message
|
||||
SMTP -> SMTP: Connect to SMTP server
|
||||
SMTP -> SMTP: Authenticate
|
||||
SMTP -> SMTP: Send email
|
||||
SMTP -> SMTP: Disconnect
|
||||
SMTP --> EmailSvc: Success / Failure
|
||||
deactivate SMTP
|
||||
|
||||
alt Email sent successfully
|
||||
EmailSvc --> RegSvc: emailSent = true
|
||||
else Email failed
|
||||
EmailSvc --> RegSvc: emailSent = false\n(error suppressed)
|
||||
end
|
||||
deactivate EmailSvc
|
||||
|
||||
RegSvc --> API: RegisterServiceReturn(\n userAccount, accessToken,\n refreshToken, emailSent)
|
||||
deactivate RegSvc
|
||||
|
||||
API -> API: Create response body
|
||||
API -> User: 201 Created\n{\n message: "User registered successfully",\n payload: {\n userAccountId, username,\n accessToken, refreshToken,\n confirmationEmailSent\n }\n}
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
|
||||
== Login Flow ==
|
||||
|
||||
User -> API: POST /api/auth/login\n{username, password}
|
||||
activate API
|
||||
|
||||
API -> API: Validate request\n(FluentValidation)
|
||||
|
||||
alt Validation fails
|
||||
API -> User: 400 Bad Request\n{errors: {...}}
|
||||
else Validation succeeds
|
||||
|
||||
API -> LoginSvc: LoginAsync(username, password)
|
||||
activate LoginSvc
|
||||
|
||||
LoginSvc -> AuthRepo: GetUserByUsernameAsync(username)
|
||||
activate AuthRepo
|
||||
AuthRepo -> DB: EXEC usp_GetUserAccountByUsername
|
||||
activate DB
|
||||
DB -> DB: SELECT FROM UserAccount\nWHERE Username = @Username
|
||||
DB --> AuthRepo: UserAccount entity
|
||||
deactivate DB
|
||||
deactivate AuthRepo
|
||||
|
||||
alt User not found
|
||||
LoginSvc -> API: throw UnauthorizedException\n"Invalid username or password"
|
||||
API -> User: 401 Unauthorized
|
||||
else User found
|
||||
|
||||
LoginSvc -> AuthRepo: GetActiveCredentialByUserAccountIdAsync(userId)
|
||||
activate AuthRepo
|
||||
AuthRepo -> DB: EXEC USP_GetActiveUserCredentialByUserAccountId
|
||||
activate DB
|
||||
note right of DB
|
||||
SELECT FROM UserCredential
|
||||
WHERE UserAccountId = @UserAccountId
|
||||
AND IsRevoked = 0
|
||||
end note
|
||||
DB --> AuthRepo: UserCredential entity
|
||||
deactivate DB
|
||||
deactivate AuthRepo
|
||||
|
||||
alt No active credential
|
||||
LoginSvc -> API: throw UnauthorizedException
|
||||
API -> User: 401 Unauthorized
|
||||
else Active credential found
|
||||
|
||||
LoginSvc -> Argon2: Verify(password, storedHash)
|
||||
activate Argon2
|
||||
note right of Argon2
|
||||
1. Split stored hash: "salt:hash"
|
||||
2. Extract salt
|
||||
3. Hash provided password\n with same salt
|
||||
4. Constant-time comparison
|
||||
end note
|
||||
Argon2 -> Argon2: Parse salt from stored hash
|
||||
Argon2 -> Argon2: Hash provided password\nwith extracted salt
|
||||
Argon2 -> Argon2: FixedTimeEquals(\n computed, stored)
|
||||
Argon2 --> LoginSvc: true/false
|
||||
deactivate Argon2
|
||||
|
||||
alt Password invalid
|
||||
LoginSvc -> API: throw UnauthorizedException
|
||||
API -> User: 401 Unauthorized
|
||||
else Password valid
|
||||
|
||||
LoginSvc -> TokenSvc: GenerateAccessToken(user)
|
||||
activate TokenSvc
|
||||
TokenSvc -> JWT: GenerateJwt(...)
|
||||
activate JWT
|
||||
JWT --> TokenSvc: Access Token
|
||||
deactivate JWT
|
||||
TokenSvc --> LoginSvc: Access Token
|
||||
deactivate TokenSvc
|
||||
|
||||
LoginSvc -> TokenSvc: GenerateRefreshToken(user)
|
||||
activate TokenSvc
|
||||
TokenSvc -> JWT: GenerateJwt(...)
|
||||
activate JWT
|
||||
JWT --> TokenSvc: Refresh Token
|
||||
deactivate JWT
|
||||
TokenSvc --> LoginSvc: Refresh Token
|
||||
deactivate TokenSvc
|
||||
|
||||
LoginSvc --> API: LoginServiceReturn(\n userAccount, accessToken,\n refreshToken)
|
||||
deactivate LoginSvc
|
||||
|
||||
API -> User: 200 OK\n{\n message: "Logged in successfully",\n payload: {\n userAccountId, username,\n accessToken, refreshToken\n }\n}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
deactivate API
|
||||
|
||||
== Error Handling (Global Exception Filter) ==
|
||||
|
||||
note over API
|
||||
GlobalExceptionFilter catches:
|
||||
- ValidationException → 400 Bad Request
|
||||
- ConflictException → 409 Conflict
|
||||
- NotFoundException → 404 Not Found
|
||||
- UnauthorizedException → 401 Unauthorized
|
||||
- ForbiddenException → 403 Forbidden
|
||||
- All others → 500 Internal Server Error
|
||||
end note
|
||||
|
||||
@enduml
|
||||
104
docs/website/diagrams-src/database-schema.puml
Normal file
104
docs/website/diagrams-src/database-schema.puml
Normal file
@@ -0,0 +1,104 @@
|
||||
@startuml database-schema
|
||||
!theme plain
|
||||
skinparam backgroundColor #FFFFFF
|
||||
skinparam defaultFontName Arial
|
||||
skinparam linetype ortho
|
||||
|
||||
title Key Database Schema - User & Authentication
|
||||
|
||||
entity "UserAccount" as User {
|
||||
* UserAccountId: INT <<PK>>
|
||||
--
|
||||
* Username: NVARCHAR(30) <<UNIQUE>>
|
||||
* Email: NVARCHAR(255) <<UNIQUE>>
|
||||
* FirstName: NVARCHAR(50)
|
||||
* LastName: NVARCHAR(50)
|
||||
Bio: NVARCHAR(500)
|
||||
CreatedAt: DATETIME2
|
||||
UpdatedAt: DATETIME2
|
||||
LastLoginAt: DATETIME2
|
||||
}
|
||||
|
||||
entity "UserCredential" as Cred {
|
||||
* UserCredentialId: INT <<PK>>
|
||||
--
|
||||
* UserAccountId: INT <<FK>>
|
||||
* PasswordHash: VARBINARY(32)
|
||||
* PasswordSalt: VARBINARY(16)
|
||||
CredentialRotatedAt: DATETIME2
|
||||
CredentialExpiresAt: DATETIME2
|
||||
CredentialRevokedAt: DATETIME2
|
||||
* IsActive: BIT
|
||||
CreatedAt: DATETIME2
|
||||
}
|
||||
|
||||
entity "UserVerification" as Verify {
|
||||
* UserVerificationId: INT <<PK>>
|
||||
--
|
||||
* UserAccountId: INT <<FK>>
|
||||
* IsVerified: BIT
|
||||
VerifiedAt: DATETIME2
|
||||
VerificationToken: NVARCHAR(255)
|
||||
TokenExpiresAt: DATETIME2
|
||||
}
|
||||
|
||||
entity "UserAvatar" as Avatar {
|
||||
* UserAvatarId: INT <<PK>>
|
||||
--
|
||||
* UserAccountId: INT <<FK>>
|
||||
PhotoId: INT <<FK>>
|
||||
* IsActive: BIT
|
||||
CreatedAt: DATETIME2
|
||||
}
|
||||
|
||||
entity "UserFollow" as Follow {
|
||||
* UserFollowId: INT <<PK>>
|
||||
--
|
||||
* FollowerUserId: INT <<FK>>
|
||||
* FollowedUserId: INT <<FK>>
|
||||
CreatedAt: DATETIME2
|
||||
}
|
||||
|
||||
entity "Photo" as Photo {
|
||||
* PhotoId: INT <<PK>>
|
||||
--
|
||||
* Url: NVARCHAR(500)
|
||||
* CloudinaryPublicId: NVARCHAR(255)
|
||||
Width: INT
|
||||
Height: INT
|
||||
Format: NVARCHAR(10)
|
||||
CreatedAt: DATETIME2
|
||||
}
|
||||
|
||||
' Relationships
|
||||
User ||--o{ Cred : "has"
|
||||
User ||--o| Verify : "has"
|
||||
User ||--o{ Avatar : "has"
|
||||
User ||--o{ Follow : "follows"
|
||||
User ||--o{ Follow : "followed by"
|
||||
Avatar }o--|| Photo : "refers to"
|
||||
|
||||
note right of Cred
|
||||
Password hashing:
|
||||
- Algorithm: Argon2id
|
||||
- Memory: 64MB
|
||||
- Iterations: 4
|
||||
- Salt: 128-bit
|
||||
- Hash: 256-bit
|
||||
end note
|
||||
|
||||
note right of Verify
|
||||
Account verification
|
||||
via email token
|
||||
with expiry
|
||||
end note
|
||||
|
||||
note bottom of User
|
||||
Core stored procedures:
|
||||
- USP_RegisterUser
|
||||
- USP_GetUserAccountByUsername
|
||||
- USP_RotateUserCredential
|
||||
- USP_UpdateUserAccount
|
||||
end note
|
||||
|
||||
@enduml
|
||||
227
docs/website/diagrams-src/deployment.puml
Normal file
227
docs/website/diagrams-src/deployment.puml
Normal file
@@ -0,0 +1,227 @@
|
||||
@startuml deployment
|
||||
!theme plain
|
||||
skinparam backgroundColor #FFFFFF
|
||||
skinparam defaultFontName Arial
|
||||
skinparam linetype ortho
|
||||
|
||||
title Docker Deployment Architecture
|
||||
|
||||
' External systems
|
||||
actor Developer
|
||||
cloud "Docker Host" as Host
|
||||
|
||||
package "Development Environment\n(docker-compose.dev.yaml)" #E3F2FD {
|
||||
|
||||
node "SQL Server\n(mcr.microsoft.com/mssql/server:2022-latest)" as DevDB {
|
||||
database "Biergarten\nDatabase" as DevDBInner {
|
||||
portin "1433"
|
||||
}
|
||||
note right
|
||||
Environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=***
|
||||
- MSSQL_PID=Developer
|
||||
|
||||
Volumes:
|
||||
- biergarten-dev-data
|
||||
end note
|
||||
}
|
||||
|
||||
node "API Container\n(API.Core)" as DevAPI {
|
||||
component "ASP.NET Core 10" as API1
|
||||
portin "8080:8080 (HTTP)" as DevPort1
|
||||
portin "8081:8081 (HTTPS)" as DevPort2
|
||||
|
||||
note right
|
||||
Environment:
|
||||
- ASPNETCORE_ENVIRONMENT=Development
|
||||
- DB_SERVER=sql-server
|
||||
- DB_NAME=Biergarten
|
||||
- DB_USER/PASSWORD
|
||||
- JWT_SECRET
|
||||
- SMTP_* (10+ variables)
|
||||
|
||||
Health Check:
|
||||
/health endpoint
|
||||
end note
|
||||
}
|
||||
|
||||
node "Migrations\n(run-once)" as DevMig {
|
||||
component "Database.Migrations" as Mig1
|
||||
note bottom
|
||||
Runs: DbUp migrations
|
||||
Environment:
|
||||
- CLEAR_DATABASE=false
|
||||
Depends on: sql-server
|
||||
end note
|
||||
}
|
||||
|
||||
node "Seed\n(run-once)" as DevSeed {
|
||||
component "Database.Seed" as Seed1
|
||||
note bottom
|
||||
Creates:
|
||||
- 100 test users
|
||||
- Location data (US/CA/MX)
|
||||
- test.user account
|
||||
Depends on: migrations
|
||||
end note
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
package "Test Environment\n(docker-compose.test.yaml)" #FFF3E0 {
|
||||
|
||||
node "SQL Server\n(isolated instance)" as TestDB {
|
||||
database "Biergarten\nTest Database" as TestDBInner {
|
||||
portin "1434"
|
||||
}
|
||||
note right
|
||||
Fresh instance each run
|
||||
CLEAR_DATABASE=true
|
||||
|
||||
Volumes:
|
||||
- biergarten-test-data
|
||||
(ephemeral)
|
||||
end note
|
||||
}
|
||||
|
||||
node "Migrations\n(test)" as TestMig {
|
||||
component "Database.Migrations"
|
||||
}
|
||||
|
||||
node "Seed\n(test)" as TestSeed {
|
||||
component "Database.Seed"
|
||||
note bottom
|
||||
Minimal seed:
|
||||
- test.user only
|
||||
- Essential data
|
||||
end note
|
||||
}
|
||||
|
||||
node "API.Specs\n(Integration Tests)" as Specs {
|
||||
component "Reqnroll + xUnit" as SpecsComp
|
||||
note right
|
||||
Tests:
|
||||
- Registration flow
|
||||
- Login flow
|
||||
- Validation rules
|
||||
- 404 handling
|
||||
|
||||
Uses: TestApiFactory
|
||||
Mocks: Email services
|
||||
end note
|
||||
}
|
||||
|
||||
node "Infrastructure.Repository.Tests\n(Unit Tests)" as RepoTests {
|
||||
component "xUnit + DbMocker" as RepoComp
|
||||
note right
|
||||
Tests:
|
||||
- AuthRepository
|
||||
- UserAccountRepository
|
||||
- SQL command building
|
||||
|
||||
Uses: Mock connections
|
||||
No real database needed
|
||||
end note
|
||||
}
|
||||
|
||||
node "Service.Auth.Tests\n(Unit Tests)" as SvcTests {
|
||||
component "xUnit + Moq" as SvcComp
|
||||
note right
|
||||
Tests:
|
||||
- RegisterService
|
||||
- LoginService
|
||||
- Token generation
|
||||
|
||||
Uses: Mocked dependencies
|
||||
No database or infrastructure
|
||||
end note
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
folder "test-results/\n(mounted volume)" as Results {
|
||||
file "api-specs/\n results.trx" as Result1
|
||||
file "repository-tests/\n results.trx" as Result2
|
||||
file "service-auth-tests/\n results.trx" as Result3
|
||||
|
||||
note bottom
|
||||
TRX format
|
||||
Readable by:
|
||||
- Visual Studio
|
||||
- Azure DevOps
|
||||
- GitHub Actions
|
||||
end note
|
||||
}
|
||||
|
||||
' External access
|
||||
Developer --> Host : docker compose up
|
||||
Host --> DevAPI : http://localhost:8080
|
||||
|
||||
' Development dependencies
|
||||
DevMig --> DevDB : 1. Run migrations
|
||||
DevSeed --> DevDB : 2. Seed data
|
||||
DevAPI --> DevDB : 3. Connect & serve
|
||||
DevMig .up.> DevDB : depends_on
|
||||
DevSeed .up.> DevMig : depends_on
|
||||
DevAPI .up.> DevSeed : depends_on
|
||||
|
||||
' Test dependencies
|
||||
TestMig --> TestDB : 1. Migrate
|
||||
TestSeed --> TestDB : 2. Seed
|
||||
Specs --> TestDB : 3. Integration test
|
||||
RepoTests ..> TestDB : Mock (no connection)
|
||||
SvcTests ..> TestDB : Mock (no connection)
|
||||
|
||||
TestMig .up.> TestDB : depends_on
|
||||
TestSeed .up.> TestMig : depends_on
|
||||
Specs .up.> TestSeed : depends_on
|
||||
|
||||
' Test results export
|
||||
Specs --> Results : Export TRX
|
||||
RepoTests --> Results : Export TRX
|
||||
SvcTests --> Results : Export TRX
|
||||
|
||||
' Network notes
|
||||
note bottom of DevDB
|
||||
<b>Dev Network (bridge: biergarten-dev)</b>
|
||||
Internal DNS:
|
||||
- sql-server (resolves to SQL container)
|
||||
- api (resolves to API container)
|
||||
end note
|
||||
|
||||
note bottom of TestDB
|
||||
<b>Test Network (bridge: biergarten-test)</b>
|
||||
All test components isolated
|
||||
end note
|
||||
|
||||
' Startup sequence notes
|
||||
note top of DevMig
|
||||
Startup Order:
|
||||
1. SQL Server (health check)
|
||||
2. Migrations (run-once)
|
||||
3. Seed (run-once)
|
||||
4. API (long-running)
|
||||
end note
|
||||
|
||||
note top of Specs
|
||||
Test Execution:
|
||||
All tests run in parallel
|
||||
Results aggregated
|
||||
end note
|
||||
|
||||
' Production note
|
||||
note as ProductionNote
|
||||
<b>Production Deployment (not shown):</b>
|
||||
|
||||
Would include:
|
||||
• Azure SQL Database / AWS RDS
|
||||
• Azure Container Apps / ECS
|
||||
• Azure Key Vault for secrets
|
||||
• Application Insights / CloudWatch
|
||||
• Load balancer
|
||||
• HTTPS termination
|
||||
• CDN for static assets
|
||||
end note
|
||||
|
||||
@enduml
|
||||
332
docs/website/docker.md
Normal file
332
docs/website/docker.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# Docker Guide
|
||||
|
||||
This document covers Docker deployment, configuration, and troubleshooting for
|
||||
The Biergarten App.
|
||||
|
||||
## Overview
|
||||
|
||||
The project uses Docker Compose to orchestrate multiple services:
|
||||
|
||||
- SQL Server 2022 database
|
||||
- Database migrations runner (DbUp)
|
||||
- Database seeder
|
||||
- .NET API
|
||||
- Test runners
|
||||
|
||||
See the [deployment diagram](diagrams/pdf/deployment.pdf) for visual
|
||||
representation.
|
||||
|
||||
## Docker Compose Environments
|
||||
|
||||
### 1. Development (`docker-compose.dev.yaml`)
|
||||
|
||||
**Purpose**: Local development with persistent data
|
||||
|
||||
**Features**:
|
||||
|
||||
- Persistent SQL Server volume
|
||||
- Hot reload support
|
||||
- Swagger UI enabled
|
||||
- Seed data included
|
||||
- `CLEAR_DATABASE=true` (drops and recreates schema)
|
||||
|
||||
**Services**:
|
||||
|
||||
```yaml
|
||||
sqlserver # SQL Server 2022 (port 1433)
|
||||
database.migrations # DbUp migrations
|
||||
database.seed # Seed initial data
|
||||
api.core # Web API (ports 8080, 8081)
|
||||
```
|
||||
|
||||
**Start Development Environment**:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yaml up -d
|
||||
```
|
||||
|
||||
**Access**:
|
||||
|
||||
- API Swagger: http://localhost:8080/swagger
|
||||
- Health Check: http://localhost:8080/health
|
||||
- SQL Server: localhost:1433 (sa credentials from .env.dev)
|
||||
|
||||
**Stop Environment**:
|
||||
|
||||
```bash
|
||||
# Stop services (keep volumes)
|
||||
docker compose -f docker-compose.dev.yaml down
|
||||
|
||||
# Stop and remove volumes (fresh start)
|
||||
docker compose -f docker-compose.dev.yaml down -v
|
||||
```
|
||||
|
||||
### 2. Testing (`docker-compose.test.yaml`)
|
||||
|
||||
**Purpose**: Automated CI/CD testing in isolated environment
|
||||
|
||||
**Features**:
|
||||
|
||||
- Fresh database each run
|
||||
- All test suites execute in parallel
|
||||
- Test results exported to `./test-results/`
|
||||
- Containers auto-exit after completion
|
||||
- Fully isolated testnet network
|
||||
|
||||
**Services**:
|
||||
|
||||
```yaml
|
||||
sqlserver # Test database
|
||||
database.migrations # Fresh schema
|
||||
database.seed # Test data
|
||||
api.specs # Reqnroll BDD tests
|
||||
repository.tests # Repository unit tests
|
||||
service.auth.tests # Service unit tests
|
||||
```
|
||||
|
||||
**Run Tests**:
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
||||
|
||||
# View results
|
||||
ls -la test-results/
|
||||
cat test-results/api-specs/results.trx
|
||||
cat test-results/repository-tests/results.trx
|
||||
cat test-results/service-auth-tests/results.trx
|
||||
|
||||
# Clean up
|
||||
docker compose -f docker-compose.test.yaml down -v
|
||||
```
|
||||
|
||||
### 3. Production (`docker-compose.prod.yaml`)
|
||||
|
||||
**Purpose**: Production-ready deployment
|
||||
|
||||
**Features**:
|
||||
|
||||
- Production logging levels
|
||||
- No database clearing
|
||||
- Optimized build configurations
|
||||
- Health checks enabled
|
||||
- Restart policies (unless-stopped)
|
||||
- Security hardening
|
||||
|
||||
**Services**:
|
||||
|
||||
```yaml
|
||||
sqlserver # Production SQL Server
|
||||
database.migrations # Schema updates only
|
||||
api.core # Production API
|
||||
```
|
||||
|
||||
**Deploy Production**:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.prod.yaml up -d
|
||||
```
|
||||
|
||||
## Service Dependencies
|
||||
|
||||
Docker Compose manages startup order using health checks:
|
||||
|
||||
```mermaid
|
||||
sqlserver (health check)
|
||||
↓
|
||||
database.migrations (completes successfully)
|
||||
↓
|
||||
database.seed (completes successfully)
|
||||
↓
|
||||
api.core / tests (start when ready)
|
||||
```
|
||||
|
||||
**Health Check Example** (SQL Server):
|
||||
|
||||
```yaml
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1'",
|
||||
]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
start_period: 30s
|
||||
```
|
||||
|
||||
**Dependency Configuration**:
|
||||
|
||||
```yaml
|
||||
api.core:
|
||||
depends_on:
|
||||
database.seed:
|
||||
condition: service_completed_successfully
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
### Persistent Volumes
|
||||
|
||||
**Development**:
|
||||
|
||||
- `sqlserverdata-dev` - Database files persist between restarts
|
||||
- `nuget-cache-dev` - NuGet package cache (speeds up builds)
|
||||
|
||||
**Testing**:
|
||||
|
||||
- `sqlserverdata-test` - Temporary, typically removed after tests
|
||||
|
||||
**Production**:
|
||||
|
||||
- `sqlserverdata-prod` - Production database files
|
||||
- `nuget-cache-prod` - Production NuGet cache
|
||||
|
||||
### Mounted Volumes
|
||||
|
||||
**Test Results**:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./test-results:/app/test-results
|
||||
```
|
||||
|
||||
Test results are written to host filesystem for CI/CD integration.
|
||||
|
||||
**Code Volumes** (development only):
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- ./src:/app/src # Hot reload for development
|
||||
```
|
||||
|
||||
## Networks
|
||||
|
||||
Each environment uses isolated bridge networks:
|
||||
|
||||
- `devnet` - Development network
|
||||
- `testnet` - Testing network (fully isolated)
|
||||
- `prodnet` - Production network
|
||||
|
||||
## Environment Variables
|
||||
|
||||
All containers are configured via environment variables from `.env` files:
|
||||
|
||||
```yaml
|
||||
env_file: ".env.dev" # or .env.test, .env.prod
|
||||
|
||||
environment:
|
||||
ASPNETCORE_ENVIRONMENT: "Development"
|
||||
DOTNET_RUNNING_IN_CONTAINER: "true"
|
||||
DB_SERVER: "${DB_SERVER}"
|
||||
DB_NAME: "${DB_NAME}"
|
||||
DB_USER: "${DB_USER}"
|
||||
DB_PASSWORD: "${DB_PASSWORD}"
|
||||
JWT_SECRET: "${JWT_SECRET}"
|
||||
```
|
||||
|
||||
For complete list, see [Environment Variables](environment-variables.md).
|
||||
|
||||
## Common Commands
|
||||
|
||||
### View Services
|
||||
|
||||
```bash
|
||||
# Running services
|
||||
docker compose -f docker-compose.dev.yaml ps
|
||||
|
||||
# All containers (including stopped)
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
### View Logs
|
||||
|
||||
```bash
|
||||
# All services
|
||||
docker compose -f docker-compose.dev.yaml logs -f
|
||||
|
||||
# Specific service
|
||||
docker compose -f docker-compose.dev.yaml logs -f api.core
|
||||
|
||||
# Last 100 lines
|
||||
docker compose -f docker-compose.dev.yaml logs --tail=100 api.core
|
||||
```
|
||||
|
||||
### Execute Commands in Container
|
||||
|
||||
```bash
|
||||
# Interactive shell
|
||||
docker exec -it dev-env-api-core bash
|
||||
|
||||
# Run command
|
||||
docker exec dev-env-sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'password' -C
|
||||
```
|
||||
|
||||
### Restart Services
|
||||
|
||||
```bash
|
||||
# Restart all services
|
||||
docker compose -f docker-compose.dev.yaml restart
|
||||
|
||||
# Restart specific service
|
||||
docker compose -f docker-compose.dev.yaml restart api.core
|
||||
|
||||
# Rebuild and restart
|
||||
docker compose -f docker-compose.dev.yaml up -d --build api.core
|
||||
```
|
||||
|
||||
### Build Images
|
||||
|
||||
```bash
|
||||
# Build all images
|
||||
docker compose -f docker-compose.dev.yaml build
|
||||
|
||||
# Build specific service
|
||||
docker compose -f docker-compose.dev.yaml build api.core
|
||||
|
||||
# Build without cache
|
||||
docker compose -f docker-compose.dev.yaml build --no-cache
|
||||
```
|
||||
|
||||
### Clean Up
|
||||
|
||||
```bash
|
||||
# Stop and remove containers
|
||||
docker compose -f docker-compose.dev.yaml down
|
||||
|
||||
# Remove containers and volumes
|
||||
docker compose -f docker-compose.dev.yaml down -v
|
||||
|
||||
# Remove containers, volumes, and images
|
||||
docker compose -f docker-compose.dev.yaml down -v --rmi all
|
||||
|
||||
# System-wide cleanup
|
||||
docker system prune -af --volumes
|
||||
```
|
||||
|
||||
## Dockerfile Structure
|
||||
|
||||
### Multi-Stage Build
|
||||
|
||||
```dockerfile
|
||||
# Stage 1: Build
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
COPY ["Project/Project.csproj", "Project/"]
|
||||
RUN dotnet restore
|
||||
COPY . .
|
||||
RUN dotnet build -c Release
|
||||
|
||||
# Stage 2: Runtime
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/build .
|
||||
ENTRYPOINT ["dotnet", "Project.dll"]
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
||||
- [.NET Docker Images](https://hub.docker.com/_/microsoft-dotnet)
|
||||
- [SQL Server Docker Images](https://hub.docker.com/_/microsoft-mssql-server)
|
||||
306
docs/website/environment-variables.md
Normal file
306
docs/website/environment-variables.md
Normal file
@@ -0,0 +1,306 @@
|
||||
# Environment Variables
|
||||
|
||||
This document covers the active environment variables used by the current
|
||||
Biergarten stack.
|
||||
|
||||
## Overview
|
||||
|
||||
The application uses environment variables for:
|
||||
|
||||
- **.NET API backend** - database connections, token secrets, runtime settings
|
||||
- **React Router website** - API base URL and session signing
|
||||
- **Docker containers** - environment-specific orchestration
|
||||
|
||||
## Configuration Patterns
|
||||
|
||||
### Backend (.NET API)
|
||||
|
||||
Direct environment variable access via `Environment.GetEnvironmentVariable()`.
|
||||
|
||||
### Frontend (`src/Website`)
|
||||
|
||||
The active website reads runtime values from the server environment for its auth
|
||||
and API integration.
|
||||
|
||||
### Docker
|
||||
|
||||
Environment-specific `.env` files loaded via `env_file:` in docker-compose.yaml:
|
||||
|
||||
- `.env.dev` - Development
|
||||
- `.env.test` - Testing
|
||||
- `.env.prod` - Production
|
||||
|
||||
## Backend Variables (.NET API)
|
||||
|
||||
### Database Connection
|
||||
|
||||
**Option 1: Component-Based (Recommended for Docker)**
|
||||
|
||||
Build connection string from individual components:
|
||||
|
||||
```bash
|
||||
DB_SERVER=sqlserver,1433 # SQL Server host and port
|
||||
DB_NAME=Biergarten # Database name
|
||||
DB_USER=sa # SQL Server username
|
||||
DB_PASSWORD=YourStrong!Passw0rd # SQL Server password
|
||||
DB_TRUST_SERVER_CERTIFICATE=True # Optional, defaults to True
|
||||
```
|
||||
|
||||
**Option 2: Full Connection String (Local Development)**
|
||||
|
||||
Provide complete connection string:
|
||||
|
||||
```bash
|
||||
DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
|
||||
```
|
||||
|
||||
**Priority**: `DB_CONNECTION_STRING` is checked first. If not found, connection
|
||||
string is built from components.
|
||||
|
||||
**Implementation**: See `DefaultSqlConnectionFactory.cs`
|
||||
|
||||
### JWT Authentication Secrets (Backend)
|
||||
|
||||
The backend uses separate secrets for different token types to enable
|
||||
independent key rotation and validation isolation.
|
||||
|
||||
```bash
|
||||
# Access token secret (1-hour tokens)
|
||||
ACCESS_TOKEN_SECRET=<generated-secret> # Signs short-lived access tokens
|
||||
|
||||
# Refresh token secret (21-day tokens)
|
||||
REFRESH_TOKEN_SECRET=<generated-secret> # Signs long-lived refresh tokens
|
||||
|
||||
# Confirmation token secret (30-minute tokens)
|
||||
CONFIRMATION_TOKEN_SECRET=<generated-secret> # Signs email confirmation tokens
|
||||
|
||||
# Website base URL (used in confirmation emails)
|
||||
WEBSITE_BASE_URL=https://thebiergarten.app # Base URL for the website
|
||||
```
|
||||
|
||||
**Security Requirements**:
|
||||
|
||||
- Each secret should be minimum 32 characters
|
||||
- Recommend 127+ characters for production
|
||||
- Generate using cryptographically secure random functions
|
||||
- Never reuse secrets across token types or environments
|
||||
- Rotate secrets periodically in production
|
||||
|
||||
**Generate Secrets**:
|
||||
|
||||
```bash
|
||||
# macOS/Linux - Generate 127-character base64 secret
|
||||
openssl rand -base64 127
|
||||
|
||||
# Windows PowerShell
|
||||
[Convert]::ToBase64String((1..127 | %{Get-Random -Max 256}))
|
||||
```
|
||||
|
||||
**Token Expiration**:
|
||||
|
||||
- **Access tokens**: 1 hour
|
||||
- **Refresh tokens**: 21 days
|
||||
- **Confirmation tokens**: 30 minutes
|
||||
|
||||
(Defined in `TokenServiceExpirationHours` class)
|
||||
|
||||
**JWT Implementation**:
|
||||
|
||||
- **Algorithm**: HS256 (HMAC-SHA256)
|
||||
- **Handler**: Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
|
||||
- **Validation**: Token signature, expiration, and malformed token checks
|
||||
|
||||
### Migration Control
|
||||
|
||||
```bash
|
||||
CLEAR_DATABASE=true
|
||||
```
|
||||
|
||||
- **Required**: No
|
||||
- **Default**: false
|
||||
- **Effect**: If "true", drops and recreates database during migrations
|
||||
- **Usage**: Development and testing environments ONLY
|
||||
- **Warning**: NEVER use in production
|
||||
|
||||
### ASP.NET Core Configuration
|
||||
|
||||
```bash
|
||||
ASPNETCORE_ENVIRONMENT=Development # Development, Production, Staging
|
||||
ASPNETCORE_URLS=http://0.0.0.0:8080 # Binding address and port
|
||||
DOTNET_RUNNING_IN_CONTAINER=true # Flag for container execution
|
||||
```
|
||||
|
||||
## Frontend Variables (`src/Website`)
|
||||
|
||||
The active website does not use the old Next.js/Prisma environment model. Its
|
||||
core runtime variables are:
|
||||
|
||||
```bash
|
||||
API_BASE_URL=http://localhost:8080 # Base URL for the .NET API
|
||||
SESSION_SECRET=<generated-secret> # Cookie session signing secret
|
||||
NODE_ENV=development # Standard Node runtime mode
|
||||
```
|
||||
|
||||
### Frontend Variable Details
|
||||
|
||||
#### `API_BASE_URL`
|
||||
|
||||
- **Required**: Yes for local development
|
||||
- **Default in code**: `http://localhost:8080`
|
||||
- **Used by**: `src/Website/app/lib/auth.server.ts`
|
||||
- **Purpose**: Routes website auth actions to the .NET API
|
||||
|
||||
#### `SESSION_SECRET`
|
||||
|
||||
- **Required**: Strongly recommended in all environments
|
||||
- **Default in local code path**: `dev-secret-change-me`
|
||||
- **Used by**: React Router cookie session storage in `auth.server.ts`
|
||||
- **Purpose**: Signs and validates the website session cookie
|
||||
|
||||
#### `NODE_ENV`
|
||||
|
||||
- **Required**: No
|
||||
- **Typical values**: `development`, `production`, `test`
|
||||
- **Purpose**: Controls secure cookie behavior and runtime mode
|
||||
|
||||
### Admin Account (Seeding)
|
||||
|
||||
```bash
|
||||
ADMIN_PASSWORD=SecureAdminPassword123! # Initial admin password for seeding
|
||||
```
|
||||
|
||||
- **Required**: No (only needed for seeding)
|
||||
- **Purpose**: Sets admin account password during database seeding
|
||||
- **Security**: Use strong password, change immediately in production
|
||||
|
||||
## Docker-Specific Variables
|
||||
|
||||
### SQL Server Container
|
||||
|
||||
```bash
|
||||
SA_PASSWORD=YourStrong!Passw0rd # SQL Server SA password
|
||||
ACCEPT_EULA=Y # Accept SQL Server EULA (required)
|
||||
MSSQL_PID=Express # SQL Server edition (Express, Developer, Enterprise)
|
||||
```
|
||||
|
||||
**Password Requirements**:
|
||||
|
||||
- Minimum 8 characters
|
||||
- Uppercase, lowercase, digits, and special characters
|
||||
- Maps to `DB_PASSWORD` for application containers
|
||||
|
||||
## Environment File Structure
|
||||
|
||||
### Backend/Docker (Root Directory)
|
||||
|
||||
```
|
||||
.env.example # Template (tracked in Git)
|
||||
.env.dev # Development config (gitignored)
|
||||
.env.test # Testing config (gitignored)
|
||||
.env.prod # Production config (gitignored)
|
||||
```
|
||||
|
||||
**Setup**:
|
||||
|
||||
```bash
|
||||
cp .env.example .env.dev
|
||||
# Edit .env.dev with your values
|
||||
```
|
||||
|
||||
## Legacy Frontend Variables
|
||||
|
||||
Variables for the archived Next.js frontend (`src/Website-v1`) have been removed
|
||||
from this active reference. See
|
||||
[archive/legacy-website-v1.md](archive/legacy-website-v1.md) if you need the
|
||||
legacy Prisma, Cloudinary, Mapbox, or SparkPost notes.
|
||||
|
||||
**Docker Compose Mapping**:
|
||||
|
||||
- `docker-compose.dev.yaml` → `.env.dev`
|
||||
- `docker-compose.test.yaml` → `.env.test`
|
||||
- `docker-compose.prod.yaml` → `.env.prod`
|
||||
|
||||
## Variable Reference Table
|
||||
|
||||
| Variable | Backend | Frontend | Docker | Required | Notes |
|
||||
| ----------------------------- | :-----: | :------: | :----: | :------: | -------------------------- |
|
||||
| `DB_SERVER` | ✓ | | ✓ | Yes\* | SQL Server address |
|
||||
| `DB_NAME` | ✓ | | ✓ | Yes\* | Database name |
|
||||
| `DB_USER` | ✓ | | ✓ | Yes\* | SQL username |
|
||||
| `DB_PASSWORD` | ✓ | | ✓ | Yes\* | SQL password |
|
||||
| `DB_CONNECTION_STRING` | ✓ | | | Yes\* | Alternative to components |
|
||||
| `DB_TRUST_SERVER_CERTIFICATE` | ✓ | | ✓ | No | Defaults to `True` |
|
||||
| `ACCESS_TOKEN_SECRET` | ✓ | | ✓ | Yes | Access token signing |
|
||||
| `REFRESH_TOKEN_SECRET` | ✓ | | ✓ | Yes | Refresh token signing |
|
||||
| `CONFIRMATION_TOKEN_SECRET` | ✓ | | ✓ | Yes | Confirmation token signing |
|
||||
| `WEBSITE_BASE_URL` | ✓ | | | Yes | Website URL for emails |
|
||||
| `API_BASE_URL` | | ✓ | | Yes | Website-to-API base URL |
|
||||
| `SESSION_SECRET` | | ✓ | | Yes | Website session signing |
|
||||
| `NODE_ENV` | | ✓ | | No | Runtime mode |
|
||||
| `CLEAR_DATABASE` | ✓ | | ✓ | No | Dev/test reset flag |
|
||||
| `ASPNETCORE_ENVIRONMENT` | ✓ | | ✓ | Yes | ASP.NET environment |
|
||||
| `ASPNETCORE_URLS` | ✓ | | ✓ | Yes | API binding address |
|
||||
| `SA_PASSWORD` | | | ✓ | Yes | SQL Server container |
|
||||
| `ACCEPT_EULA` | | | ✓ | Yes | SQL Server EULA |
|
||||
| `MSSQL_PID` | | | ✓ | No | SQL Server edition |
|
||||
| `DOTNET_RUNNING_IN_CONTAINER` | ✓ | | ✓ | No | Container flag |
|
||||
|
||||
\* Either `DB_CONNECTION_STRING` OR the component variables (`DB_SERVER`,
|
||||
`DB_NAME`, `DB_USER`, `DB_PASSWORD`) must be provided.
|
||||
|
||||
## Validation
|
||||
|
||||
### Backend Validation
|
||||
|
||||
Variables are validated at startup:
|
||||
|
||||
- Missing required variables cause application to fail
|
||||
- JWT_SECRET length is enforced (min 32 chars)
|
||||
- Connection string format is validated
|
||||
|
||||
### Frontend Validation
|
||||
|
||||
The active website relies on runtime defaults for local development and the
|
||||
surrounding server environment in deployed environments.
|
||||
|
||||
- `API_BASE_URL` defaults to `http://localhost:8080`
|
||||
- `SESSION_SECRET` falls back to a development-only local secret
|
||||
- `NODE_ENV` controls secure cookie behavior
|
||||
|
||||
## Example Configuration Files
|
||||
|
||||
### `.env.dev` (Backend/Docker)
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DB_SERVER=sqlserver,1433
|
||||
DB_NAME=Biergarten
|
||||
DB_USER=sa
|
||||
DB_PASSWORD=Dev_Password_123!
|
||||
|
||||
# JWT Authentication Secrets
|
||||
ACCESS_TOKEN_SECRET=<generated-with-openssl>
|
||||
REFRESH_TOKEN_SECRET=<generated-with-openssl>
|
||||
CONFIRMATION_TOKEN_SECRET=<generated-with-openssl>
|
||||
WEBSITE_BASE_URL=http://localhost:3000
|
||||
|
||||
# Migration
|
||||
CLEAR_DATABASE=true
|
||||
|
||||
# ASP.NET Core
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
ASPNETCORE_URLS=http://0.0.0.0:8080
|
||||
|
||||
# SQL Server Container
|
||||
SA_PASSWORD=Dev_Password_123!
|
||||
ACCEPT_EULA=Y
|
||||
MSSQL_PID=Express
|
||||
```
|
||||
|
||||
### Frontend local runtime example
|
||||
|
||||
```bash
|
||||
API_BASE_URL=http://localhost:8080
|
||||
SESSION_SECRET=<generated-with-openssl>
|
||||
NODE_ENV=development
|
||||
```
|
||||
139
docs/website/getting-started.md
Normal file
139
docs/website/getting-started.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Getting Started
|
||||
|
||||
This guide covers local setup for the current Biergarten stack: the .NET backend
|
||||
in `src/Core` and the active React Router frontend in `src/Website`.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **.NET SDK 10+**
|
||||
- **Node.js 18+**
|
||||
- **Docker Desktop** or equivalent Docker Engine setup
|
||||
- **Java 8+** if you want to regenerate PlantUML diagrams
|
||||
|
||||
## Recommended Path: Docker for Backend, Node for Frontend
|
||||
|
||||
### 1. Clone the Repository
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd the-biergarten-app
|
||||
```
|
||||
|
||||
### 2. Configure Backend Environment Variables
|
||||
|
||||
```bash
|
||||
cp .env.example .env.dev
|
||||
```
|
||||
|
||||
At minimum, ensure `.env.dev` includes valid database and token values:
|
||||
|
||||
```bash
|
||||
DB_SERVER=sqlserver,1433
|
||||
DB_NAME=Biergarten
|
||||
DB_USER=sa
|
||||
DB_PASSWORD=YourStrong!Passw0rd
|
||||
ACCESS_TOKEN_SECRET=<generated>
|
||||
REFRESH_TOKEN_SECRET=<generated>
|
||||
CONFIRMATION_TOKEN_SECRET=<generated>
|
||||
WEBSITE_BASE_URL=http://localhost:3000
|
||||
```
|
||||
|
||||
See [Environment Variables](environment-variables.md) for the full list.
|
||||
|
||||
### 3. Start the Backend Stack
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yaml up -d
|
||||
```
|
||||
|
||||
This starts SQL Server, migrations, seeding, and the API.
|
||||
|
||||
Available endpoints:
|
||||
|
||||
- API Swagger: http://localhost:8080/swagger
|
||||
- Health Check: http://localhost:8080/health
|
||||
|
||||
### 4. Start the Active Frontend
|
||||
|
||||
```bash
|
||||
cd src/Website
|
||||
npm install
|
||||
API_BASE_URL=http://localhost:8080 SESSION_SECRET=dev-secret-change-me npm run dev
|
||||
```
|
||||
|
||||
The website will be available at the local address printed by React Router dev.
|
||||
|
||||
Required frontend runtime variables for local work:
|
||||
|
||||
- `API_BASE_URL` - Base URL for the .NET API
|
||||
- `SESSION_SECRET` - Cookie session signing secret for the website server
|
||||
|
||||
### 5. Optional: Run Storybook
|
||||
|
||||
```bash
|
||||
cd src/Website
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
Storybook runs at http://localhost:6006 by default.
|
||||
|
||||
## Useful Commands
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.dev.yaml logs -f
|
||||
docker compose -f docker-compose.dev.yaml down
|
||||
docker compose -f docker-compose.dev.yaml down -v
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd src/Website
|
||||
npm run lint
|
||||
npm run typecheck
|
||||
npm run format:check
|
||||
npm run test:storybook
|
||||
npm run test:storybook:playwright
|
||||
```
|
||||
|
||||
## Manual Backend Setup
|
||||
|
||||
If you do not want to use Docker, you can run the backend locally.
|
||||
|
||||
### 1. Set Environment Variables
|
||||
|
||||
```bash
|
||||
export DB_CONNECTION_STRING="Server=localhost,1433;Database=Biergarten;User Id=sa;Password=YourStrong!Passw0rd;TrustServerCertificate=True;"
|
||||
export ACCESS_TOKEN_SECRET="<generated>"
|
||||
export REFRESH_TOKEN_SECRET="<generated>"
|
||||
export CONFIRMATION_TOKEN_SECRET="<generated>"
|
||||
export WEBSITE_BASE_URL="http://localhost:3000"
|
||||
```
|
||||
|
||||
### 2. Run Migrations and Seed
|
||||
|
||||
```bash
|
||||
cd src/Core
|
||||
dotnet run --project Database/Database.Migrations/Database.Migrations.csproj
|
||||
dotnet run --project Database/Database.Seed/Database.Seed.csproj
|
||||
```
|
||||
|
||||
### 3. Start the API
|
||||
|
||||
```bash
|
||||
dotnet run --project API/API.Core/API.Core.csproj
|
||||
```
|
||||
|
||||
## Legacy Frontend Note
|
||||
|
||||
The previous Next.js frontend now lives in `src/Website-v1` and is not the
|
||||
active website. Legacy setup details have been moved to
|
||||
[docs/archive/legacy-website-v1.md](archive/legacy-website-v1.md).
|
||||
|
||||
## Next Steps
|
||||
|
||||
- Review [Architecture](architecture.md)
|
||||
- Run backend and frontend checks from [Testing](testing.md)
|
||||
- Use [Docker Guide](docker.md) for container troubleshooting
|
||||
347
docs/website/testing.md
Normal file
347
docs/website/testing.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# Testing
|
||||
|
||||
This document describes the testing strategy and how to run tests for The
|
||||
Biergarten App.
|
||||
|
||||
## Overview
|
||||
|
||||
The project uses a multi-layered testing approach across backend and frontend:
|
||||
|
||||
- **API.Specs** - BDD integration tests using Reqnroll (Gherkin)
|
||||
- **Infrastructure.Repository.Tests** - Unit tests for data access layer
|
||||
- **Service.Auth.Tests** - Unit tests for authentication business logic
|
||||
- **Storybook Vitest project** - Browser-based interaction tests for shared
|
||||
website stories
|
||||
- **Storybook Playwright suite** - Browser checks against Storybook-rendered
|
||||
components
|
||||
|
||||
## Running Tests with Docker (Recommended)
|
||||
|
||||
The easiest way to run all tests is using Docker Compose, which sets up an
|
||||
isolated test environment:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
||||
```
|
||||
|
||||
This command:
|
||||
|
||||
1. Starts a fresh SQL Server instance
|
||||
2. Runs database migrations
|
||||
3. Seeds test data
|
||||
4. Executes all test suites in parallel
|
||||
5. Exports results to `./test-results/`
|
||||
6. Exits when tests complete
|
||||
|
||||
### View Test Results
|
||||
|
||||
```bash
|
||||
# List test result files
|
||||
ls -la test-results/
|
||||
|
||||
# View specific test results
|
||||
cat test-results/api-specs/results.trx
|
||||
cat test-results/repository-tests/results.trx
|
||||
cat test-results/service-auth-tests/results.trx
|
||||
```
|
||||
|
||||
### Clean Up
|
||||
|
||||
```bash
|
||||
# Remove test containers and volumes
|
||||
docker compose -f docker-compose.test.yaml down -v
|
||||
```
|
||||
|
||||
## Running Tests Locally
|
||||
|
||||
You can run individual test projects locally without Docker:
|
||||
|
||||
### Integration Tests (API.Specs)
|
||||
|
||||
```bash
|
||||
cd src/Core
|
||||
dotnet test API/API.Specs/API.Specs.csproj
|
||||
```
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- SQL Server instance running
|
||||
- Database migrated and seeded
|
||||
- Environment variables set (DB connection, JWT secret)
|
||||
|
||||
### Repository Tests
|
||||
|
||||
```bash
|
||||
cd src/Core
|
||||
dotnet test Infrastructure/Infrastructure.Repository.Tests/Infrastructure.Repository.Tests.csproj
|
||||
```
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- SQL Server instance running (uses mock data)
|
||||
|
||||
### Service Tests
|
||||
|
||||
```bash
|
||||
cd src/Core
|
||||
dotnet test Service/Service.Auth.Tests/Service.Auth.Tests.csproj
|
||||
```
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- No database required (uses Moq for mocking)
|
||||
|
||||
### Frontend Storybook Tests
|
||||
|
||||
```bash
|
||||
cd src/Website
|
||||
npm install
|
||||
npm run test:storybook
|
||||
```
|
||||
|
||||
**Purpose**:
|
||||
|
||||
- Verifies shared stories such as form fields, submit buttons, navbar states,
|
||||
toasts, and the theme gallery
|
||||
- Runs in browser mode via Vitest and Storybook integration
|
||||
|
||||
### Frontend Playwright Storybook Tests
|
||||
|
||||
```bash
|
||||
cd src/Website
|
||||
npm install
|
||||
npm run test:storybook:playwright
|
||||
```
|
||||
|
||||
**Requirements**:
|
||||
|
||||
- Storybook dependencies installed
|
||||
- Playwright browser dependencies installed
|
||||
- The command will start or reuse the Storybook server defined in
|
||||
`playwright.storybook.config.ts`
|
||||
|
||||
## Test Coverage
|
||||
|
||||
### Current Coverage
|
||||
|
||||
**Authentication & User Management**:
|
||||
|
||||
- User registration with validation
|
||||
- User login with JWT token generation
|
||||
- Password hashing and verification (Argon2id)
|
||||
- JWT token generation and claims
|
||||
- Invalid credentials handling
|
||||
- 404 error responses
|
||||
|
||||
**Repository Layer**:
|
||||
|
||||
- User account creation
|
||||
- User credential management
|
||||
- GetUserByUsername queries
|
||||
- Stored procedure execution
|
||||
|
||||
**Service Layer**:
|
||||
|
||||
- Login service with password verification
|
||||
- Register service with validation
|
||||
- Business logic for authentication flow
|
||||
|
||||
**Frontend UI Coverage**:
|
||||
|
||||
- Shared submit button states
|
||||
- Form field happy path and error presentation
|
||||
- Navbar guest, authenticated, and mobile behavior
|
||||
- Theme gallery rendering across Biergarten themes
|
||||
- Toast interactions and themed notification display
|
||||
|
||||
### Planned Coverage
|
||||
|
||||
- [ ] Email verification workflow
|
||||
- [ ] Password reset functionality
|
||||
- [ ] Token refresh mechanism
|
||||
- [ ] Brewery data management
|
||||
- [ ] Beer post operations
|
||||
- [ ] User follow/unfollow
|
||||
- [ ] Image upload service
|
||||
- [ ] Frontend route integration coverage beyond Storybook stories
|
||||
|
||||
## Testing Frameworks & Tools
|
||||
|
||||
### xUnit
|
||||
|
||||
- Primary unit testing framework
|
||||
- Used for Repository and Service layer tests
|
||||
- Supports parallel test execution
|
||||
|
||||
### Reqnroll (Gherkin/BDD)
|
||||
|
||||
- Behavior-driven development framework
|
||||
- Used for API integration tests
|
||||
- Human-readable test scenarios in `.feature` files
|
||||
|
||||
### FluentAssertions
|
||||
|
||||
- Expressive assertion library
|
||||
- Makes test assertions more readable
|
||||
- Used across all test projects
|
||||
|
||||
### Moq
|
||||
|
||||
- Mocking framework for .NET
|
||||
- Used in Service layer tests
|
||||
- Enables isolated unit testing
|
||||
|
||||
### DbMocker
|
||||
|
||||
- Database mocking for repository tests
|
||||
- Simulates SQL Server responses
|
||||
- No real database required for unit tests
|
||||
|
||||
## Test Structure
|
||||
|
||||
### API.Specs (Integration Tests)
|
||||
|
||||
```
|
||||
API.Specs/
|
||||
├── Features/
|
||||
│ ├── Authentication.feature # Login/register scenarios
|
||||
│ └── UserManagement.feature # User CRUD scenarios
|
||||
├── Steps/
|
||||
│ ├── AuthenticationSteps.cs # Step definitions
|
||||
│ └── UserManagementSteps.cs
|
||||
└── Mocks/
|
||||
└── TestApiFactory.cs # Test server setup
|
||||
```
|
||||
|
||||
**Example Feature**:
|
||||
|
||||
```gherkin
|
||||
Feature: User Authentication
|
||||
As a user
|
||||
I want to register and login
|
||||
So that I can access the platform
|
||||
|
||||
Scenario: Successful user registration
|
||||
Given I have valid registration details
|
||||
When I register a new account
|
||||
Then I should receive a JWT token
|
||||
And my account should be created
|
||||
```
|
||||
|
||||
### Infrastructure.Repository.Tests
|
||||
|
||||
```
|
||||
Infrastructure.Repository.Tests/
|
||||
├── AuthRepositoryTests.cs # Auth repository tests
|
||||
├── UserAccountRepositoryTests.cs # User account tests
|
||||
└── TestFixtures/
|
||||
└── DatabaseFixture.cs # Shared test setup
|
||||
```
|
||||
|
||||
### Service.Auth.Tests
|
||||
|
||||
```
|
||||
Service.Auth.Tests/
|
||||
├── LoginService.test.cs # Login business logic tests
|
||||
└── RegisterService.test.cs # Registration business logic tests
|
||||
```
|
||||
|
||||
## Writing Tests
|
||||
|
||||
### Unit Test Example (xUnit)
|
||||
|
||||
```csharp
|
||||
public class LoginServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task LoginAsync_ValidCredentials_ReturnsToken()
|
||||
{
|
||||
// Arrange
|
||||
var mockRepo = new Mock<IAuthRepository>();
|
||||
var mockJwt = new Mock<IJwtService>();
|
||||
var service = new AuthService(mockRepo.Object, mockJwt.Object);
|
||||
|
||||
// Act
|
||||
var result = await service.LoginAsync("testuser", "password123");
|
||||
|
||||
// Assert
|
||||
result.Should().NotBeNull();
|
||||
result.Token.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Test Example (Reqnroll)
|
||||
|
||||
```gherkin
|
||||
Scenario: User login with valid credentials
|
||||
Given a registered user with username "testuser"
|
||||
When I POST to "/api/auth/login" with valid credentials
|
||||
Then the response status should be 200
|
||||
And the response should contain a JWT token
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Tests run automatically in CI/CD pipelines using the test Docker Compose
|
||||
configuration:
|
||||
|
||||
```bash
|
||||
# CI/CD command
|
||||
docker compose -f docker-compose.test.yaml build
|
||||
docker compose -f docker-compose.test.yaml up --abort-on-container-exit
|
||||
docker compose -f docker-compose.test.yaml down -v
|
||||
```
|
||||
|
||||
Exit codes:
|
||||
|
||||
- `0` - All tests passed
|
||||
- Non-zero - Test failures occurred
|
||||
|
||||
Frontend UI checks should also be included in CI for the active website
|
||||
workspace:
|
||||
|
||||
```bash
|
||||
cd src/Website
|
||||
npm ci
|
||||
npm run test:storybook
|
||||
npm run test:storybook:playwright
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tests Failing Due to Database Connection
|
||||
|
||||
Ensure SQL Server is running and environment variables are set:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.test.yaml ps
|
||||
```
|
||||
|
||||
### Port Conflicts
|
||||
|
||||
If port 1433 is in use, stop other SQL Server instances or modify the port in
|
||||
`docker-compose.test.yaml`.
|
||||
|
||||
### Stale Test Data
|
||||
|
||||
Clean up test database:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.test.yaml down -v
|
||||
```
|
||||
|
||||
### View Container Logs
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.test.yaml logs <service-name>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Isolation**: Each test should be independent and not rely on other tests
|
||||
2. **Cleanup**: Use fixtures and dispose patterns for resource cleanup
|
||||
3. **Mocking**: Mock external dependencies in unit tests
|
||||
4. **Descriptive Names**: Use clear, descriptive test method names
|
||||
5. **Arrange-Act-Assert**: Follow AAA pattern in unit tests
|
||||
6. **Given-When-Then**: Follow GWT pattern in BDD scenarios
|
||||
229
docs/website/token-validation.md
Normal file
229
docs/website/token-validation.md
Normal file
@@ -0,0 +1,229 @@
|
||||
# Token Validation Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The Core project implements comprehensive JWT token validation across three
|
||||
token types:
|
||||
|
||||
- **Access Tokens**: Short-lived (1 hour) tokens for API authentication
|
||||
- **Refresh Tokens**: Long-lived (21 days) tokens for obtaining new access
|
||||
tokens
|
||||
- **Confirmation Tokens**: Short-lived (30 minutes) tokens for email
|
||||
confirmation
|
||||
|
||||
## Components
|
||||
|
||||
### Infrastructure Layer
|
||||
|
||||
#### [ITokenInfrastructure](Infrastructure.Jwt/ITokenInfrastructure.cs)
|
||||
|
||||
Low-level JWT operations.
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `GenerateJwt()` - Creates signed JWT tokens
|
||||
- `ValidateJwtAsync()` - Validates token signature, expiration, and format
|
||||
|
||||
**Implementation:**
|
||||
[JwtInfrastructure.cs](Infrastructure.Jwt/JwtInfrastructure.cs)
|
||||
|
||||
- Uses Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler
|
||||
- Algorithm: HS256 (HMAC-SHA256)
|
||||
- Validates token lifetime, signature, and well-formedness
|
||||
|
||||
### Service Layer
|
||||
|
||||
#### [ITokenValidationService](Service.Auth/ITokenValidationService.cs)
|
||||
|
||||
High-level token validation with context (token type, user extraction).
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `ValidateAccessTokenAsync(string token)` - Validates access tokens
|
||||
- `ValidateRefreshTokenAsync(string token)` - Validates refresh tokens
|
||||
- `ValidateConfirmationTokenAsync(string token)` - Validates confirmation tokens
|
||||
|
||||
**Returns:** `ValidatedToken` record containing:
|
||||
|
||||
- `UserId` (Guid)
|
||||
- `Username` (string)
|
||||
- `Principal` (ClaimsPrincipal) - Full JWT claims
|
||||
|
||||
**Implementation:**
|
||||
[TokenValidationService.cs](Service.Auth/TokenValidationService.cs)
|
||||
|
||||
- Reads token secrets from environment variables
|
||||
- Extracts and validates claims (Sub, UniqueName)
|
||||
- Throws `UnauthorizedException` on validation failure
|
||||
|
||||
#### [ITokenService](Service.Auth/ITokenService.cs)
|
||||
|
||||
Token generation (existing service extended).
|
||||
|
||||
**Methods:**
|
||||
|
||||
- `GenerateAccessToken(UserAccount)` - Creates 1-hour access token
|
||||
- `GenerateRefreshToken(UserAccount)` - Creates 21-day refresh token
|
||||
- `GenerateConfirmationToken(UserAccount)` - Creates 30-minute confirmation
|
||||
token
|
||||
|
||||
### Integration Points
|
||||
|
||||
#### [ConfirmationService](Service.Auth/IConfirmationService.cs)
|
||||
|
||||
**Flow:**
|
||||
|
||||
1. Receives confirmation token from user
|
||||
2. Calls `TokenValidationService.ValidateConfirmationTokenAsync()`
|
||||
3. Extracts user ID from validated token
|
||||
4. Calls `AuthRepository.ConfirmUserAccountAsync()` to update database
|
||||
5. Returns confirmation result
|
||||
|
||||
#### [RefreshTokenService](Service.Auth/RefreshTokenService.cs)
|
||||
|
||||
**Flow:**
|
||||
|
||||
1. Receives refresh token from user
|
||||
2. Calls `TokenValidationService.ValidateRefreshTokenAsync()`
|
||||
3. Retrieves user account via `AuthRepository.GetUserByIdAsync()`
|
||||
4. Issues new access and refresh tokens via `TokenService`
|
||||
5. Returns new token pair
|
||||
|
||||
#### [AuthController](API.Core/Controllers/AuthController.cs)
|
||||
|
||||
**Endpoints:**
|
||||
|
||||
- `POST /api/auth/register` - Register new user
|
||||
- `POST /api/auth/login` - Authenticate user
|
||||
- `POST /api/auth/confirm?token=...` - Confirm email
|
||||
- `POST /api/auth/refresh` - Refresh access token
|
||||
|
||||
## Validation Security
|
||||
|
||||
### Token Secrets
|
||||
|
||||
Three independent secrets enable:
|
||||
|
||||
- **Key rotation** - Rotate each secret type independently
|
||||
- **Isolation** - Compromise of one secret doesn't affect others
|
||||
- **Different expiration** - Different token types can expire at different rates
|
||||
|
||||
**Environment Variables:**
|
||||
|
||||
```bash
|
||||
ACCESS_TOKEN_SECRET=... # Signs 1-hour access tokens
|
||||
REFRESH_TOKEN_SECRET=... # Signs 21-day refresh tokens
|
||||
CONFIRMATION_TOKEN_SECRET=... # Signs 30-minute confirmation tokens
|
||||
```
|
||||
|
||||
### Validation Checks
|
||||
|
||||
Each token is validated for:
|
||||
|
||||
1. **Signature Verification** - Token must be signed with correct secret
|
||||
2. **Expiration** - Token must not be expired (checked against current time)
|
||||
3. **Claims Presence** - Required claims (Sub, UniqueName) must be present
|
||||
4. **Claims Format** - UserId claim must be a valid GUID
|
||||
|
||||
### Error Handling
|
||||
|
||||
Validation failures return HTTP 401 Unauthorized:
|
||||
|
||||
- Invalid signature → "Invalid token"
|
||||
- Expired token → "Invalid token" (message doesn't reveal reason for security)
|
||||
- Missing claims → "Invalid token"
|
||||
- Malformed claims → "Invalid token"
|
||||
|
||||
## Token Lifecycle
|
||||
|
||||
### Access Token Lifecycle
|
||||
|
||||
1. **Generation**: During login (1-hour validity)
|
||||
2. **Usage**: Included in Authorization header on API requests
|
||||
3. **Validation**: Validated on protected endpoints
|
||||
4. **Expiration**: Token becomes invalid after 1 hour
|
||||
5. **Refresh**: Use refresh token to obtain new access token
|
||||
|
||||
### Refresh Token Lifecycle
|
||||
|
||||
1. **Generation**: During login (21-day validity)
|
||||
2. **Storage**: Client-side (secure storage)
|
||||
3. **Usage**: Posted to `/api/auth/refresh` endpoint
|
||||
4. **Validation**: Validated by RefreshTokenService
|
||||
5. **Rotation**: New refresh token issued on successful refresh
|
||||
6. **Expiration**: Token becomes invalid after 21 days
|
||||
|
||||
### Confirmation Token Lifecycle
|
||||
|
||||
1. **Generation**: During user registration (30-minute validity)
|
||||
2. **Delivery**: Emailed to user in confirmation link
|
||||
3. **Usage**: User clicks link, token posted to `/api/auth/confirm`
|
||||
4. **Validation**: Validated by ConfirmationService
|
||||
5. **Completion**: User account marked as confirmed
|
||||
6. **Expiration**: Token becomes invalid after 30 minutes
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
**TokenValidationService.test.cs**
|
||||
|
||||
- Happy path: Valid token extraction
|
||||
- Error cases: Invalid, expired, malformed tokens
|
||||
- Missing/invalid claims scenarios
|
||||
|
||||
**RefreshTokenService.test.cs**
|
||||
|
||||
- Successful refresh with valid token
|
||||
- Invalid/expired refresh token rejection
|
||||
- Non-existent user handling
|
||||
|
||||
**ConfirmationService.test.cs**
|
||||
|
||||
- Successful confirmation with valid token
|
||||
- Token validation failures
|
||||
- User not found scenarios
|
||||
|
||||
### BDD Tests (Reqnroll)
|
||||
|
||||
**TokenRefresh.feature**
|
||||
|
||||
- Successful token refresh
|
||||
- Invalid/expired token rejection
|
||||
- Missing token validation
|
||||
|
||||
**Confirmation.feature**
|
||||
|
||||
- Successful email confirmation
|
||||
- Expired/tampered token rejection
|
||||
- Missing token validation
|
||||
|
||||
**AccessTokenValidation.feature**
|
||||
|
||||
- Protected endpoint access token validation
|
||||
- Invalid/expired access token rejection
|
||||
- Token type mismatch (refresh used as access token)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Stretch Goals
|
||||
|
||||
1. **Middleware for Access Token Validation**
|
||||
- Automatically validate access tokens on protected routes
|
||||
- Populate HttpContext.User from token claims
|
||||
- Return 401 for invalid/missing tokens
|
||||
|
||||
2. **Token Blacklisting**
|
||||
- Implement token revocation (e.g., on logout)
|
||||
- Store blacklisted tokens in cache/database
|
||||
- Check blacklist during validation
|
||||
|
||||
3. **Refresh Token Rotation Strategy**
|
||||
- Detect token reuse (replay attacks)
|
||||
- Automatically invalidate entire token chain on reuse
|
||||
- Log suspicious activity
|
||||
|
||||
4. **Structured Logging**
|
||||
- Log token validation attempts
|
||||
- Track failed validation reasons
|
||||
- Alert on repeated validation failures (brute force detection)
|
||||
Reference in New Issue
Block a user