diff --git a/backend/internal/api/handlers/users.go b/backend/internal/api/handlers/users.go index 8dbc749..16a14c2 100644 --- a/backend/internal/api/handlers/users.go +++ b/backend/internal/api/handlers/users.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "git.workaround.org/chaas/imc/backend/internal/auth" "git.workaround.org/chaas/imc/backend/internal/db" "git.workaround.org/chaas/imc/backend/internal/mail" "github.com/gin-gonic/gin" @@ -22,7 +23,7 @@ func NewUserHandler(database *db.DB) *UserHandler { type CreateUserRequest struct { Email string `json:"email" binding:"required"` - Password string `json:"password" binding:"required"` + Password string `json:"password"` Quota int64 `json:"quota"` } @@ -169,7 +170,11 @@ func (h *UserHandler) Create(c *gin.Context) { return } - passwordHash := "{BLF-CRYPT}" + req.Password + password := req.Password + if password == "" { + password = auth.GenerateRandomPassword(16) + } + passwordHash := "{BLF-CRYPT}" + password err = h.db.CreateUser(ctx, domain.ID, req.Email, passwordHash, req.Quota) if err != nil { @@ -178,7 +183,7 @@ func (h *UserHandler) Create(c *gin.Context) { return } - Created(c, map[string]string{"message": "user created"}) + Created(c, map[string]interface{}{"message": "user created", "password": password}) } func (h *UserHandler) Update(c *gin.Context) { diff --git a/backend/internal/auth/jwt.go b/backend/internal/auth/jwt.go index 425b468..9c05e8f 100644 --- a/backend/internal/auth/jwt.go +++ b/backend/internal/auth/jwt.go @@ -3,8 +3,10 @@ package auth import ( - "errors" // standard errors package - "time" // time handling + "crypto/rand" // cryptographically secure random number generator + "errors" // standard errors package + "log" // logging + "time" // time handling "github.com/golang-jwt/jwt/v5" // JWT library for token handling "golang.org/x/crypto/bcrypt" // bcrypt for secure password hashing @@ -125,3 +127,16 @@ func CheckPassword(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } + +// GenerateRandomPassword creates a cryptographically secure random password. +func GenerateRandomPassword(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*" + result := make([]byte, length) + if _, err := rand.Read(result); err != nil { + log.Fatalf("Failed to generate random password: %v", err) + } + for i := range result { + result[i] = charset[int(result[i])%len(charset)] + } + return string(result) +} diff --git a/backend/internal/db/virtual_users.go b/backend/internal/db/virtual_users.go index 26169ed..9150b38 100644 --- a/backend/internal/db/virtual_users.go +++ b/backend/internal/db/virtual_users.go @@ -25,7 +25,7 @@ func (d *DB) GetUserByEmail(ctx context.Context, email string) (imcdb.VirtualUse } func (d *DB) CreateUser(ctx context.Context, domainID uint32, email, passwordHash string, quota int64) error { - quotaNull := sql.NullInt64{Int64: quota, Valid: quota > 0} + quotaNull := sql.NullInt64{Int64: quota, Valid: true} return d.Queries.CreateUser(ctx, imcdb.CreateUserParams{ DomainID: domainID, Email: email, @@ -42,7 +42,7 @@ func (d *DB) UpdateUserPassword(ctx context.Context, id uint32, passwordHash str } func (d *DB) UpdateUserQuota(ctx context.Context, id uint32, quota int64) error { - quotaNull := sql.NullInt64{Int64: quota, Valid: quota > 0} + quotaNull := sql.NullInt64{Int64: quota, Valid: true} return d.Queries.UpdateUserQuota(ctx, imcdb.UpdateUserQuotaParams{ Quota: quotaNull, ID: id, diff --git a/frontend/src/routes/domains/[name]/users/+page.svelte b/frontend/src/routes/domains/[name]/users/+page.svelte index 72706e3..2352f92 100644 --- a/frontend/src/routes/domains/[name]/users/+page.svelte +++ b/frontend/src/routes/domains/[name]/users/+page.svelte @@ -14,6 +14,8 @@ let localPart = $state(''); let newUser = $state({ password: '', quota: 0 }); let createError = $state(''); + let createdPassword = $state(''); + let showCreatedPassword = $state(false); let domainName = $state(''); async function loadUsers(name: string) { @@ -35,6 +37,8 @@ function openModal() { createError = ''; + createdPassword = ''; + showCreatedPassword = false; localPart = ''; newUser = { password: '', quota: 0 }; dialogEl?.showModal(); @@ -44,6 +48,17 @@ dialogEl?.close(); } + function generatePassword() { + const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*'; + let password = ''; + const array = new Uint8Array(16); + crypto.getRandomValues(array); + for (let i = 0; i < 16; i++) { + password += chars[array[i] % chars.length]; + } + newUser.password = password; + } + async function createUser() { createError = ''; const email = `${localPart}@${domainName}`; @@ -55,14 +70,15 @@ 'Content-Type': 'application/json', Authorization: `Bearer ${token}` }, - body: JSON.stringify({ email, password: newUser.password, quota: newUser.quota }) + body: JSON.stringify({ email, password: newUser.password || undefined, quota: newUser.quota }) }); + const data = await res.json(); if (res.ok) { - closeModal(); + createdPassword = data.password || ''; + showCreatedPassword = true; await loadUsers(domainName); } else { - const error = await res.json(); - createError = error.error || 'Failed to create user'; + createError = data.error || 'Failed to create user'; } } catch (e) { createError = 'Failed to create user'; @@ -70,6 +86,10 @@ } } + async function copyPassword() { + await navigator.clipboard.writeText(createdPassword); + } + async function deleteUser(id: number) { if (!confirm('Delete this user? Mailbox will NOT be deleted.')) return; try { @@ -191,13 +211,20 @@
+ {#if showCreatedPassword && createdPassword} +{createdPassword}
+
+