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 @@