Files
Containr/app/backend/internal/cli/commands/invites.go
T
Tomáš DvořákandDevin 50e39fe140 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>
2026-10-09 10:35:12 +02:00

93 lines
2.2 KiB
Go

package commands
import (
"github.com/spf13/cobra"
)
var inviteEmail string
var inviteTTL int
// InvitesCmd manages team invite links (admin).
var InvitesCmd = &cobra.Command{
Use: "invites",
Short: "Manage team invite links (admin)",
}
var inviteListCmd = &cobra.Command{
Use: "list",
Short: "List invites",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := client()
if err != nil {
return err
}
data, err := c.Do("GET", "/admin/invites", nil)
if err != nil {
return err
}
items, err := unwrapList(data, "invites")
if err != nil {
return err
}
rows := make([][]string, 0, len(items))
for _, i := range items {
state := "open"
if _, used := i["used_at"]; used && i["used_at"] != nil {
state = "used"
}
rows = append(rows, []string{
str(i, "id"), str(i, "email"), str(i, "expires_at"), state,
})
}
printRows(data, []string{"ID", "EMAIL", "EXPIRES", "STATE"}, rows)
return nil
},
}
var inviteCreateCmd = &cobra.Command{
Use: "create",
Short: "Create an invite link (--email to bind, --hours for TTL)",
RunE: func(cmd *cobra.Command, args []string) error {
c, err := client()
if err != nil {
return err
}
req := map[string]interface{}{}
if cmd.Flags().Changed("email") {
req["email"] = inviteEmail
}
if cmd.Flags().Changed("hours") {
req["expires_in_hours"] = inviteTTL
}
data, err := c.Do("POST", "/admin/invites", req)
if err != nil {
return err
}
PrintRaw(data)
return nil
},
}
var inviteRevokeCmd = &cobra.Command{
Use: "revoke <id>",
Short: "Revoke an invite",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if !Confirm("Revoke invite " + args[0] + "?") {
return &APIError{Message: "aborted (pass --yes to skip confirmation)", ExitCode: ExitError}
}
c, err := client()
if err != nil {
return err
}
_, err = c.Do("DELETE", "/admin/invites/"+args[0], nil)
return err
},
}
func init() {
inviteCreateCmd.Flags().StringVar(&inviteEmail, "email", "", "bind invite to this email")
inviteCreateCmd.Flags().IntVar(&inviteTTL, "hours", 168, "invite lifetime in hours (1-2160)")
InvitesCmd.AddCommand(inviteListCmd, inviteCreateCmd, inviteRevokeCmd)
}