ultisuite-backend/internal/users/provision.go
R3D347HR4Y 621b0099d6
Some checks are pending
CI / Go tests (push) Waiting to run
CI / Integration tests (push) Waiting to run
CI / DB migrations (push) Waiting to run
feat(deploy): enhance Nginx configuration and API integration for UltiAI
- Updated .env.example to include new configuration options for the UltiAI branding and API endpoints.
- Enhanced Nginx configuration to support new API routes for the MCP and WebSocket connections.
- Introduced sub-filters for branding adjustments in Nginx responses.
- Added new JavaScript patch for API endpoint adjustments.
- Implemented tests for new API functionalities and improved error handling in the AI gateway.
2026-06-15 00:22:23 +02:00

141 lines
4.0 KiB
Go

package users
import (
"context"
"errors"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/ultisuite/ulti-backend/internal/auth"
)
// ProvisionEmail returns the email stored for a newly provisioned user.
func ProvisionEmail(claims *auth.Claims) string {
if claims == nil {
return ""
}
email := strings.TrimSpace(claims.Email)
if email != "" {
return email
}
return claims.Sub + "@unknown.ultimail.local"
}
// EnsureUser inserts or updates the Ultimail user row for OIDC claims and returns the internal UUID.
func EnsureUser(ctx context.Context, db *pgxpool.Pool, claims *auth.Claims) (string, error) {
if db == nil {
return "", fmt.Errorf("database not configured")
}
if claims == nil || strings.TrimSpace(claims.Sub) == "" {
return "", fmt.Errorf("missing subject claim")
}
email := ProvisionEmail(claims)
name := strings.TrimSpace(claims.Name)
var userCount int64
if err := db.QueryRow(ctx, `SELECT COUNT(*) FROM users`).Scan(&userCount); err != nil {
return "", fmt.Errorf("count users: %w", err)
}
isFirstUser := userCount == 0
var userID string
err := db.QueryRow(ctx, `
INSERT INTO users (external_id, email, name, platform_admin)
VALUES ($1, $2, $3, $4)
ON CONFLICT (external_id) DO UPDATE SET
email = EXCLUDED.email,
name = EXCLUDED.name,
updated_at = NOW()
RETURNING id
`, claims.Sub, email, name, isFirstUser).Scan(&userID)
if err != nil {
return "", fmt.Errorf("provision user: %w", err)
}
if isFirstUser {
if err := bootstrapFirstUserAdmin(ctx, db, userID); err != nil {
return "", fmt.Errorf("bootstrap platform admin: %w", err)
}
}
return userID, nil
}
// LookupUserID returns the internal user UUID for an OIDC subject.
func LookupUserID(ctx context.Context, db *pgxpool.Pool, externalID string) (string, error) {
if db == nil {
return "", fmt.Errorf("database not configured")
}
externalID = strings.TrimSpace(externalID)
if externalID == "" {
return "", pgx.ErrNoRows
}
var userID string
err := db.QueryRow(ctx, `SELECT id::text FROM users WHERE external_id = $1`, externalID).Scan(&userID)
return userID, err
}
// LookupIdentityByEmail returns the OIDC subject and stored email for a user row.
func LookupIdentityByEmail(ctx context.Context, db *pgxpool.Pool, email string) (externalID, storedEmail string, err error) {
if db == nil {
return "", "", fmt.Errorf("database not configured")
}
email = strings.ToLower(strings.TrimSpace(email))
if email == "" {
return "", "", pgx.ErrNoRows
}
err = db.QueryRow(ctx, `
SELECT external_id, email FROM users WHERE lower(email) = $1
`, email).Scan(&externalID, &storedEmail)
return externalID, storedEmail, err
}
// LookupUserIDByEmail returns the internal user UUID for a stored email address.
func LookupUserIDByEmail(ctx context.Context, db *pgxpool.Pool, email string) (string, error) {
if db == nil {
return "", fmt.Errorf("database not configured")
}
email = strings.ToLower(strings.TrimSpace(email))
if email == "" {
return "", pgx.ErrNoRows
}
var userID string
err := db.QueryRow(ctx, `SELECT id::text FROM users WHERE lower(email) = $1`, email).Scan(&userID)
return userID, err
}
// ResolveProvisionUser finds an existing user by external_id or email, or creates one from Authentik enrollment data.
func ResolveProvisionUser(ctx context.Context, db *pgxpool.Pool, externalID, email, name string) (string, error) {
externalID = strings.TrimSpace(externalID)
email = strings.ToLower(strings.TrimSpace(email))
if externalID != "" {
userID, err := LookupUserID(ctx, db, externalID)
if err == nil {
return userID, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return "", err
}
}
if email != "" {
userID, err := LookupUserIDByEmail(ctx, db, email)
if err == nil {
return userID, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return "", err
}
}
if externalID == "" {
return "", fmt.Errorf("cannot provision user without external_id or existing email")
}
return EnsureUser(ctx, db, &auth.Claims{
Sub: externalID,
Email: email,
Name: name,
})
}