Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ JWT_SECRET=
ACCESS_TOKEN_DURATION=15m
REFRESH_TOKEN_DURATION=168h # 7 days
RESET_TOKEN_DURATION=1h
VERIFY_EMAIL_TOKEN_DURATION=24h
RATE_LIMIT_PER_MINUTE=60

# ─── CORS ─────────────────────────────────────────────────────────────────────
Expand All @@ -27,7 +28,7 @@ RATE_LIMIT_PER_MINUTE=60
CORS_ORIGINS=*

# ─── Email (SMTP) ─────────────────────────────────────────────────────────────
# Leave SMTP_HOST empty to disable password reset email delivery safely.
# Leave SMTP_HOST empty to disable password reset and email verification delivery safely.
# For local development, prefer MailHog via docker-compose.
SMTP_HOST=
SMTP_PORT=587
Expand Down
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ All API routes live under `/api/v1/`. Public routes need no auth; protected rout
| `users` | Auth subjects; email unique per org |
| `sessions` | Refresh token hashes (never store raw tokens) |
| `password_reset_tokens` | One-time reset tokens (hashed) |
| `email_verification_tokens` | One-time email verification tokens (hashed) |
| `roles` | RBAC roles per org |
| `permissions` | System-level named permissions (seeded) |
| `role_permissions` | Many-to-many role ↔ permission |
Expand Down Expand Up @@ -196,6 +197,7 @@ All configuration is loaded from `.env` (or environment variables) via Viper. Se
| `JWT_SECRET` | — | Required; min 32 chars |
| `ACCESS_TOKEN_DURATION` | `15m` | Short-lived access token TTL |
| `REFRESH_TOKEN_DURATION` | `168h` | Refresh token TTL (7 days) |
| `VERIFY_EMAIL_TOKEN_DURATION` | `24h` | Email verification token TTL |
| `RATE_LIMIT_PER_MINUTE` | `60` | Requests per IP per minute |
| `CORS_ORIGINS` | `*` | Comma-separated allowed origins |
| `SMTP_*` | — | Optional; prints emails to stdout if unset |
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ curl http://localhost:8080/api/v1/users/me \
- **API key lifecycle management** to issue, scope, revoke, and audit keys
- **Audit logs** for authentication and authorization events
- **Webhooks** for auth events with HMAC signatures
- **Email verification** with one-time token links sent via SMTP
- **Password reset** via SMTP
- **Rate limiting** backed by Redis
- **Health and readiness endpoints** for orchestration
Expand Down Expand Up @@ -128,6 +129,7 @@ All configuration is via environment variables (see `.env.example`):
| `ACCESS_TOKEN_DURATION` | `15m` | Access token lifetime |
| `REFRESH_TOKEN_DURATION` | `168h` | Refresh token lifetime (7 days) |
| `RESET_TOKEN_DURATION` | `1h` | Password reset token lifetime |
| `VERIFY_EMAIL_TOKEN_DURATION` | `24h` | Email verification token lifetime |
| `RATE_LIMIT_PER_MINUTE` | `60` | Max requests per IP per minute |
| `CORS_ORIGINS` | `*` | Comma-separated allowed origins |
| `SMTP_HOST` | — | SMTP server for password reset delivery |
Expand All @@ -153,6 +155,8 @@ Protected routes currently require `Authorization: Bearer <access-token>`. Refre
| `POST` | `/api/v1/auth/password/reset-request` | None | Request password reset email |
| `POST` | `/api/v1/auth/password/reset-confirm` | None | Confirm reset with token |
| `PUT` | `/api/v1/auth/password/change` | JWT | Change password |
| `POST` | `/api/v1/auth/email/verify-request` | JWT | Resend verification email |
| `POST` | `/api/v1/auth/email/verify-confirm` | None | Confirm email with token |
| `GET` | `/api/v1/users/me` | JWT | Get own profile |
| `PUT` | `/api/v1/users/me` | JWT | Update own profile |
| `GET` | `/api/v1/users` | JWT | List org users |
Expand Down Expand Up @@ -193,7 +197,7 @@ Protected routes currently require `Authorization: Bearer <access-token>`. Refre
## Roadmap

- [ ] Scoped API-key auth on protected routes
- [ ] Email verification flow
- [x] Email verification flow
- [ ] Multi-Factor Authentication (TOTP)
- [ ] OAuth2 provider (use UniAuth as your OAuth2 server)
- [ ] Social login (Google, GitHub)
Expand Down
203 changes: 203 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,107 @@ const docTemplate = `{
}
}
},
"/api/v1/auth/email/verify-confirm": {
"post": {
"description": "Validates the one-time verification token and marks the user's email as verified.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Confirm email verification",
"parameters": [
{
"description": "Verification token",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api_handlers.VerifyEmailConfirmBody"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerMessageResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"401": {
"description": "Token invalid or expired",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
}
}
}
},
"/api/v1/auth/email/verify-request": {
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Sends a verification email to the authenticated user. Returns 409 if the email is already verified.",
"produces": [
"application/json"
],
"tags": [
"Auth"
],
"summary": "Request email verification",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerMessageResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"403": {
"description": "Account is inactive",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"409": {
"description": "Email already verified",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
}
}
}
},
"/api/v1/auth/login": {
"post": {
"description": "Validates credentials and returns a JWT access token and a refresh token. Only the access token is accepted in the Authorization header.",
Expand Down Expand Up @@ -1257,6 +1358,73 @@ const docTemplate = `{
}
}
}
},
"post": {
"security": [
{
"BearerAuth": []
}
],
"description": "Creates a new user inside the caller's organization. The organization is derived from the JWT — no org_id is accepted in the request body. New users always default to non-superuser. Optionally assign initial roles at creation time. Requires the ` + "`" + `users:write` + "`" + ` permission.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Users"
],
"summary": "Create a user in the organization",
"parameters": [
{
"description": "User creation payload",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/internal_api_handlers.CreateUserRequest"
}
}
],
"responses": {
"201": {
"description": "Created",
"schema": {
"$ref": "#/definitions/internal_api_handlers.UserView"
}
},
"400": {
"description": "Validation error (weak password, empty email, invalid role ID)",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"403": {
"description": "Missing users:write permission",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"409": {
"description": "Email already exists in organization",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/internal_api_handlers.SwaggerErrorResponse"
}
}
}
}
},
"/api/v1/users/me": {
Expand Down Expand Up @@ -2086,6 +2254,32 @@ const docTemplate = `{
}
}
},
"internal_api_handlers.CreateUserRequest": {
"type": "object",
"properties": {
"email": {
"type": "string",
"example": "employee@acme.com"
},
"full_name": {
"type": "string",
"example": "John Doe"
},
"password": {
"type": "string",
"example": "S3cur3P@ss!"
},
"role_ids": {
"type": "array",
"items": {
"type": "string"
},
"example": [
"550e8400-e29b-41d4-a716-446655440002"
]
}
}
},
"internal_api_handlers.CreateWebhookRequest": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -2504,6 +2698,15 @@ const docTemplate = `{
}
}
},
"internal_api_handlers.VerifyEmailConfirmBody": {
"type": "object",
"properties": {
"token": {
"type": "string",
"example": "550e8400-e29b-41d4-a716-446655440099"
}
}
},
"internal_api_handlers.WebhookListResponse": {
"type": "object",
"properties": {
Expand Down
Loading
Loading