diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index 4f0608bc..d81240a9 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -378,3 +378,13 @@ func (e *ClientIdAlreadyExistsError) Error() string { func (e *ClientIdAlreadyExistsError) HttpStatusCode() int { return http.StatusBadRequest } + +type UserEmailNotSetError struct{} + +func (e *UserEmailNotSetError) Error() string { + return "The user does not have an email address set" +} + +func (e *UserEmailNotSetError) HttpStatusCode() int { + return http.StatusBadRequest +} diff --git a/backend/internal/dto/app_config_dto.go b/backend/internal/dto/app_config_dto.go index 7b8c1d91..215f094a 100644 --- a/backend/internal/dto/app_config_dto.go +++ b/backend/internal/dto/app_config_dto.go @@ -21,6 +21,7 @@ type AppConfigUpdateDto struct { SignupDefaultUserGroupIDs string `json:"signupDefaultUserGroupIDs" binding:"omitempty,json"` SignupDefaultCustomClaims string `json:"signupDefaultCustomClaims" binding:"omitempty,json"` AccentColor string `json:"accentColor"` + RequireUserEmail string `json:"requireUserEmail" binding:"required"` SmtpHost string `json:"smtpHost"` SmtpPort string `json:"smtpPort"` SmtpFrom string `json:"smtpFrom" binding:"omitempty,email"` diff --git a/backend/internal/dto/user_dto.go b/backend/internal/dto/user_dto.go index 10814f3f..985b12d7 100644 --- a/backend/internal/dto/user_dto.go +++ b/backend/internal/dto/user_dto.go @@ -10,7 +10,7 @@ import ( type UserDto struct { ID string `json:"id"` Username string `json:"username"` - Email string `json:"email" ` + Email *string `json:"email" ` FirstName string `json:"firstName"` LastName *string `json:"lastName"` DisplayName string `json:"displayName"` @@ -24,7 +24,7 @@ type UserDto struct { type UserCreateDto struct { Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"` - Email string `json:"email" binding:"required,email" unorm:"nfc"` + Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"` FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"` LastName string `json:"lastName" binding:"max=50" unorm:"nfc"` DisplayName string `json:"displayName" binding:"required,min=1,max=100" unorm:"nfc"` @@ -64,9 +64,9 @@ type UserUpdateUserGroupDto struct { } type SignUpDto struct { - Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"` - Email string `json:"email" binding:"required,email" unorm:"nfc"` - FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"` - LastName string `json:"lastName" binding:"max=50" unorm:"nfc"` - Token string `json:"token"` + Username string `json:"username" binding:"required,username,min=2,max=50" unorm:"nfc"` + Email *string `json:"email" binding:"omitempty,email" unorm:"nfc"` + FirstName string `json:"firstName" binding:"required,min=1,max=50" unorm:"nfc"` + LastName string `json:"lastName" binding:"max=50" unorm:"nfc"` + Token string `json:"token"` } diff --git a/backend/internal/dto/user_dto_test.go b/backend/internal/dto/user_dto_test.go index 014afa53..a037218f 100644 --- a/backend/internal/dto/user_dto_test.go +++ b/backend/internal/dto/user_dto_test.go @@ -3,6 +3,7 @@ package dto import ( "testing" + "github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/stretchr/testify/require" ) @@ -16,7 +17,7 @@ func TestUserCreateDto_Validate(t *testing.T) { name: "valid input", input: UserCreateDto{ Username: "testuser", - Email: "test@example.com", + Email: utils.Ptr("test@example.com"), FirstName: "John", LastName: "Doe", DisplayName: "John Doe", @@ -26,7 +27,7 @@ func TestUserCreateDto_Validate(t *testing.T) { { name: "missing username", input: UserCreateDto{ - Email: "test@example.com", + Email: utils.Ptr("test@example.com"), FirstName: "John", LastName: "Doe", DisplayName: "John Doe", @@ -36,7 +37,7 @@ func TestUserCreateDto_Validate(t *testing.T) { { name: "missing display name", input: UserCreateDto{ - Email: "test@example.com", + Email: utils.Ptr("test@example.com"), FirstName: "John", LastName: "Doe", }, @@ -46,7 +47,7 @@ func TestUserCreateDto_Validate(t *testing.T) { name: "username contains invalid characters", input: UserCreateDto{ Username: "test/ser", - Email: "test@example.com", + Email: utils.Ptr("test@example.com"), FirstName: "John", LastName: "Doe", DisplayName: "John Doe", @@ -57,7 +58,7 @@ func TestUserCreateDto_Validate(t *testing.T) { name: "invalid email", input: UserCreateDto{ Username: "testuser", - Email: "not-an-email", + Email: utils.Ptr("not-an-email"), FirstName: "John", LastName: "Doe", DisplayName: "John Doe", @@ -68,7 +69,7 @@ func TestUserCreateDto_Validate(t *testing.T) { name: "first name too short", input: UserCreateDto{ Username: "testuser", - Email: "test@example.com", + Email: utils.Ptr("test@example.com"), FirstName: "", LastName: "Doe", DisplayName: "John Doe", @@ -79,7 +80,7 @@ func TestUserCreateDto_Validate(t *testing.T) { name: "last name too long", input: UserCreateDto{ Username: "testuser", - Email: "test@example.com", + Email: utils.Ptr("test@example.com"), FirstName: "John", LastName: "abcdfghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz", DisplayName: "John Doe", diff --git a/backend/internal/job/api_key_expiry_job.go b/backend/internal/job/api_key_expiry_job.go index 3bd82d11..226f9c59 100644 --- a/backend/internal/job/api_key_expiry_job.go +++ b/backend/internal/job/api_key_expiry_job.go @@ -37,7 +37,7 @@ func (j *ApiKeyEmailJobs) checkAndNotifyExpiringApiKeys(ctx context.Context) err } for _, key := range apiKeys { - if key.User.Email == "" { + if key.User.Email == nil { continue } err = j.apiKeyService.SendApiKeyExpiringSoonEmail(ctx, key) diff --git a/backend/internal/model/app_config.go b/backend/internal/model/app_config.go index 09e543a7..77c57bca 100644 --- a/backend/internal/model/app_config.go +++ b/backend/internal/model/app_config.go @@ -46,6 +46,7 @@ type AppConfig struct { // Internal InstanceID AppConfigVariable `key:"instanceId,internal"` // Internal // Email + RequireUserEmail AppConfigVariable `key:"requireUserEmail,public"` // Public SmtpHost AppConfigVariable `key:"smtpHost"` SmtpPort AppConfigVariable `key:"smtpPort"` SmtpFrom AppConfigVariable `key:"smtpFrom"` diff --git a/backend/internal/model/user.go b/backend/internal/model/user.go index a692b43f..76dc29b6 100644 --- a/backend/internal/model/user.go +++ b/backend/internal/model/user.go @@ -13,12 +13,12 @@ import ( type User struct { Base - Username string `sortable:"true"` - Email string `sortable:"true"` - FirstName string `sortable:"true"` - LastName string `sortable:"true"` - DisplayName string `sortable:"true"` - IsAdmin bool `sortable:"true"` + Username string `sortable:"true"` + Email *string `sortable:"true"` + FirstName string `sortable:"true"` + LastName string `sortable:"true"` + DisplayName string `sortable:"true"` + IsAdmin bool `sortable:"true"` Locale *string LdapID *string Disabled bool `sortable:"true"` diff --git a/backend/internal/service/api_key_service.go b/backend/internal/service/api_key_service.go index 547ca641..18ec4d25 100644 --- a/backend/internal/service/api_key_service.go +++ b/backend/internal/service/api_key_service.go @@ -144,9 +144,13 @@ func (s *ApiKeyService) SendApiKeyExpiringSoonEmail(ctx context.Context, apiKey } } + if user.Email == nil { + return &common.UserEmailNotSetError{} + } + err := SendEmail(ctx, s.emailService, email.Address{ Name: user.FullName(), - Email: user.Email, + Email: *user.Email, }, ApiKeyExpiringSoonTemplate, &ApiKeyExpiringSoonTemplateData{ ApiKeyName: apiKey.Name, ExpiresAt: apiKey.ExpiresAt.ToTime(), diff --git a/backend/internal/service/app_config_service.go b/backend/internal/service/app_config_service.go index 96f3dcf8..f9611691 100644 --- a/backend/internal/service/app_config_service.go +++ b/backend/internal/service/app_config_service.go @@ -71,6 +71,7 @@ func (s *AppConfigService) getDefaultDbConfig() *model.AppConfig { // Internal InstanceID: model.AppConfigVariable{Value: ""}, // Email + RequireUserEmail: model.AppConfigVariable{Value: "true"}, SmtpHost: model.AppConfigVariable{}, SmtpPort: model.AppConfigVariable{}, SmtpFrom: model.AppConfigVariable{}, diff --git a/backend/internal/service/audit_log_service.go b/backend/internal/service/audit_log_service.go index fb5ceec7..7855174b 100644 --- a/backend/internal/service/audit_log_service.go +++ b/backend/internal/service/audit_log_service.go @@ -111,9 +111,13 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres return } + if user.Email == nil { + return + } + innerErr = SendEmail(innerCtx, s.emailService, email.Address{ Name: user.FullName(), - Email: user.Email, + Email: *user.Email, }, NewLoginTemplate, &NewLoginTemplateData{ IPAddress: ipAddress, Country: createdAuditLog.Country, @@ -122,7 +126,7 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ctx context.Context, ipAddres DateTime: createdAuditLog.CreatedAt.UTC(), }) if innerErr != nil { - slog.ErrorContext(innerCtx, "Failed to send notification email", slog.Any("error", innerErr), slog.String("address", user.Email)) + slog.ErrorContext(innerCtx, "Failed to send notification email", slog.Any("error", innerErr), slog.String("address", *user.Email)) return } }() diff --git a/backend/internal/service/e2etest_service.go b/backend/internal/service/e2etest_service.go index cae91c9c..31c931ed 100644 --- a/backend/internal/service/e2etest_service.go +++ b/backend/internal/service/e2etest_service.go @@ -79,7 +79,7 @@ func (s *TestService) SeedDatabase(baseURL string) error { ID: "f4b89dc2-62fb-46bf-9f5f-c34f4eafe93e", }, Username: "tim", - Email: "tim.cook@test.com", + Email: utils.Ptr("tim.cook@test.com"), FirstName: "Tim", LastName: "Cook", DisplayName: "Tim Cook", @@ -90,7 +90,7 @@ func (s *TestService) SeedDatabase(baseURL string) error { ID: "1cd19686-f9a6-43f4-a41f-14a0bf5b4036", }, Username: "craig", - Email: "craig.federighi@test.com", + Email: utils.Ptr("craig.federighi@test.com"), FirstName: "Craig", LastName: "Federighi", DisplayName: "Craig Federighi", diff --git a/backend/internal/service/email_service.go b/backend/internal/service/email_service.go index 62df2675..8ecaf7e5 100644 --- a/backend/internal/service/email_service.go +++ b/backend/internal/service/email_service.go @@ -62,9 +62,13 @@ func (srv *EmailService) SendTestEmail(ctx context.Context, recipientUserId stri return err } + if user.Email == nil { + return &common.UserEmailNotSetError{} + } + return SendEmail(ctx, srv, email.Address{ - Email: user.Email, + Email: *user.Email, Name: user.FullName(), }, TestTemplate, nil) } diff --git a/backend/internal/service/jwt_service_test.go b/backend/internal/service/jwt_service_test.go index 70d4915a..46425b3a 100644 --- a/backend/internal/service/jwt_service_test.go +++ b/backend/internal/service/jwt_service_test.go @@ -16,6 +16,7 @@ import ( "github.com/lestrrat-go/jwx/v3/jwa" "github.com/lestrrat-go/jwx/v3/jwk" "github.com/lestrrat-go/jwx/v3/jwt" + "github.com/pocket-id/pocket-id/backend/internal/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -342,7 +343,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) { Base: model.Base{ ID: "user123", }, - Email: "user@example.com", + Email: utils.Ptr("user@example.com"), IsAdmin: false, } @@ -385,7 +386,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) { Base: model.Base{ ID: "admin123", }, - Email: "admin@example.com", + Email: utils.Ptr("admin@example.com"), IsAdmin: true, } @@ -464,7 +465,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) { Base: model.Base{ ID: "eddsauser123", }, - Email: "eddsauser@example.com", + Email: utils.Ptr("eddsauser@example.com"), IsAdmin: true, } @@ -521,7 +522,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) { Base: model.Base{ ID: "ecdsauser123", }, - Email: "ecdsauser@example.com", + Email: utils.Ptr("ecdsauser@example.com"), IsAdmin: true, } @@ -578,7 +579,7 @@ func TestGenerateVerifyAccessToken(t *testing.T) { Base: model.Base{ ID: "rsauser123", }, - Email: "rsauser@example.com", + Email: utils.Ptr("rsauser@example.com"), IsAdmin: true, } @@ -965,7 +966,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) { Base: model.Base{ ID: "user123", }, - Email: "user@example.com", + Email: utils.Ptr("user@example.com"), } const clientID = "test-client-123" @@ -1092,7 +1093,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) { Base: model.Base{ ID: "eddsauser789", }, - Email: "eddsaoauth@example.com", + Email: utils.Ptr("eddsaoauth@example.com"), } const clientID = "eddsa-oauth-client" @@ -1149,7 +1150,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) { Base: model.Base{ ID: "ecdsauser789", }, - Email: "ecdsaoauth@example.com", + Email: utils.Ptr("ecdsaoauth@example.com"), } const clientID = "ecdsa-oauth-client" @@ -1206,7 +1207,7 @@ func TestGenerateVerifyOAuthAccessToken(t *testing.T) { Base: model.Base{ ID: "rsauser789", }, - Email: "rsaoauth@example.com", + Email: utils.Ptr("rsaoauth@example.com"), } const clientID = "rsa-oauth-client" diff --git a/backend/internal/service/ldap_service.go b/backend/internal/service/ldap_service.go index 8c24505f..1840beb3 100644 --- a/backend/internal/service/ldap_service.go +++ b/backend/internal/service/ldap_service.go @@ -17,6 +17,7 @@ import ( "github.com/go-ldap/ldap/v3" "github.com/google/uuid" + "github.com/pocket-id/pocket-id/backend/internal/utils" "golang.org/x/text/unicode/norm" "gorm.io/gorm" @@ -348,7 +349,7 @@ func (s *LdapService) SyncUsers(ctx context.Context, tx *gorm.DB, client *ldap.C newUser := dto.UserCreateDto{ Username: value.GetAttributeValue(dbConfig.LdapAttributeUserUsername.Value), - Email: value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value), + Email: utils.PtrOrNil(value.GetAttributeValue(dbConfig.LdapAttributeUserEmail.Value)), FirstName: value.GetAttributeValue(dbConfig.LdapAttributeUserFirstName.Value), LastName: value.GetAttributeValue(dbConfig.LdapAttributeUserLastName.Value), DisplayName: value.GetAttributeValue(dbConfig.LdapAttributeUserDisplayName.Value), diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index da477bb3..147fc079 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -244,6 +244,10 @@ func (s *UserService) CreateUser(ctx context.Context, input dto.UserCreateDto) ( } func (s *UserService) createUserInternal(ctx context.Context, input dto.UserCreateDto, isLdapSync bool, tx *gorm.DB) (model.User, error) { + if s.appConfigService.GetDbConfig().RequireUserEmail.IsTrue() && input.Email == nil { + return model.User{}, &common.UserEmailNotSetError{} + } + user := model.User{ FirstName: input.FirstName, LastName: input.LastName, @@ -339,6 +343,10 @@ func (s *UserService) UpdateUser(ctx context.Context, userID string, updatedUser } func (s *UserService) updateUserInternal(ctx context.Context, userID string, updatedUser dto.UserCreateDto, updateOwnUser bool, isLdapSync bool, tx *gorm.DB) (model.User, error) { + if s.appConfigService.GetDbConfig().RequireUserEmail.IsTrue() && updatedUser.Email == nil { + return model.User{}, &common.UserEmailNotSetError{} + } + var user model.User err := tx. WithContext(ctx). @@ -437,6 +445,10 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use return err } + if user.Email == nil { + return &common.UserEmailNotSetError{} + } + oneTimeAccessToken, err := s.createOneTimeAccessTokenInternal(ctx, user.ID, ttl, tx) if err != nil { return err @@ -464,7 +476,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use errInternal := SendEmail(innerCtx, s.emailService, email.Address{ Name: user.FullName(), - Email: user.Email, + Email: *user.Email, }, OneTimeAccessTemplate, &OneTimeAccessTemplateData{ Code: oneTimeAccessToken, LoginLink: link, @@ -472,7 +484,7 @@ func (s *UserService) requestOneTimeAccessEmailInternal(ctx context.Context, use ExpirationString: utils.DurationToString(ttl), }) if errInternal != nil { - slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", user.Email)) + slog.ErrorContext(innerCtx, "Failed to send one-time access token email", slog.Any("error", errInternal), slog.String("address", *user.Email)) return } }() diff --git a/backend/internal/utils/ptr_util.go b/backend/internal/utils/ptr_util.go index d791f67a..dae0bb5b 100644 --- a/backend/internal/utils/ptr_util.go +++ b/backend/internal/utils/ptr_util.go @@ -1,13 +1,16 @@ package utils +// Ptr returns a pointer to the given value. func Ptr[T any](v T) *T { return &v } -func PtrValueOrZero[T any](ptr *T) T { - if ptr == nil { - var zero T - return zero +// PtrOrNil returns a pointer to v if v is not the zero value of its type, +// otherwise it returns nil. +func PtrOrNil[T comparable](v T) *T { + var zero T + if v == zero { + return nil } - return *ptr + return &v } diff --git a/backend/resources/migrations/postgres/20251001115300_optional_email.down.sql b/backend/resources/migrations/postgres/20251001115300_optional_email.down.sql new file mode 100644 index 00000000..b8553463 --- /dev/null +++ b/backend/resources/migrations/postgres/20251001115300_optional_email.down.sql @@ -0,0 +1 @@ +-- No-op because email was optional before the migration \ No newline at end of file diff --git a/backend/resources/migrations/postgres/20251001115300_optional_email.up.sql b/backend/resources/migrations/postgres/20251001115300_optional_email.up.sql new file mode 100644 index 00000000..9aa26564 --- /dev/null +++ b/backend/resources/migrations/postgres/20251001115300_optional_email.up.sql @@ -0,0 +1 @@ +ALTER TABLE users ALTER COLUMN email DROP NOT NULL; diff --git a/backend/resources/migrations/sqlite/20251001115300_optional_email.down.sql b/backend/resources/migrations/sqlite/20251001115300_optional_email.down.sql new file mode 100644 index 00000000..b8553463 --- /dev/null +++ b/backend/resources/migrations/sqlite/20251001115300_optional_email.down.sql @@ -0,0 +1 @@ +-- No-op because email was optional before the migration \ No newline at end of file diff --git a/backend/resources/migrations/sqlite/20251001115300_optional_email.up.sql b/backend/resources/migrations/sqlite/20251001115300_optional_email.up.sql new file mode 100644 index 00000000..83d5cd04 --- /dev/null +++ b/backend/resources/migrations/sqlite/20251001115300_optional_email.up.sql @@ -0,0 +1,40 @@ +PRAGMA foreign_keys = OFF; +BEGIN; + +CREATE TABLE users_new +( + id TEXT NOT NULL PRIMARY KEY, + created_at DATETIME, + username TEXT NOT NULL COLLATE NOCASE UNIQUE, + email TEXT UNIQUE, + first_name TEXT, + last_name TEXT NOT NULL, + display_name TEXT NOT NULL, + is_admin NUMERIC NOT NULL DEFAULT FALSE, + ldap_id TEXT, + locale TEXT, + disabled NUMERIC NOT NULL DEFAULT FALSE +); + +INSERT INTO users_new (id, created_at, username, email, first_name, last_name, display_name, is_admin, ldap_id, locale, + disabled) +SELECT id, + created_at, + username, + email, + first_name, + last_name, + display_name, + is_admin, + ldap_id, + locale, + disabled +FROM users; + +DROP TABLE users; + +ALTER TABLE users_new + RENAME TO users; + +COMMIT; +PRAGMA foreign_keys = ON; \ No newline at end of file diff --git a/frontend/messages/cs.json b/frontend/messages/cs.json index 620d570c..7d49879d 100644 --- a/frontend/messages/cs.json +++ b/frontend/messages/cs.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Nejsou k dispozici žádná náhledová data", "copy_all": "Kopírovat vše", "preview": "Náhled", - "preview_for_user": "Náhled pro {name} ({email})", + "preview_for_user": "Náhled pro {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Náhled OIDC dat, která by byla odeslána pro uživatele", "show": "Zobrazit", "select_an_option": "Vyberte možnost", diff --git a/frontend/messages/da.json b/frontend/messages/da.json index 5e1d9e68..b2fc3fab 100644 --- a/frontend/messages/da.json +++ b/frontend/messages/da.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Ingen forhåndsvisningsdata tilgængelig", "copy_all": "Kopiér alt", "preview": "Forhåndsvisning", - "preview_for_user": "Forhåndsvisning for {name} ({email})", + "preview_for_user": "Forhåndsvisning for {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Forhåndsvis OIDC-data, der ville blive sendt for denne bruger", "show": "Vis", "select_an_option": "Vælg en indstilling", diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 2823e102..c3bec262 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Keine Vorschaudaten verfügbar", "copy_all": "Alles kopieren", "preview": "Vorschau", - "preview_for_user": "Vorschau für {name} ({email})", + "preview_for_user": "Vorschau für {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Vorschau der OIDC-Daten, für diesen Benutzer", "show": "Anzeigen", "select_an_option": "Wähle eine Option", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 761cd357..22684134 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -373,7 +373,7 @@ "no_preview_data_available": "No preview data available", "copy_all": "Copy All", "preview": "Preview", - "preview_for_user": "Preview for {name} ({email})", + "preview_for_user": "Preview for {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Preview the OIDC data that would be sent for this user", "show": "Show", "select_an_option": "Select an option", @@ -453,5 +453,7 @@ "ui_config_disabled_info_title": "UI Configuration Disabled", "ui_config_disabled_info_description": "The UI configuration is disabled because the application configuration settings are managed through environment variables. Some settings may not be editable.", "logo_from_url_description": "Paste a direct image URL (svg, png, webp). Find icons at Selfh.st Icons or Dashboard Icons.", - "invalid_url": "Invalid URL" + "invalid_url": "Invalid URL", + "require_user_email": "Require Email Address", + "require_user_email_description": "Requires users to have an email address. If disabled, the users without an email address won't be able to use features that require an email address." } diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 38ff56d5..73a7f08a 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -373,7 +373,7 @@ "no_preview_data_available": "No hay datos de vista previa disponibles.", "copy_all": "Copiar todo", "preview": "Vista previa", - "preview_for_user": "Vista previa de « {name} » ({email})", + "preview_for_user": "Vista previa de « {name} »", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Previsualiza los datos OIDC que se enviarían para este usuario.", "show": "Mostrar", "select_an_option": "Selecciona una opción", diff --git a/frontend/messages/fr.json b/frontend/messages/fr.json index 63e157a0..73d7df6c 100644 --- a/frontend/messages/fr.json +++ b/frontend/messages/fr.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Aucune donnée d'aperçu disponible", "copy_all": "Tout copier", "preview": "Aperçu", - "preview_for_user": "Aperçu pour {name} ({email})", + "preview_for_user": "Aperçu pour {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Aperçu des données OIDC qui seraient envoyées pour cet utilisateur", "show": "Afficher", "select_an_option": "Sélectionner une option", diff --git a/frontend/messages/it.json b/frontend/messages/it.json index 7df93900..7df9e093 100644 --- a/frontend/messages/it.json +++ b/frontend/messages/it.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Dati di anteprima non disponibili", "copy_all": "Copia tutto", "preview": "Anteprima", - "preview_for_user": "Anteprima per {name} ({email})", + "preview_for_user": "Anteprima per {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Anteprima dei dati OIDC che saranno inviati per l'utente", "show": "Mostra", "select_an_option": "Seleziona un'opzione", diff --git a/frontend/messages/ko.json b/frontend/messages/ko.json index 2b252818..ef668d6c 100644 --- a/frontend/messages/ko.json +++ b/frontend/messages/ko.json @@ -373,7 +373,7 @@ "no_preview_data_available": "미리보기 데이터가 없습니다", "copy_all": "모두 복사", "preview": "미리보기", - "preview_for_user": "{name} ({email}) 미리보기", + "preview_for_user": "{name} 미리보기", "preview_the_oidc_data_that_would_be_sent_for_this_user": "이 사용자를 위해 전송될 OIDC 데이터 미리보기", "show": "표시", "select_an_option": "옵션 선택", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index b64d2b9a..8b6833d9 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Geen voorbeeldgegevens beschikbaar", "copy_all": "Alles kopiëren", "preview": "Voorbeeld", - "preview_for_user": "Voorbeeld van {name} ({email})", + "preview_for_user": "Voorbeeld van {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Bekijk een voorbeeld van de OIDC-gegevens die voor deze gebruiker zouden worden verzonden.", "show": "Laten zien", "select_an_option": "Kies een optie", diff --git a/frontend/messages/pl.json b/frontend/messages/pl.json index 4e6ffc04..7756ff42 100644 --- a/frontend/messages/pl.json +++ b/frontend/messages/pl.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Brak dostępnych danych podglądu", "copy_all": "Skopiuj wszystko", "preview": "Podgląd", - "preview_for_user": "Zapowiedź książki „ {name} ” ({email})", + "preview_for_user": "Zapowiedź książki „ {name} ”", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Wyświetl podgląd danych OIDC, które zostaną wysłane dla tego użytkownika.", "show": "Pokaż", "select_an_option": "Wybierz opcję", diff --git a/frontend/messages/pt-BR.json b/frontend/messages/pt-BR.json index 0d59d481..8c4a7a51 100644 --- a/frontend/messages/pt-BR.json +++ b/frontend/messages/pt-BR.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Não tem dados de pré-visualização disponíveis", "copy_all": "Copiar tudo", "preview": "Pré-visualização", - "preview_for_user": "Prévia de “ {name} ” ({email})", + "preview_for_user": "Prévia de “ {name} ”", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Dá uma olhada nos dados OIDC que seriam enviados para esse usuário.", "show": "Mostrar", "select_an_option": "Escolha uma opção", diff --git a/frontend/messages/ru.json b/frontend/messages/ru.json index e8ea96ac..587b78da 100644 --- a/frontend/messages/ru.json +++ b/frontend/messages/ru.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Предварительный просмотр данных не доступен", "copy_all": "Копировать все", "preview": "Предпросмотр", - "preview_for_user": "Предпросмотр для {name} ({email})", + "preview_for_user": "Предпросмотр для {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Предпросмотр данных OIDC, которые будут отправлены для этого пользователя", "show": "Показать", "select_an_option": "Выберите опцию", diff --git a/frontend/messages/sv.json b/frontend/messages/sv.json index b0ba9745..31f15500 100644 --- a/frontend/messages/sv.json +++ b/frontend/messages/sv.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Inga förhandsgranskningsdata tillgängliga", "copy_all": "Kopiera allt", "preview": "Förhandsgranska", - "preview_for_user": "Förhandsgranskning för {name} ({email})", + "preview_for_user": "Förhandsgranskning för {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Förhandsgranska OIDC-data som skulle skickas för denna användare", "show": "Visa", "select_an_option": "Välj ett alternativ", diff --git a/frontend/messages/uk.json b/frontend/messages/uk.json index 7d2642c8..cf5ebe93 100644 --- a/frontend/messages/uk.json +++ b/frontend/messages/uk.json @@ -373,7 +373,7 @@ "no_preview_data_available": "Попередній перегляд даних недоступний", "copy_all": "Скопіювати все", "preview": "Попередній перегляд", - "preview_for_user": "Попередній перегляд для {name} ({email})", + "preview_for_user": "Попередній перегляд для {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Попередній перегляд OIDC-даних для цього користувача", "show": "Показати", "select_an_option": "Обрати варіант", diff --git a/frontend/messages/vi.json b/frontend/messages/vi.json index a5026fb0..0f64d924 100644 --- a/frontend/messages/vi.json +++ b/frontend/messages/vi.json @@ -373,7 +373,7 @@ "no_preview_data_available": "No preview data available", "copy_all": "Sao chép tất cả", "preview": "Xem trước", - "preview_for_user": "Xem trước cho {name} ({email})", + "preview_for_user": "Xem trước cho {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "Xem trước dữ liệu OIDC sẽ được gửi cho người dùng này", "show": "Hiển thị", "select_an_option": "Chọn một tùy chọn", diff --git a/frontend/messages/zh-CN.json b/frontend/messages/zh-CN.json index 9dfd461d..b9ed05f3 100644 --- a/frontend/messages/zh-CN.json +++ b/frontend/messages/zh-CN.json @@ -373,7 +373,7 @@ "no_preview_data_available": "暂无可用的预览数据", "copy_all": "全部复制", "preview": "预览", - "preview_for_user": "为 {name} ({email}) 预览", + "preview_for_user": "为 {name} 预览", "preview_the_oidc_data_that_would_be_sent_for_this_user": "预览将为此用户发送的 OIDC 数据", "show": "显示", "select_an_option": "请选择", diff --git a/frontend/messages/zh-TW.json b/frontend/messages/zh-TW.json index e1583ec5..6fb58cad 100644 --- a/frontend/messages/zh-TW.json +++ b/frontend/messages/zh-TW.json @@ -373,7 +373,7 @@ "no_preview_data_available": "無預覽資料", "copy_all": "全部複製", "preview": "預覽", - "preview_for_user": "預覽 {name} ({email})", + "preview_for_user": "預覽 {name}", "preview_the_oidc_data_that_would_be_sent_for_this_user": "預覽將為此使用者傳送的 OIDC 資料", "show": "顯示", "select_an_option": "選擇一個選項", diff --git a/frontend/src/lib/components/signup/signup-form.svelte b/frontend/src/lib/components/signup/signup-form.svelte index 00d5f6b9..f0a0ca41 100644 --- a/frontend/src/lib/components/signup/signup-form.svelte +++ b/frontend/src/lib/components/signup/signup-form.svelte @@ -1,11 +1,13 @@