Initial commit
This commit is contained in:
282
skills/agents/references/examples/ldap-selfservice/AGENTS.md
Normal file
282
skills/agents/references/examples/ldap-selfservice/AGENTS.md
Normal file
@@ -0,0 +1,282 @@
|
||||
# Agent Guidelines
|
||||
|
||||
<!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-10-09 -->
|
||||
|
||||
**Precedence**: Nearest AGENTS.md wins. This is the root file with global defaults.
|
||||
|
||||
**Project**: LDAP Selfservice Password Changer — hybrid Go + TypeScript web application with WCAG 2.2 AAA accessibility compliance.
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
- [internal/AGENTS.md](internal/AGENTS.md) - Go backend services
|
||||
- [internal/web/AGENTS.md](internal/web/AGENTS.md) - TypeScript frontend & Tailwind CSS
|
||||
|
||||
## Global Defaults
|
||||
|
||||
### Project Overview
|
||||
|
||||
Self-service password change/reset web app for LDAP/ActiveDirectory with email-based password reset, rate limiting, and strict accessibility standards. Single binary deployment with embedded assets.
|
||||
|
||||
**Stack**: Go 1.25 + Fiber, TypeScript (ultra-strict), Tailwind CSS 4, Docker multi-stage builds, pnpm 10.18
|
||||
|
||||
**Key characteristics**:
|
||||
|
||||
- Docker-first: All dev/CI must work via Docker
|
||||
- Accessibility: WCAG 2.2 AAA compliance (7:1 contrast, keyboard nav, screen readers)
|
||||
- Type-safe: Go with testcontainers, TypeScript with all strict flags
|
||||
- Security-focused: LDAPS, rate limiting, token-based reset, no password storage
|
||||
|
||||
### Setup
|
||||
|
||||
**Prerequisites**: Docker + Docker Compose (required), Go 1.25+, Node.js 24+, pnpm 10.18+ (for native dev)
|
||||
|
||||
```bash
|
||||
# Clone and setup environment
|
||||
git clone <repo>
|
||||
cd ldap-selfservice-password-changer
|
||||
cp .env.local.example .env.local # Edit with your LDAP config
|
||||
|
||||
# Docker (recommended)
|
||||
docker compose --profile dev up
|
||||
|
||||
# Native development
|
||||
pnpm install
|
||||
go mod download
|
||||
pnpm dev # Runs concurrent TS watch, CSS watch, and Go with hot-reload
|
||||
```
|
||||
|
||||
See [docs/development-guide.md](docs/development-guide.md) for comprehensive setup.
|
||||
|
||||
### Build & Test Commands
|
||||
|
||||
**Package manager**: pnpm (specified in package.json: `pnpm@10.18.1`)
|
||||
|
||||
```bash
|
||||
# Build everything
|
||||
pnpm build # Build frontend assets + Go binary
|
||||
|
||||
# Frontend only
|
||||
pnpm build:assets # TypeScript + Tailwind CSS
|
||||
pnpm js:build # TypeScript compile + minify
|
||||
pnpm css:build # Tailwind CSS + PostCSS
|
||||
|
||||
# Development (watch mode)
|
||||
pnpm dev # Concurrent: TS watch, CSS watch, Go hot-reload
|
||||
pnpm js:dev # TypeScript watch
|
||||
pnpm css:dev # CSS watch
|
||||
pnpm go:dev # Go with nodemon hot-reload
|
||||
|
||||
# Tests
|
||||
go test -v ./... # All Go tests with verbose output
|
||||
go test ./internal/... # Specific package tests
|
||||
|
||||
# Formatting
|
||||
pnpm prettier --write . # Format TS, Go templates, config files
|
||||
pnpm prettier --check . # Check formatting (CI)
|
||||
|
||||
# Type checking
|
||||
pnpm js:build # TypeScript strict compilation (no emit in dev)
|
||||
go build -v ./... # Go compilation + type checking
|
||||
```
|
||||
|
||||
**CI commands** (from `.github/workflows/check.yml`):
|
||||
|
||||
- Type check: `pnpm js:build` and `go build -v ./...`
|
||||
- Format check: `pnpm prettier --check .`
|
||||
- Tests: `go test -v ./...`
|
||||
|
||||
### Code Style
|
||||
|
||||
**TypeScript**:
|
||||
|
||||
- Ultra-strict tsconfig: `strict: true`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `noPropertyAccessFromIndexSignature`
|
||||
- Prettier formatting: 120 char width, semicolons, double quotes, 2-space tabs
|
||||
- No `any` types - use proper type definitions
|
||||
- Follow existing patterns in `internal/web/static/`
|
||||
|
||||
**Go**:
|
||||
|
||||
- Standard Go formatting (`go fmt`)
|
||||
- Prettier with `prettier-plugin-go-template` for HTML templates
|
||||
- Follow Go project layout: `internal/` for private packages, `main.go` at root
|
||||
- Use testcontainers for integration tests (see `*_test.go` files)
|
||||
- Error wrapping with context
|
||||
|
||||
**General**:
|
||||
|
||||
- Composition over inheritance
|
||||
- SOLID, KISS, DRY, YAGNI principles
|
||||
- Law of Demeter: minimize coupling
|
||||
- No secrets in VCS (use .env.local, excluded from git)
|
||||
|
||||
### Security
|
||||
|
||||
- **No secrets in git**: Use `.env.local` (gitignored), never commit LDAP credentials
|
||||
- **LDAPS required**: Production must use encrypted LDAP connections
|
||||
- **Rate limiting**: 3 requests/hour per IP (configurable via `RATE_LIMIT_*` env vars)
|
||||
- **Token security**: Cryptographic random tokens with configurable expiry
|
||||
- **Input validation**: Strict validation on all user inputs (see `internal/validators/`)
|
||||
- **Dependency scanning**: Renovate enabled, review changelogs for major updates
|
||||
- **No PII logging**: Redact sensitive data in logs
|
||||
- **Run as non-root**: Dockerfile uses UID 65534 (nobody)
|
||||
|
||||
### PR/Commit Checklist
|
||||
|
||||
✅ **Before commit**:
|
||||
|
||||
- [ ] Run `pnpm prettier --write .` (format all)
|
||||
- [ ] Run `pnpm js:build` (TypeScript strict check)
|
||||
- [ ] Run `go test ./...` (all tests pass)
|
||||
- [ ] Run `go build` (compilation check)
|
||||
- [ ] No secrets in changed files
|
||||
- [ ] Update docs if behavior changed
|
||||
- [ ] WCAG 2.2 AAA compliance maintained (if UI changed)
|
||||
|
||||
✅ **Commit format**: Conventional Commits
|
||||
|
||||
```
|
||||
type(scope): description
|
||||
|
||||
Examples:
|
||||
feat(auth): add password reset via email
|
||||
fix(validators): correct password policy regex
|
||||
docs(api): update JSON-RPC examples
|
||||
chore(deps): update pnpm to v10.18.1
|
||||
```
|
||||
|
||||
**No Claude attribution** in commit messages.
|
||||
|
||||
✅ **PR requirements**:
|
||||
|
||||
- [ ] All CI checks pass (types, formatting, tests)
|
||||
- [ ] Keep PRs small (~≤300 net LOC if possible)
|
||||
- [ ] Include ticket ID if applicable: `fix(rate-limit): ISSUE-123: fix memory leak`
|
||||
- [ ] Update relevant docs in same PR (README, AGENTS.md, docs/)
|
||||
|
||||
### Good vs Bad Examples
|
||||
|
||||
**✅ Good - TypeScript strict types**:
|
||||
|
||||
```typescript
|
||||
interface PasswordPolicy {
|
||||
minLength: number;
|
||||
requireNumbers: boolean;
|
||||
}
|
||||
|
||||
function validatePassword(password: string, policy: PasswordPolicy): boolean {
|
||||
const hasNumber = /\d/.test(password);
|
||||
return password.length >= policy.minLength && (!policy.requireNumbers || hasNumber);
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Bad - Using any or unsafe access**:
|
||||
|
||||
```typescript
|
||||
function validatePassword(password: any, policy: any) {
|
||||
// ❌ any types
|
||||
return password.length >= policy.minLength; // ❌ unsafe access
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Good - Go error handling**:
|
||||
|
||||
```go
|
||||
func connectLDAP(config LDAPConfig) (*ldap.Conn, error) {
|
||||
conn, err := ldap.DialURL(config.URL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to LDAP at %s: %w", config.URL, err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Bad - Ignoring errors**:
|
||||
|
||||
```go
|
||||
func connectLDAP(config LDAPConfig) *ldap.Conn {
|
||||
conn, _ := ldap.DialURL(config.URL) // ❌ ignoring error
|
||||
return conn // ❌ may return nil
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Good - Accessible UI**:
|
||||
|
||||
```html
|
||||
<button type="submit" aria-label="Submit password change" class="bg-blue-600 hover:bg-blue-700 focus:ring-4">
|
||||
Change Password
|
||||
</button>
|
||||
```
|
||||
|
||||
**❌ Bad - Inaccessible UI**:
|
||||
|
||||
```html
|
||||
<div onclick="submit()">Submit</div>
|
||||
❌ not keyboard accessible, wrong semantics
|
||||
```
|
||||
|
||||
### When Stuck
|
||||
|
||||
1. **Check existing docs**: [docs/](docs/) has comprehensive guides
|
||||
2. **Review similar code**: Look for patterns in `internal/` packages
|
||||
3. **Run tests**: `go test -v ./...` often reveals issues
|
||||
4. **Check CI logs**: GitHub Actions shows exact failure points
|
||||
5. **Verify environment**: Ensure `.env.local` is properly configured
|
||||
6. **Docker issues**: `docker compose down -v && docker compose --profile dev up --build`
|
||||
7. **Type errors**: Review `tsconfig.json` strict flags, use proper types
|
||||
8. **Accessibility**: See [docs/accessibility.md](docs/accessibility.md) for WCAG 2.2 AAA guidelines
|
||||
|
||||
### House Rules
|
||||
|
||||
**Docker-First Philosophy**:
|
||||
|
||||
- All dev and CI must work via Docker Compose
|
||||
- Native setup is optional convenience, not requirement
|
||||
- Use profiles in compose.yml: `--profile dev` or `--profile test`
|
||||
|
||||
**Documentation Currency**:
|
||||
|
||||
- Update docs in same PR as code changes
|
||||
- No drift between code and documentation
|
||||
- Keep README, AGENTS.md, and docs/ synchronized
|
||||
|
||||
**Testing Standards**:
|
||||
|
||||
- Aim for ≥80% coverage on changed code
|
||||
- Use testcontainers for integration tests (see existing `*_test.go`)
|
||||
- For bugfixes: write failing test first (TDD)
|
||||
- Tests must pass before PR approval
|
||||
|
||||
**Scope Discipline**:
|
||||
|
||||
- Build only what's requested
|
||||
- No speculative features
|
||||
- MVP first, iterate based on feedback
|
||||
- YAGNI: You Aren't Gonna Need It
|
||||
|
||||
**Accessibility Non-Negotiable**:
|
||||
|
||||
- WCAG 2.2 AAA compliance required
|
||||
- 7:1 contrast ratios for text
|
||||
- Full keyboard navigation support
|
||||
- Screen reader tested (VoiceOver/NVDA)
|
||||
- See [docs/accessibility.md](docs/accessibility.md)
|
||||
|
||||
**Commit Practices**:
|
||||
|
||||
- Atomic commits: one logical change per commit
|
||||
- Conventional Commits format enforced
|
||||
- Never commit secrets or `.env.local`
|
||||
- Keep PRs focused and reviewable
|
||||
|
||||
**Type Safety**:
|
||||
|
||||
- TypeScript: No `any`, all strict flags enabled
|
||||
- Go: Leverage type system, avoid `interface{}`
|
||||
- Validate inputs at boundaries
|
||||
|
||||
**Dependency Management**:
|
||||
|
||||
- Renovate auto-updates enabled
|
||||
- Major version updates require changelog review
|
||||
- Use Context7 MCP or official docs for migrations
|
||||
- Keep pnpm-lock.yaml and go.sum committed
|
||||
@@ -0,0 +1,371 @@
|
||||
# Go Backend Services
|
||||
|
||||
<!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-10-09 -->
|
||||
|
||||
**Scope**: Go backend packages in `internal/` directory
|
||||
|
||||
**See also**: [../AGENTS.md](../AGENTS.md) for global standards, [web/AGENTS.md](web/AGENTS.md) for frontend
|
||||
|
||||
## Overview
|
||||
|
||||
Backend services for LDAP selfservice password change/reset functionality. Organized as internal Go packages:
|
||||
|
||||
- **email/**: SMTP email service for password reset tokens
|
||||
- **options/**: Configuration management from environment variables
|
||||
- **ratelimit/**: IP-based rate limiting (3 req/hour default)
|
||||
- **resettoken/**: Cryptographic token generation and validation
|
||||
- **rpc/**: JSON-RPC 2.0 API handlers (password change/reset)
|
||||
- **validators/**: Password policy validation logic
|
||||
- **web/**: HTTP server setup, static assets, routing (see [web/AGENTS.md](web/AGENTS.md))
|
||||
|
||||
## Setup/Environment
|
||||
|
||||
**Required environment variables** (configure in `.env.local`):
|
||||
|
||||
```bash
|
||||
# LDAP connection
|
||||
LDAP_URL=ldaps://ldap.example.com:636
|
||||
LDAP_USER_BASE_DN=ou=users,dc=example,dc=com
|
||||
LDAP_BIND_DN=cn=admin,dc=example,dc=com
|
||||
LDAP_BIND_PASSWORD=secret
|
||||
|
||||
# Email for password reset
|
||||
SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=noreply@example.com
|
||||
SMTP_PASSWORD=secret
|
||||
SMTP_FROM=noreply@example.com
|
||||
APP_BASE_URL=https://passwd.example.com
|
||||
|
||||
# Rate limiting (optional)
|
||||
RATE_LIMIT_REQUESTS=3
|
||||
RATE_LIMIT_WINDOW=1h
|
||||
|
||||
# Token expiry (optional)
|
||||
TOKEN_EXPIRY_DURATION=1h
|
||||
```
|
||||
|
||||
**Go toolchain**: Requires Go 1.25+ (specified in `go.mod`)
|
||||
|
||||
**Key dependencies**:
|
||||
|
||||
- `github.com/gofiber/fiber/v2` - HTTP server
|
||||
- `github.com/netresearch/simple-ldap-go` - LDAP client
|
||||
- `github.com/testcontainers/testcontainers-go` - Integration testing
|
||||
- `github.com/joho/godotenv` - Environment loading
|
||||
|
||||
## Build & Tests
|
||||
|
||||
```bash
|
||||
# Development
|
||||
go run . # Start server with hot-reload (via pnpm go:dev)
|
||||
go build -v ./... # Compile all packages
|
||||
go test -v ./... # Run all tests with verbose output
|
||||
|
||||
# Specific package testing
|
||||
go test ./internal/validators/... # Test password validators
|
||||
go test ./internal/ratelimit/... # Test rate limiter
|
||||
go test ./internal/resettoken/... # Test token generation
|
||||
go test -run TestSpecificFunction # Run specific test
|
||||
|
||||
# Integration tests (uses testcontainers)
|
||||
go test -v ./internal/email/... # Requires Docker for MailHog container
|
||||
|
||||
# Coverage
|
||||
go test -cover ./... # Coverage summary
|
||||
go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out
|
||||
|
||||
# Build optimized binary
|
||||
CGO_ENABLED=0 go build -ldflags="-w -s" -o ldap-passwd
|
||||
```
|
||||
|
||||
**CI validation** (from `.github/workflows/check.yml`):
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
go build -v ./...
|
||||
go test -v ./...
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
**Go Standards**:
|
||||
|
||||
- Use `go fmt` (automatic via Prettier with go-template plugin)
|
||||
- Follow [Effective Go](https://go.dev/doc/effective_go)
|
||||
- Package-level documentation comments required
|
||||
- Exported functions must have doc comments
|
||||
|
||||
**Project Conventions**:
|
||||
|
||||
- Internal packages only: No public API outside this project
|
||||
- Error wrapping with context: `fmt.Errorf("context: %w", err)`
|
||||
- Use structured logging (consider adding in future)
|
||||
- Prefer explicit over implicit
|
||||
- Use interfaces for testability (see `email/service.go`)
|
||||
|
||||
**Naming**:
|
||||
|
||||
- `internal/package/file.go` - implementation
|
||||
- `internal/package/file_test.go` - tests
|
||||
- Descriptive variable names (not `x`, `y`, `tmp`)
|
||||
- No stuttering: `email.Service`, not `email.EmailService`
|
||||
|
||||
**Error Handling**:
|
||||
|
||||
```go
|
||||
// ✅ Good: wrap with context
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect LDAP at %s: %w", config.URL, err)
|
||||
}
|
||||
|
||||
// ❌ Bad: lose context
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ❌ Worse: ignore
|
||||
conn, _ := ldap.Dial(url)
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
|
||||
- Table-driven tests preferred
|
||||
- Use testcontainers for external dependencies (LDAP, SMTP)
|
||||
- Test files colocated with code: `validators/validate_test.go`
|
||||
- Descriptive test names: `TestPasswordValidation_RequiresMinimumLength`
|
||||
|
||||
## Security
|
||||
|
||||
**LDAP Security**:
|
||||
|
||||
- Always use LDAPS in production (`ldaps://` URLs)
|
||||
- Bind credentials in environment, never hardcoded
|
||||
- Validate user input before LDAP queries (prevent injection)
|
||||
- Use `simple-ldap-go` helpers to avoid raw LDAP filter construction
|
||||
|
||||
**Password Security**:
|
||||
|
||||
- Never log passwords (plain or hashed)
|
||||
- No password storage - passwords go directly to LDAP
|
||||
- Passwords only in memory during request lifetime
|
||||
- HTTPS required for transport security
|
||||
|
||||
**Token Security**:
|
||||
|
||||
- Cryptographic random tokens (see `resettoken/token.go`)
|
||||
- Configurable expiry (default 1h)
|
||||
- Single-use tokens (invalidated after use)
|
||||
- No token storage in logs or metrics
|
||||
|
||||
**Rate Limiting**:
|
||||
|
||||
- IP-based limits: 3 requests/hour default
|
||||
- Configurable via `RATE_LIMIT_*` env vars
|
||||
- In-memory store (consider Redis for multi-instance)
|
||||
- Apply to both change and reset endpoints
|
||||
|
||||
**Input Validation**:
|
||||
|
||||
- Strict validation on all user inputs (see `validators/`)
|
||||
- Reject malformed requests early
|
||||
- Validate email format, username format, password policies
|
||||
- No HTML/script injection vectors
|
||||
|
||||
## PR/Commit Checklist
|
||||
|
||||
**Before committing Go code**:
|
||||
|
||||
- [ ] Run `go fmt ./...` (or `pnpm prettier --write .`)
|
||||
- [ ] Run `go vet ./...` (static analysis)
|
||||
- [ ] Run `go test ./...` (all tests pass)
|
||||
- [ ] Run `go build` (compilation check)
|
||||
- [ ] Update package doc comments if API changed
|
||||
- [ ] Add/update tests for new functionality
|
||||
- [ ] Check for sensitive data in logs
|
||||
- [ ] Verify error messages provide useful context
|
||||
|
||||
**Testing requirements**:
|
||||
|
||||
- New features must have tests
|
||||
- Bug fixes must have regression tests
|
||||
- Aim for ≥80% coverage on changed packages
|
||||
- Integration tests for external dependencies
|
||||
|
||||
**Documentation**:
|
||||
|
||||
- Update package doc comments (godoc)
|
||||
- Update [docs/api-reference.md](../docs/api-reference.md) for RPC changes
|
||||
- Update [docs/development-guide.md](../docs/development-guide.md) for new setup steps
|
||||
- Update environment variable examples in `.env` and docs
|
||||
|
||||
## Good vs Bad Examples
|
||||
|
||||
**✅ Good: Type-safe configuration**
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
LDAPURL string `env:"LDAP_URL" validate:"required,url"`
|
||||
BindDN string `env:"LDAP_BIND_DN" validate:"required"`
|
||||
BindPassword string `env:"LDAP_BIND_PASSWORD" validate:"required"`
|
||||
}
|
||||
|
||||
func LoadConfig() (*Config, error) {
|
||||
var cfg Config
|
||||
if err := env.Parse(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("parse config: %w", err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Bad: Unsafe configuration**
|
||||
|
||||
```go
|
||||
func LoadConfig() *Config {
|
||||
return &Config{
|
||||
LDAPURL: os.Getenv("LDAP_URL"), // ❌ no validation, may be empty
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Good: Table-driven tests**
|
||||
|
||||
```go
|
||||
func TestPasswordValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
password string
|
||||
policy PasswordPolicy
|
||||
wantErr bool
|
||||
}{
|
||||
{"valid password", "Test123!", PasswordPolicy{MinLength: 8}, false},
|
||||
{"too short", "Ab1!", PasswordPolicy{MinLength: 8}, true},
|
||||
{"no numbers", "TestTest", PasswordPolicy{RequireNumbers: true}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := ValidatePassword(tt.password, tt.policy)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("got error %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Bad: Non-descriptive tests**
|
||||
|
||||
```go
|
||||
func TestPassword(t *testing.T) {
|
||||
err := ValidatePassword("test") // ❌ what policy? what's expected?
|
||||
if err == nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Good: Interface for testability**
|
||||
|
||||
```go
|
||||
type EmailService interface {
|
||||
SendResetToken(ctx context.Context, to, token string) error
|
||||
}
|
||||
|
||||
type SMTPService struct {
|
||||
host string
|
||||
port int
|
||||
}
|
||||
|
||||
func (s *SMTPService) SendResetToken(ctx context.Context, to, token string) error {
|
||||
// real implementation
|
||||
}
|
||||
|
||||
// In tests, use mock implementation
|
||||
type MockEmailService struct {
|
||||
SendFunc func(ctx context.Context, to, token string) error
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Bad: Hard-to-test concrete dependency**
|
||||
|
||||
```go
|
||||
func ResetPassword(username string) error {
|
||||
service := NewSMTPService() // ❌ hardcoded, can't mock
|
||||
return service.SendEmail(...)
|
||||
}
|
||||
```
|
||||
|
||||
## When Stuck
|
||||
|
||||
**Go-specific issues**:
|
||||
|
||||
1. **Module issues**: `go mod tidy` to clean dependencies
|
||||
2. **Import errors**: Check `go.mod` requires correct versions
|
||||
3. **Test failures**: `go test -v ./... -run FailingTest` for verbose output
|
||||
4. **LDAP connection**: Verify `LDAP_URL` format and network access
|
||||
5. **Email testing**: Ensure Docker running for testcontainers (MailHog)
|
||||
6. **Rate limit testing**: Tests may fail if system time incorrect
|
||||
|
||||
**Debugging**:
|
||||
|
||||
```bash
|
||||
# Verbose test output
|
||||
go test -v ./internal/package/...
|
||||
|
||||
# Run specific test
|
||||
go test -run TestName ./internal/package/
|
||||
|
||||
# Race detector (for concurrency issues)
|
||||
go test -race ./...
|
||||
|
||||
# Build with debug info
|
||||
go build -gcflags="all=-N -l"
|
||||
```
|
||||
|
||||
**Common pitfalls**:
|
||||
|
||||
- **Nil pointer dereference**: Check error returns before using values
|
||||
- **Context cancellation**: Always respect `context.Context` in long operations
|
||||
- **Resource leaks**: Defer `Close()` calls immediately after acquiring resources
|
||||
- **Goroutine leaks**: Ensure all goroutines can exit
|
||||
- **Time zones**: Use `time.UTC` for consistency
|
||||
|
||||
## Package-Specific Notes
|
||||
|
||||
### email/
|
||||
|
||||
- Uses testcontainers for integration tests
|
||||
- MailHog container spins up automatically in tests
|
||||
- Mock `EmailService` interface for unit tests in other packages
|
||||
|
||||
### options/
|
||||
|
||||
- Configuration loaded from environment via `godotenv`
|
||||
- Validation happens at startup (fail-fast)
|
||||
- See `.env.local.example` for required variables
|
||||
|
||||
### ratelimit/
|
||||
|
||||
- In-memory store (map with mutex)
|
||||
- Consider Redis for multi-instance deployments
|
||||
- Tests use fixed time.Now for deterministic results
|
||||
|
||||
### resettoken/
|
||||
|
||||
- Crypto/rand for token generation (never math/rand)
|
||||
- Base64 URL encoding (safe for URLs)
|
||||
- Store tokens server-side with expiry
|
||||
|
||||
### rpc/
|
||||
|
||||
- JSON-RPC 2.0 specification compliance
|
||||
- Error codes defined in [docs/api-reference.md](../docs/api-reference.md)
|
||||
- Request validation before processing
|
||||
|
||||
### validators/
|
||||
|
||||
- Pure functions (no side effects)
|
||||
- Configurable policies from environment
|
||||
- Clear error messages for user feedback
|
||||
@@ -0,0 +1,448 @@
|
||||
# Frontend - TypeScript & Tailwind CSS
|
||||
|
||||
<!-- Managed by agent: keep sections & order; edit content, not structure. Last updated: 2025-10-09 -->
|
||||
|
||||
**Scope**: Frontend assets in `internal/web/` directory - TypeScript, Tailwind CSS, HTML templates
|
||||
|
||||
**See also**: [../../AGENTS.md](../../AGENTS.md) for global standards, [../AGENTS.md](../AGENTS.md) for Go backend
|
||||
|
||||
## Overview
|
||||
|
||||
Frontend implementation for LDAP selfservice password changer with strict accessibility compliance:
|
||||
|
||||
- **static/**: Client-side TypeScript, compiled CSS, static assets
|
||||
- **js/**: TypeScript source files (compiled to ES modules)
|
||||
- **styles.css**: Tailwind CSS output
|
||||
- Icons, logos, favicons, manifest
|
||||
- **templates/**: Go HTML templates (\*.gohtml)
|
||||
- **handlers.go**: HTTP route handlers
|
||||
- **middleware.go**: Security headers, CORS, etc.
|
||||
- **server.go**: Fiber server setup
|
||||
|
||||
**Key characteristics**:
|
||||
|
||||
- **WCAG 2.2 AAA**: 7:1 contrast, keyboard navigation, screen reader support, adaptive density
|
||||
- **Ultra-strict TypeScript**: All strict flags enabled, no `any` types
|
||||
- **Tailwind CSS 4**: Utility-first, dark mode, responsive, accessible patterns
|
||||
- **Progressive enhancement**: Works without JavaScript (forms submit via HTTP)
|
||||
- **Password manager friendly**: Proper autocomplete attributes
|
||||
|
||||
## Setup/Environment
|
||||
|
||||
**Prerequisites**: Node.js 24+, pnpm 10.18+ (from root `package.json`)
|
||||
|
||||
```bash
|
||||
# From project root
|
||||
pnpm install # Install dependencies
|
||||
|
||||
# Development (watch mode)
|
||||
pnpm css:dev # Tailwind CSS watch
|
||||
pnpm js:dev # TypeScript watch
|
||||
# OR
|
||||
pnpm dev # Concurrent: CSS + TS + Go hot-reload
|
||||
```
|
||||
|
||||
**No .env needed for frontend** - all config comes from Go backend
|
||||
|
||||
**Browser targets**: Modern browsers with ES module support (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)
|
||||
|
||||
## Build & Tests
|
||||
|
||||
```bash
|
||||
# Build frontend assets
|
||||
pnpm build:assets # TypeScript + CSS (production builds)
|
||||
|
||||
# TypeScript
|
||||
pnpm js:build # Compile TS → ES modules + minify
|
||||
pnpm js:dev # Watch mode with preserveWatchOutput
|
||||
tsc --noEmit # Type check only (no output)
|
||||
|
||||
# CSS
|
||||
pnpm css:build # Tailwind + PostCSS → styles.css
|
||||
pnpm css:dev # Watch mode
|
||||
|
||||
# Formatting
|
||||
pnpm prettier --write internal/web/ # Format TS, CSS, HTML templates
|
||||
pnpm prettier --check internal/web/ # Check formatting (CI)
|
||||
```
|
||||
|
||||
**No unit tests yet** - TypeScript strict mode catches most errors, integration via Go tests
|
||||
|
||||
**CI validation** (from `.github/workflows/check.yml`):
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm js:build # TypeScript strict compilation
|
||||
pnpm prettier --check .
|
||||
```
|
||||
|
||||
**Accessibility testing**:
|
||||
|
||||
- Keyboard navigation: Tab through all interactive elements
|
||||
- Screen reader: Test with VoiceOver (macOS/iOS) or NVDA (Windows)
|
||||
- Contrast: Verify 7:1 ratios with browser dev tools
|
||||
- See [../../docs/accessibility.md](../../docs/accessibility.md) for comprehensive guide
|
||||
|
||||
## Code Style
|
||||
|
||||
**TypeScript Ultra-Strict** (from `tsconfig.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
}
|
||||
```
|
||||
|
||||
**No `any` types allowed**:
|
||||
|
||||
```typescript
|
||||
// ✅ Good: explicit types
|
||||
function validatePassword(password: string, minLength: number): boolean {
|
||||
return password.length >= minLength;
|
||||
}
|
||||
|
||||
// ❌ Bad: any type
|
||||
function validatePassword(password: any): boolean {
|
||||
return password.length >= 8; // ❌ unsafe
|
||||
}
|
||||
```
|
||||
|
||||
**Prettier formatting**:
|
||||
|
||||
- 120 char width
|
||||
- 2-space indentation
|
||||
- Semicolons required
|
||||
- Double quotes (not single)
|
||||
- Trailing comma: none
|
||||
|
||||
**File organization**:
|
||||
|
||||
- TypeScript source: `static/js/*.ts`
|
||||
- Output: `static/js/*.js` (minified ES modules)
|
||||
- CSS input: `tailwind.css` (Tailwind directives)
|
||||
- CSS output: `static/styles.css` (PostCSS processed)
|
||||
|
||||
## Accessibility Standards (WCAG 2.2 AAA)
|
||||
|
||||
**Required compliance** - not optional:
|
||||
|
||||
### Keyboard Navigation
|
||||
|
||||
- All interactive elements focusable with Tab
|
||||
- Visual focus indicators (4px outline, 7:1 contrast)
|
||||
- Logical tab order (top to bottom, left to right)
|
||||
- No keyboard traps
|
||||
- Skip links where needed
|
||||
|
||||
### Screen Readers
|
||||
|
||||
- Semantic HTML: `<button>`, `<input>`, `<label>`, not `<div onclick>`
|
||||
- ARIA labels on icon-only buttons: `aria-label="Submit"`
|
||||
- Error messages: `aria-describedby` linking to error text
|
||||
- Live regions for dynamic content: `aria-live="polite"`
|
||||
- Form field associations: `<label for="id">` + `<input id="id">`
|
||||
|
||||
### Color & Contrast
|
||||
|
||||
- Text: 7:1 contrast ratio (AAA)
|
||||
- Large text (18pt+): 4.5:1 minimum
|
||||
- Focus indicators: 3:1 against adjacent colors
|
||||
- Dark mode: same contrast requirements
|
||||
- Never rely on color alone (use icons, text, patterns)
|
||||
|
||||
### Responsive & Adaptive
|
||||
|
||||
- Responsive: layout adapts to viewport size
|
||||
- Text zoom: 200% without horizontal scroll
|
||||
- Adaptive density: spacing adjusts for user preferences
|
||||
- Touch targets: 44×44 CSS pixels minimum (mobile)
|
||||
|
||||
### Examples
|
||||
|
||||
**✅ Good: Accessible button**
|
||||
|
||||
```html
|
||||
<button type="submit" class="btn-primary focus:ring-4 focus:ring-blue-300" aria-label="Submit password change">
|
||||
<svg aria-hidden="true">...</svg>
|
||||
Change Password
|
||||
</button>
|
||||
```
|
||||
|
||||
**❌ Bad: Inaccessible div-button**
|
||||
|
||||
```html
|
||||
<div onclick="submit()" class="button">❌ not keyboard accessible Submit</div>
|
||||
```
|
||||
|
||||
**✅ Good: Form with error handling**
|
||||
|
||||
```html
|
||||
<form>
|
||||
<label for="password">New Password</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
aria-describedby="password-error"
|
||||
aria-invalid="true"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<div id="password-error" role="alert">Password must be at least 8 characters</div>
|
||||
</form>
|
||||
```
|
||||
|
||||
**❌ Bad: Form without associations**
|
||||
|
||||
```html
|
||||
<form>
|
||||
<div>Password</div>
|
||||
❌ not a label, no association <input type="password" /> ❌ no autocomplete, no error linkage
|
||||
<div style="color: red">Error</div>
|
||||
❌ no role="alert", only color
|
||||
</form>
|
||||
```
|
||||
|
||||
## Tailwind CSS Patterns
|
||||
|
||||
**Use utility classes**, not custom CSS:
|
||||
|
||||
**✅ Good: Utility classes**
|
||||
|
||||
```html
|
||||
<button
|
||||
class="rounded-lg bg-blue-600 px-4 py-2 font-semibold text-white hover:bg-blue-700 focus:ring-4 focus:ring-blue-300"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
```
|
||||
|
||||
**❌ Bad: Custom CSS**
|
||||
|
||||
```html
|
||||
<button class="custom-button">Submit</button>
|
||||
<style>
|
||||
.custom-button {
|
||||
background: blue;
|
||||
} /* ❌ Use Tailwind utilities */
|
||||
</style>
|
||||
```
|
||||
|
||||
**Dark mode support**:
|
||||
|
||||
```html
|
||||
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-gray-100">Content</div>
|
||||
```
|
||||
|
||||
**Responsive design**:
|
||||
|
||||
```html
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- Responsive grid: 1 col mobile, 2 tablet, 3 desktop -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**Focus states (required)**:
|
||||
|
||||
```html
|
||||
<button class="focus:ring-4 focus:ring-blue-300 focus:outline-none">
|
||||
<!-- 4px focus ring, 7:1 contrast -->
|
||||
</button>
|
||||
```
|
||||
|
||||
## TypeScript Patterns
|
||||
|
||||
**Strict null checking**:
|
||||
|
||||
```typescript
|
||||
// ✅ Good: handle nulls explicitly
|
||||
function getElement(id: string): HTMLElement | null {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
const el = getElement("password");
|
||||
if (el) {
|
||||
// ✅ null check
|
||||
el.textContent = "Hello";
|
||||
}
|
||||
|
||||
// ❌ Bad: assume non-null
|
||||
const el = getElement("password");
|
||||
el.textContent = "Hello"; // ❌ may crash if null
|
||||
```
|
||||
|
||||
**Type guards**:
|
||||
|
||||
```typescript
|
||||
// ✅ Good: type guard for forms
|
||||
function isHTMLFormElement(element: Element): element is HTMLFormElement {
|
||||
return element instanceof HTMLFormElement;
|
||||
}
|
||||
|
||||
const form = document.querySelector("form");
|
||||
if (form && isHTMLFormElement(form)) {
|
||||
form.addEventListener("submit", handleSubmit);
|
||||
}
|
||||
```
|
||||
|
||||
**No unsafe array access**:
|
||||
|
||||
```typescript
|
||||
// ✅ Good: check array bounds
|
||||
const items = ["a", "b", "c"];
|
||||
const first = items[0]; // string | undefined (noUncheckedIndexedAccess)
|
||||
if (first) {
|
||||
console.log(first.toUpperCase());
|
||||
}
|
||||
|
||||
// ❌ Bad: unsafe access
|
||||
console.log(items[0].toUpperCase()); // ❌ may crash if empty array
|
||||
```
|
||||
|
||||
## PR/Commit Checklist
|
||||
|
||||
**Before committing frontend code**:
|
||||
|
||||
- [ ] Run `pnpm js:build` (TypeScript strict check)
|
||||
- [ ] Run `pnpm prettier --write internal/web/`
|
||||
- [ ] Verify keyboard navigation works
|
||||
- [ ] Test with screen reader (VoiceOver/NVDA)
|
||||
- [ ] Check contrast ratios (7:1 for text)
|
||||
- [ ] Test dark mode
|
||||
- [ ] Verify password manager autofill works
|
||||
- [ ] No console errors in browser
|
||||
- [ ] Test on mobile viewport (responsive)
|
||||
|
||||
**Accessibility checklist**:
|
||||
|
||||
- [ ] All interactive elements keyboard accessible
|
||||
- [ ] Focus indicators visible (4px outline, 7:1 contrast)
|
||||
- [ ] ARIA labels on icon-only buttons
|
||||
- [ ] Form fields properly labeled
|
||||
- [ ] Error messages linked with aria-describedby
|
||||
- [ ] No color-only information conveyance
|
||||
- [ ] Touch targets ≥44×44 CSS pixels (mobile)
|
||||
|
||||
**Performance checklist**:
|
||||
|
||||
- [ ] Minified JS (via `pnpm js:minify`)
|
||||
- [ ] CSS optimized (cssnano via PostCSS)
|
||||
- [ ] No unused Tailwind classes (purged automatically)
|
||||
- [ ] No console.log in production code
|
||||
|
||||
## Good vs Bad Examples
|
||||
|
||||
**✅ Good: Type-safe DOM access**
|
||||
|
||||
```typescript
|
||||
function setupPasswordToggle(): void {
|
||||
const toggle = document.getElementById("toggle-password");
|
||||
const input = document.getElementById("password");
|
||||
|
||||
if (!toggle || !(input instanceof HTMLInputElement)) {
|
||||
return; // Guard against missing elements
|
||||
}
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
input.type = input.type === "password" ? "text" : "password";
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Bad: Unsafe DOM access**
|
||||
|
||||
```typescript
|
||||
function setupPasswordToggle() {
|
||||
const toggle = document.getElementById("toggle-password")!; // ❌ non-null assertion
|
||||
const input = document.getElementById("password") as any; // ❌ any type
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
input.type = input.type === "password" ? "text" : "password"; // ❌ may crash
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Good: Accessible form validation**
|
||||
|
||||
```typescript
|
||||
function showError(input: HTMLInputElement, message: string): void {
|
||||
const errorId = `${input.id}-error`;
|
||||
let errorEl = document.getElementById(errorId);
|
||||
|
||||
if (!errorEl) {
|
||||
errorEl = document.createElement("div");
|
||||
errorEl.id = errorId;
|
||||
errorEl.setAttribute("role", "alert");
|
||||
errorEl.className = "text-red-600 dark:text-red-400 text-sm mt-1";
|
||||
input.parentElement?.appendChild(errorEl);
|
||||
}
|
||||
|
||||
errorEl.textContent = message;
|
||||
input.setAttribute("aria-invalid", "true");
|
||||
input.setAttribute("aria-describedby", errorId);
|
||||
}
|
||||
```
|
||||
|
||||
**❌ Bad: Inaccessible validation**
|
||||
|
||||
```typescript
|
||||
function showError(input: any, message: string) {
|
||||
// ❌ any type
|
||||
input.style.borderColor = "red"; // ❌ color only, no text
|
||||
alert(message); // ❌ blocks UI, not persistent
|
||||
}
|
||||
```
|
||||
|
||||
## When Stuck
|
||||
|
||||
**TypeScript issues**:
|
||||
|
||||
1. **Type errors**: Check `tsconfig.json` flags, use proper types (no `any`)
|
||||
2. **Null errors**: Add null checks or type guards
|
||||
3. **Module errors**: Ensure ES module syntax (`import`/`export`)
|
||||
4. **Build errors**: `pnpm install` to refresh dependencies
|
||||
|
||||
**CSS issues**:
|
||||
|
||||
1. **Styles not applying**: Check Tailwind purge config, rebuild with `pnpm css:build`
|
||||
2. **Dark mode broken**: Use `dark:` prefix on utilities
|
||||
3. **Responsive broken**: Use `md:`, `lg:` breakpoint prefixes
|
||||
4. **Custom classes**: Don't - use Tailwind utilities instead
|
||||
|
||||
**Accessibility issues**:
|
||||
|
||||
1. **Keyboard nav broken**: Check tab order, focus indicators
|
||||
2. **Screen reader confusion**: Verify ARIA labels, semantic HTML
|
||||
3. **Contrast failure**: Use darker colors, test with dev tools
|
||||
4. **See**: [../../docs/accessibility.md](../../docs/accessibility.md)
|
||||
|
||||
**Browser dev tools**:
|
||||
|
||||
- Accessibility tab: Check ARIA, contrast, structure
|
||||
- Lighthouse: Run accessibility audit (aim for 100 score)
|
||||
- Console: No errors in production code
|
||||
|
||||
## Testing Workflow
|
||||
|
||||
**Manual testing required** (no automated frontend tests yet):
|
||||
|
||||
1. **Visual testing**: Check all pages in light/dark mode
|
||||
2. **Keyboard testing**: Tab through all interactive elements
|
||||
3. **Screen reader testing**: Use VoiceOver (Cmd+F5) or NVDA
|
||||
4. **Responsive testing**: Test mobile, tablet, desktop viewports
|
||||
5. **Browser testing**: Chrome, Firefox, Safari, Edge
|
||||
6. **Password manager**: Test autofill with 1Password, LastPass, etc.
|
||||
|
||||
**Accessibility testing tools**:
|
||||
|
||||
- Browser dev tools Lighthouse
|
||||
- axe DevTools extension
|
||||
- WAVE browser extension
|
||||
- Manual keyboard/screen reader testing (required)
|
||||
|
||||
**Integration testing**: Go backend tests exercise full request/response flow including frontend templates
|
||||
Reference in New Issue
Block a user