Update documentation (#156)

This commit is contained in:
Aaron Po
2026-02-21 05:02:22 -05:00
committed by GitHub
parent c5683df4b6
commit 50c2f5dfda
11 changed files with 3135 additions and 865 deletions

View 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

View 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

View File

@@ -0,0 +1,523 @@
@startuml class-diagram
!theme plain
skinparam backgroundColor #FFFFFF
skinparam defaultFontName Arial
skinparam classAttributeIconSize 0
skinparam linetype ortho
title Biergarten Application - Class Diagram
' API Layer
package "API.Core" <<Rectangle>> #E3F2FD {
class AuthController {
- IRegisterService _registerService
- ILoginService _loginService
+ <<async>> Task<ActionResult> Register(RegisterRequest)
+ <<async>> Task<ActionResult> Login(LoginRequest)
}
class UserController {
- IUserService _userService
+ <<async>> Task<ActionResult<IEnumerable<UserAccount>>> GetAll(int?, int?)
+ <<async>> Task<ActionResult<UserAccount>> GetById(Guid)
}
class GlobalExceptionFilter {
- ILogger<GlobalExceptionFilter> _logger
+ void OnException(ExceptionContext)
}
package "Contracts" {
class RegisterRequest <<record>> {
+ string Username
+ string FirstName
+ string LastName
+ string Email
+ DateTime DateOfBirth
+ string Password
}
class LoginRequest <<record>> {
+ string Username
+ string Password
}
class RegisterRequestValidator {
+ RegisterRequestValidator()
}
class LoginRequestValidator {
+ LoginRequestValidator()
}
class "ResponseBody<T>" <<record>> {
+ string Message
+ T Payload
}
class LoginPayload <<record>> {
+ Guid UserAccountId
+ string Username
+ string RefreshToken
+ string AccessToken
}
class RegistrationPayload <<record>> {
+ Guid UserAccountId
+ string Username
+ string RefreshToken
+ string AccessToken
+ bool ConfirmationEmailSent
}
}
}
' Service Layer
package "Service Layer" <<Rectangle>> #C8E6C9 {
package "Service.Auth" {
interface IRegisterService {
+ <<async>> Task<RegisterServiceReturn> RegisterAsync(UserAccount, string)
}
class RegisterService {
- IAuthRepository _authRepo
- IPasswordInfrastructure _passwordInfra
- ITokenService _tokenService
- IEmailService _emailService
+ <<async>> Task<RegisterServiceReturn> RegisterAsync(UserAccount, string)
- <<async>> Task ValidateUserDoesNotExist(UserAccount)
}
interface ILoginService {
+ <<async>> Task<LoginServiceReturn> LoginAsync(string, string)
}
class LoginService {
- IAuthRepository _authRepo
- IPasswordInfrastructure _passwordInfra
- ITokenService _tokenService
+ <<async>> Task<LoginServiceReturn> LoginAsync(string, string)
}
interface ITokenService {
+ string GenerateAccessToken(UserAccount)
+ string GenerateRefreshToken(UserAccount)
}
class TokenService {
- ITokenInfrastructure _tokenInfrastructure
+ string GenerateAccessToken(UserAccount)
+ string GenerateRefreshToken(UserAccount)
}
class RegisterServiceReturn <<record>> {
+ bool IsAuthenticated
+ bool EmailSent
+ UserAccount UserAccount
+ string AccessToken
+ string RefreshToken
}
class LoginServiceReturn <<record>> {
+ UserAccount UserAccount
+ string RefreshToken
+ string AccessToken
}
}
package "Service.UserManagement" {
interface IUserService {
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
+ <<async>> Task<UserAccount> GetByIdAsync(Guid)
+ <<async>> Task UpdateAsync(UserAccount)
}
class UserService {
- IUserAccountRepository _repository
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
+ <<async>> Task<UserAccount> GetByIdAsync(Guid)
+ <<async>> Task UpdateAsync(UserAccount)
}
}
package "Service.Emails" {
interface IEmailService {
+ <<async>> Task SendRegistrationEmailAsync(UserAccount, string)
}
class EmailService {
- IEmailProvider _emailProvider
- IEmailTemplateProvider _templateProvider
+ <<async>> Task SendRegistrationEmailAsync(UserAccount, string)
}
}
}
' Domain Layer
package "Domain" <<Rectangle>> #FFF9C4 {
package "Domain.Entities" {
class UserAccount {
+ Guid UserAccountId
+ string Username
+ string FirstName
+ string LastName
+ string Email
+ DateTime CreatedAt
+ DateTime? UpdatedAt
+ DateTime DateOfBirth
+ byte[]? Timer
}
class UserCredential {
+ Guid UserCredentialId
+ Guid UserAccountId
+ DateTime CreatedAt
+ DateTime Expiry
+ string Hash
+ byte[]? Timer
}
class UserVerification {
+ Guid UserVerificationId
+ Guid UserAccountId
+ DateTime VerificationDateTime
+ byte[]? Timer
}
}
package "Domain.Exceptions" {
class ConflictException {
+ ConflictException(string)
}
class NotFoundException {
+ NotFoundException(string)
}
class UnauthorizedException {
+ UnauthorizedException(string)
}
class ForbiddenException {
+ ForbiddenException(string)
}
class ValidationException {
+ ValidationException(string)
}
}
}
' Infrastructure Layer
package "Infrastructure" <<Rectangle>> #E1BEE7 {
package "Infrastructure.Repository" {
interface IAuthRepository {
+ <<async>> Task<UserAccount> RegisterUserAsync(string, string, string, string, DateTime, string)
+ <<async>> Task<UserAccount?> GetUserByEmailAsync(string)
+ <<async>> Task<UserAccount?> GetUserByUsernameAsync(string)
+ <<async>> Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid)
+ <<async>> Task RotateCredentialAsync(Guid, string)
}
class AuthRepository {
- ISqlConnectionFactory _connectionFactory
+ <<async>> Task<UserAccount> RegisterUserAsync(...)
+ <<async>> Task<UserAccount?> GetUserByEmailAsync(string)
+ <<async>> Task<UserAccount?> GetUserByUsernameAsync(string)
+ <<async>> Task<UserCredential?> GetActiveCredentialByUserAccountIdAsync(Guid)
+ <<async>> Task RotateCredentialAsync(Guid, string)
# UserAccount MapToEntity(DbDataReader)
- UserCredential MapToCredentialEntity(DbDataReader)
}
interface IUserAccountRepository {
+ <<async>> Task<UserAccount?> GetByIdAsync(Guid)
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
+ <<async>> Task UpdateAsync(UserAccount)
+ <<async>> Task DeleteAsync(Guid)
+ <<async>> Task<UserAccount?> GetByUsernameAsync(string)
+ <<async>> Task<UserAccount?> GetByEmailAsync(string)
}
class UserAccountRepository {
- ISqlConnectionFactory _connectionFactory
+ <<async>> Task<UserAccount?> GetByIdAsync(Guid)
+ <<async>> Task<IEnumerable<UserAccount>> GetAllAsync(int?, int?)
+ <<async>> Task UpdateAsync(UserAccount)
+ <<async>> Task DeleteAsync(Guid)
+ <<async>> Task<UserAccount?> GetByUsernameAsync(string)
+ <<async>> Task<UserAccount?> GetByEmailAsync(string)
# UserAccount MapToEntity(DbDataReader)
}
abstract class "Repository<T>" {
# ISqlConnectionFactory _connectionFactory
# <<async>> Task<DbConnection> CreateConnection()
# {abstract} T MapToEntity(DbDataReader)
}
package "Sql" {
interface ISqlConnectionFactory {
+ DbConnection CreateConnection()
}
class DefaultSqlConnectionFactory {
- string _connectionString
+ DbConnection CreateConnection()
}
class SqlConnectionStringHelper <<static>> {
+ {static} string BuildConnectionString(string?)
+ {static} string BuildMasterConnectionString()
}
}
}
package "Infrastructure.PasswordHashing" {
interface IPasswordInfrastructure {
+ string Hash(string)
+ bool Verify(string, string)
}
class Argon2Infrastructure {
- {static} int SaltSize = 16
- {static} int HashSize = 32
- {static} int ArgonIterations = 4
- {static} int ArgonMemoryKb = 65536
+ string Hash(string)
+ bool Verify(string, string)
}
}
package "Infrastructure.Jwt" {
interface ITokenInfrastructure {
+ string GenerateJwt(Guid, string, DateTime)
}
class JwtInfrastructure {
- string? _secret
+ string GenerateJwt(Guid, string, DateTime)
}
}
package "Infrastructure.Email" {
interface IEmailProvider {
+ <<async>> Task SendAsync(string, string, string, bool)
+ <<async>> Task SendAsync(IEnumerable<string>, string, string, bool)
}
class SmtpEmailProvider {
- string _host
- int _port
- string? _username
- string? _password
- bool _useSsl
- string _fromEmail
- string _fromName
+ <<async>> Task SendAsync(string, string, string, bool)
+ <<async>> Task SendAsync(IEnumerable<string>, string, string, bool)
}
}
package "Infrastructure.Email.Templates" {
interface IEmailTemplateProvider {
+ <<async>> Task<string> RenderUserRegisteredEmailAsync(string, string)
}
class EmailTemplateProvider {
- IServiceProvider _serviceProvider
- ILoggerFactory _loggerFactory
+ <<async>> Task<string> RenderUserRegisteredEmailAsync(string, string)
- <<async>> Task<string> RenderComponentAsync<TComponent>(Dictionary<string, object?>)
}
class "UserRegistration <<Razor>>" {
+ string Username
+ string ConfirmationLink
}
class "Header <<Razor>>" {
}
class "Footer <<Razor>>" {
+ string? FooterText
}
}
}
' Database Layer
package "Database" <<Rectangle>> #FFCCBC {
class "SQL Server" <<Database>> {
.. Tables ..
UserAccount
UserCredential
UserVerification
UserAvatar
Photo
UserFollow
Country
StateProvince
City
BreweryPost
BreweryPostLocation
BreweryPostPhoto
BeerStyle
BeerPost
BeerPostPhoto
BeerPostComment
.. Stored Procedures ..
USP_RegisterUser
usp_GetUserAccountByUsername
usp_GetUserAccountByEmail
usp_GetUserAccountById
USP_GetActiveUserCredentialByUserAccountId
USP_RotateUserCredential
USP_CreateUserVerification
}
}
' Relationships - API to Service
AuthController ..> IRegisterService : uses
AuthController ..> ILoginService : uses
UserController ..> IUserService : uses
AuthController ..> RegisterRequest : receives
AuthController ..> LoginRequest : receives
AuthController ..> "ResponseBody<T>" : returns
AuthController ..> RegistrationPayload : returns
AuthController ..> LoginPayload : returns
RegisterRequest ..> RegisterRequestValidator : validated by
LoginRequest ..> LoginRequestValidator : validated by
' Relationships - Service Layer
IRegisterService <|.. RegisterService : implements
ILoginService <|.. LoginService : implements
ITokenService <|.. TokenService : implements
IUserService <|.. UserService : implements
IEmailService <|.. EmailService : implements
RegisterService ..> IAuthRepository : uses
RegisterService ..> IPasswordInfrastructure : uses
RegisterService ..> ITokenService : uses
RegisterService ..> IEmailService : uses
RegisterService ..> RegisterServiceReturn : returns
RegisterService ..> UserAccount : uses
LoginService ..> IAuthRepository : uses
LoginService ..> IPasswordInfrastructure : uses
LoginService ..> ITokenService : uses
LoginService ..> LoginServiceReturn : returns
LoginService ..> UserAccount : uses
LoginService ..> UserCredential : uses
TokenService ..> ITokenInfrastructure : uses
TokenService ..> UserAccount : uses
UserService ..> IUserAccountRepository : uses
UserService ..> UserAccount : uses
EmailService ..> IEmailProvider : uses
EmailService ..> IEmailTemplateProvider : uses
EmailService ..> UserAccount : uses
' Relationships - Repository Layer
IAuthRepository <|.. AuthRepository : implements
IUserAccountRepository <|.. UserAccountRepository : implements
"Repository<T>" <|-- AuthRepository : extends
"Repository<T>" <|-- UserAccountRepository : extends
AuthRepository ..> ISqlConnectionFactory : uses
AuthRepository ..> UserAccount : returns
AuthRepository ..> UserCredential : returns
AuthRepository ..> "SQL Server" : queries
UserAccountRepository ..> ISqlConnectionFactory : uses
UserAccountRepository ..> UserAccount : returns
UserAccountRepository ..> "SQL Server" : queries
"Repository<T>" ..> ISqlConnectionFactory : uses
ISqlConnectionFactory <|.. DefaultSqlConnectionFactory : implements
DefaultSqlConnectionFactory ..> SqlConnectionStringHelper : uses
' Relationships - Infrastructure
IPasswordInfrastructure <|.. Argon2Infrastructure : implements
ITokenInfrastructure <|.. JwtInfrastructure : implements
IEmailProvider <|.. SmtpEmailProvider : implements
IEmailTemplateProvider <|.. EmailTemplateProvider : implements
EmailTemplateProvider ..> "UserRegistration <<Razor>>" : renders
"UserRegistration <<Razor>>" ..> "Header <<Razor>>" : includes
"UserRegistration <<Razor>>" ..> "Footer <<Razor>>" : includes
' Relationships - Domain
UserAccount -- UserCredential : "1" -- "*"
UserAccount -- UserVerification : "1" -- "0..1"
' Exception handling
GlobalExceptionFilter ..> ConflictException : catches
GlobalExceptionFilter ..> NotFoundException : catches
GlobalExceptionFilter ..> UnauthorizedException : catches
GlobalExceptionFilter ..> ForbiddenException : catches
GlobalExceptionFilter ..> ValidationException : catches
RegisterService ..> ConflictException : throws
LoginService ..> UnauthorizedException : throws
UserService ..> NotFoundException : throws
' Notes
note right of Argon2Infrastructure
Security Parameters:
- Memory: 64MB
- Iterations: 4
- Salt: 16 bytes
- Output: 32 bytes
end note
note right of JwtInfrastructure
JWT Configuration:
- Algorithm: HS256
- Access: 1 hour
- Refresh: 21 days
end note
note right of "SQL Server"
Stored Procedures:
- USP_RegisterUser: Transaction
creates UserAccount +
UserCredential
- Credentials tracked with
IsRevoked flag
end note
note right of AuthRepository
Uses ADO.NET with
parameterized queries
to prevent SQL injection
end note
note bottom of RegisterService
Registration Flow:
1. Validate user doesn't exist
2. Hash password (Argon2)
3. Create account + credential
4. Generate tokens
5. Send confirmation email
end note
note bottom of LoginService
Login Flow:
1. Find user by username
2. Get active credential
3. Verify password
4. Generate tokens
5. Return authenticated user
end note
@enduml

View 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

View 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