feat(invites): team invite links — admin create/revoke, public accept (#42)

* feat(branding): white-label — public /branding, admin write, shell theming

app_settings branding.* keys (product name, logo/favicon URLs, accent
color, docs/support links). Public GET /branding so login and shell
theme pre-auth; admin writes via PUT /settings.

useBranding() applies document.title, accent CSS var and favicon;
BrandWordmark replaces hardcoded wordmarks; admin Platform settings
gain a Branding form. MCP containr_branding_get + settings_update.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(invites): team invite links — admin create/revoke, public accept

Admins mint single-use invite links (sha256-hashed token at rest,
optional email binding, configurable TTL). Public endpoints let the
accept page validate the link and register — working even while
public signup is disabled.

- migration + sqlc queries/model for user_invites
- POST /admin/invites (returns one-time token+url), GET list, DELETE revoke
- GET /auth/invites/:token public validation, POST /auth/accept-invite
- CLI containr invites list|create|revoke; MCP invites tools
- /auth/accept-invite page (expired/used/invalid states, bound-email lock)
- Admin page Team invites section: create, copy link, revoke
- OpenAPI + generated types + CLI guide

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomáš DvořákandDevin authored and GitHub committed 2026-10-09 10:35:12 +02:00
1 parent b47ced0135
commit 50e39fe140
20 files changed
+1194 -4

No files matched your search

+106
View File
@@ -2702,6 +2702,112 @@ paths:
'204':
description: Deleted
/auth/invites/{token}:
get:
tags:
- Invites
summary: Validate an invite token (public)
responses:
'200':
description: Invite state
content:
application/json:
schema:
type: object
properties:
valid: {type: boolean}
used: {type: boolean}
expired: {type: boolean}
email: {type: string, nullable: true}
expires_at: {type: string, format: date-time}
'404':
description: Invite not found
/auth/accept-invite:
post:
tags:
- Invites
summary: Register via an invite link (public)
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [token, name, email, password]
properties:
token: {type: string}
name: {type: string}
email: {type: string}
password: {type: string, minLength: 8}
responses:
'201':
description: Created session
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
'410':
description: Invite used or expired
/admin/invites:
get:
tags:
- Invites
summary: List invites (admin)
security:
- bearerAuth: []
responses:
'200':
description: All invites
content:
application/json:
schema:
type: object
properties:
invites:
type: array
items:
type: object
post:
tags:
- Invites
summary: Create an invite link (admin)
security:
- bearerAuth: []
requestBody:
content:
application/json:
schema:
type: object
properties:
email: {type: string, description: Optionally bind the invite to this email}
expires_in_hours: {type: integer, default: 168, minimum: 1, maximum: 2160}
responses:
'201':
description: Created invite with one-time token
content:
application/json:
schema:
type: object
properties:
invite: {type: object}
token: {type: string, description: Shown once}
url: {type: string}
/admin/invites/{id}:
delete:
tags:
- Invites
summary: Revoke an invite (admin)
security:
- bearerAuth: []
parameters:
- {name: id, in: path, required: true, schema: {type: string}}
responses:
'204':
description: Revoked
/branding:
get:
tags: