Record project continuation changes

This commit is contained in:
2026-05-12 21:02:29 +03:00
parent 3059d1d7a3
commit 8f69d53193
339 changed files with 101111 additions and 1769 deletions
+15 -7
View File
@@ -14,12 +14,13 @@ const (
)
type User struct {
ID string
Email string
PasswordHash string
MFAEnabled bool
CreatedAt time.Time
UpdatedAt time.Time
ID string `json:"id"`
Email string `json:"email"`
PasswordHash string `json:"-"`
MFAEnabled bool `json:"mfa_enabled"`
PlatformRole string `json:"platform_role"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type Device struct {
@@ -40,7 +41,7 @@ type AuthSession struct {
ID string
UserID string
DeviceID string
RefreshTokenHash string
RefreshTokenHash string `json:"-"`
RefreshExpiresAt time.Time
LastSeenAt *time.Time
LastRotatedAt *time.Time
@@ -69,6 +70,13 @@ type BootstrapOwnerCommand struct {
ActivationSignature string `json:"activation_signature"`
}
type CreateUserCommand struct {
ActorUserID string `json:"actor_user_id"`
Email string `json:"email"`
Password string `json:"password"`
PlatformRole string `json:"platform_role"`
}
type RevokeAuthSessionCommand struct {
UserID string `json:"user_id"`
AuthSessionID string `json:"auth_session_id"`
+30
View File
@@ -34,6 +34,10 @@ func (m *Module) RegisterRoutes(router chi.Router) {
r.Get("/devices", m.handleTrustedDevices)
r.Post("/devices/{deviceID}/revoke", m.handleRevokeTrustedDevice)
})
router.Route("/users", func(r chi.Router) {
r.Get("/", m.handleListUsers)
r.Post("/", m.handleCreateUser)
})
}
func (m *Module) handleInstallationStatus(w http.ResponseWriter, r *http.Request) {
@@ -78,6 +82,32 @@ func (m *Module) handleLogin(w http.ResponseWriter, r *http.Request) {
httpx.WriteJSON(w, http.StatusOK, result)
}
func (m *Module) handleListUsers(w http.ResponseWriter, r *http.Request) {
actorUserID := r.URL.Query().Get("actor_user_id")
users, err := m.service.ListUsers(r.Context(), actorUserID)
if err != nil {
status, message := m.service.MapError(err)
httpx.WriteError(w, status, message)
return
}
httpx.WriteJSON(w, http.StatusOK, map[string]any{"users": users})
}
func (m *Module) handleCreateUser(w http.ResponseWriter, r *http.Request) {
var cmd CreateUserCommand
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
httpx.WriteError(w, http.StatusBadRequest, "invalid user payload")
return
}
user, err := m.service.CreateUser(r.Context(), cmd)
if err != nil {
status, message := m.service.MapError(err)
httpx.WriteError(w, status, message)
return
}
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"user": user})
}
func (m *Module) handleRefresh(w http.ResponseWriter, r *http.Request) {
var cmd RefreshCommand
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
@@ -70,7 +70,7 @@ type postgresInstallationRepository struct {
func (r *postgresUserRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
const query = `
SELECT id::text, email, password_hash, mfa_enabled, created_at, updated_at
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
FROM users
WHERE email = $1
`
@@ -79,13 +79,53 @@ WHERE email = $1
func (r *postgresUserRepository) GetByID(ctx context.Context, userID string) (*User, error) {
const query = `
SELECT id::text, email, password_hash, mfa_enabled, created_at, updated_at
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
FROM users
WHERE id = $1::uuid
`
return scanOptionalUser(r.db.QueryRow(ctx, query, userID))
}
func (r *postgresUserRepository) List(ctx context.Context) ([]User, error) {
const query = `
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
FROM users
ORDER BY created_at DESC
`
rows, err := r.db.Query(ctx, query)
if err != nil {
return nil, fmt.Errorf("query users: %w", err)
}
defer rows.Close()
var users []User
for rows.Next() {
user, err := scanOptionalUser(rows)
if err != nil {
return nil, err
}
if user != nil {
users = append(users, *user)
}
}
return users, rows.Err()
}
func (r *postgresUserRepository) Create(ctx context.Context, user User) (*User, error) {
const query = `
INSERT INTO users (email, password_hash, mfa_enabled, platform_role, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
`
return scanOptionalUser(r.db.QueryRow(ctx, query,
user.Email,
user.PasswordHash,
user.MFAEnabled,
user.PlatformRole,
user.CreatedAt,
user.UpdatedAt,
))
}
func (r *postgresDeviceRepository) Upsert(ctx context.Context, params UpsertDeviceParams) (*Device, error) {
const query = `
INSERT INTO devices (
@@ -348,7 +388,7 @@ ON CONFLICT (email) DO UPDATE SET
password_hash = EXCLUDED.password_hash,
platform_role = EXCLUDED.platform_role,
updated_at = EXCLUDED.updated_at
RETURNING id::text, email, password_hash, mfa_enabled, created_at, updated_at
RETURNING id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
`, email, params.PasswordHash, params.Role, now))
if err != nil {
return nil, fmt.Errorf("upsert bootstrap owner: %w", err)
@@ -461,6 +501,7 @@ func scanOptionalUser(row scanner) (*User, error) {
&user.Email,
&user.PasswordHash,
&user.MFAEnabled,
&user.PlatformRole,
&user.CreatedAt,
&user.UpdatedAt,
); err != nil {
@@ -7,8 +7,10 @@ import (
)
type UserRepository interface {
List(ctx context.Context) ([]User, error)
GetByEmail(ctx context.Context, email string) (*User, error)
GetByID(ctx context.Context, userID string) (*User, error)
Create(ctx context.Context, user User) (*User, error)
}
type DeviceRepository interface {
+67 -1
View File
@@ -13,11 +13,13 @@ import (
"github.com/example/remote-access-platform/backend/internal/platform/authority"
"github.com/example/remote-access-platform/backend/internal/platform/module"
postgresplatform "github.com/example/remote-access-platform/backend/internal/platform/postgres"
)
type Service struct {
cfg module.Config
store Store
db postgresplatform.DBTX
transactor Transactor
tokenManager *TokenManager
authority *authority.Verifier
@@ -31,7 +33,7 @@ func NewService(deps module.Dependencies, store Store, transactor Transactor, ve
} else if verifier, err := authority.NewVerifier(deps.Config.Installation); err == nil {
authorityVerifier = verifier
}
return &Service{
service := &Service{
cfg: deps.Config,
store: store,
transactor: transactor,
@@ -45,6 +47,10 @@ func NewService(deps module.Dependencies, store Store, transactor Transactor, ve
authority: authorityVerifier,
now: time.Now,
}
if postgresStore, ok := store.(*postgresStore); ok {
service.db = postgresStore.db
}
return service
}
func (s *Service) Login(ctx context.Context, cmd LoginCommand) (*AuthResult, error) {
@@ -120,6 +126,44 @@ func (s *Service) Login(ctx context.Context, cmd LoginCommand) (*AuthResult, err
return &result, nil
}
func (s *Service) ListUsers(ctx context.Context, actorUserID string) ([]User, error) {
if err := s.ensurePlatformAdmin(ctx, actorUserID); err != nil {
return nil, err
}
return s.store.Users().List(ctx)
}
func (s *Service) CreateUser(ctx context.Context, cmd CreateUserCommand) (*User, error) {
if err := s.ensurePlatformAdmin(ctx, cmd.ActorUserID); err != nil {
return nil, err
}
email := strings.ToLower(strings.TrimSpace(cmd.Email))
password := strings.TrimSpace(cmd.Password)
role := strings.TrimSpace(cmd.PlatformRole)
if role == "" {
role = "user"
}
if email == "" || !strings.Contains(email, "@") || len(password) < 8 {
return nil, ErrInvalidBootstrapOwner
}
if role != "user" && role != authority.PlatformRoleAdmin && role != authority.PlatformRoleRecoveryAdmin {
return nil, ErrInvalidBootstrapOwner
}
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, fmt.Errorf("hash user password: %w", err)
}
now := s.now().UTC()
return s.store.Users().Create(ctx, User{
Email: email,
PasswordHash: string(passwordHash),
MFAEnabled: false,
PlatformRole: role,
CreatedAt: now,
UpdatedAt: now,
})
}
func (s *Service) Refresh(ctx context.Context, cmd RefreshCommand) (*AuthResult, error) {
authSessionID, err := s.tokenManager.ParseRefreshToken(cmd.RefreshToken)
if err != nil {
@@ -438,3 +482,25 @@ func (s *Service) installationStatusFromRecord(record *InstallationAuthorityStat
func (s *Service) strictAuthority() bool {
return s.authority != nil && s.authority.Strict()
}
func (s *Service) ensurePlatformAdmin(ctx context.Context, actorUserID string) error {
if actorUserID == "" {
return ErrInvalidCredentials
}
role := authority.PlatformRoleUser
if s.db != nil {
effectiveRole, err := authority.EffectivePlatformRole(ctx, s.db, s.authority, actorUserID)
if err != nil {
return err
}
role = effectiveRole
} else if user, err := s.store.Users().GetByID(ctx, actorUserID); err != nil {
return err
} else if user != nil && user.PlatformRole != "" {
role = user.PlatformRole
}
if role != authority.PlatformRoleAdmin && role != authority.PlatformRoleRecoveryAdmin {
return ErrDeviceRevoked
}
return nil
}