From 0d55200485dd4e4204361d6e94abcd0ab20ff582 Mon Sep 17 00:00:00 2001 From: zxyszx <299979470+zxyszx@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:43:54 +0800 Subject: [PATCH] feat: support username admin login --- apps/api/internal/app/admin_handlers.go | 45 ++++++++++---------- apps/api/internal/app/auth_handlers.go | 13 ++++-- apps/api/internal/app/permissions.go | 3 ++ apps/api/internal/app/types.go | 27 ++++++------ apps/api/internal/app/util.go | 38 +++++++++++++++++ apps/web/package.json | 2 +- apps/web/src/components/protected-layout.tsx | 2 +- apps/web/src/lib/api-types.ts | 4 +- apps/web/src/lib/api.ts | 4 +- apps/web/src/pages/admin.tsx | 26 +++++------ apps/web/src/pages/login.tsx | 6 +-- deploy/all-in-one/Dockerfile | 2 +- deploy/postfix/main.cf | 2 +- deploy/web.Dockerfile | 2 +- 14 files changed, 112 insertions(+), 64 deletions(-) diff --git a/apps/api/internal/app/admin_handlers.go b/apps/api/internal/app/admin_handlers.go index 5249de2..04adf28 100644 --- a/apps/api/internal/app/admin_handlers.go +++ b/apps/api/internal/app/admin_handlers.go @@ -94,6 +94,7 @@ func (a *App) handleListUsers(w http.ResponseWriter, r *http.Request) { func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) { var req struct { + LoginName string `json:"loginName"` Email string `json:"email"` DisplayName string `json:"displayName"` Role string `json:"role"` @@ -107,14 +108,14 @@ func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) { return } actor := currentUser(r) - email := normalizeEmail(req.Email) - if email == "" || !strings.Contains(email, "@") { - badRequest(w, errors.New("invalid email")) + loginName, err := cleanLoginName(req.LoginName, req.Email) + if err != nil { + badRequest(w, err) return } displayName := strings.TrimSpace(req.DisplayName) if displayName == "" { - displayName = email + displayName = loginName } role := strings.TrimSpace(req.Role) if role == "" { @@ -154,7 +155,7 @@ func (a *App) handleCreateUser(w http.ResponseWriter, r *http.Request) { } defer tx.Rollback() if _, err = tx.ExecContext(r.Context(), `INSERT INTO users(id,email,display_name,role,password_hash,disabled,mailbox_limit_override,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?,?)`, id, email, displayName, role, string(passwordHash), boolInt(req.Disabled), nullableInt(mailboxLimitOverride), now, now); err != nil { + VALUES(?,?,?,?,?,?,?,?,?)`, id, loginName, displayName, role, string(passwordHash), boolInt(req.Disabled), nullableInt(mailboxLimitOverride), now, now); err != nil { badRequest(w, err) return } @@ -515,14 +516,15 @@ func (a *App) handleListMailboxes(w http.ResponseWriter, r *http.Request) { func (a *App) handleCreateMailbox(w http.ResponseWriter, r *http.Request) { var req struct { - DomainID string `json:"domainId"` - LocalPart string `json:"localPart"` - DisplayName string `json:"displayName"` - Password string `json:"password"` - QuotaMB int `json:"quotaMb"` - Role string `json:"role"` - OwnerEmail string `json:"ownerEmail"` - UserID string `json:"userId"` + DomainID string `json:"domainId"` + LocalPart string `json:"localPart"` + DisplayName string `json:"displayName"` + Password string `json:"password"` + QuotaMB int `json:"quotaMb"` + Role string `json:"role"` + OwnerLoginName string `json:"ownerLoginName"` + OwnerEmail string `json:"ownerEmail"` + UserID string `json:"userId"` } if err := decodeJSON(r, &req); err != nil { badRequest(w, err) @@ -591,15 +593,12 @@ func (a *App) handleCreateMailbox(w http.ResponseWriter, r *http.Request) { return } } else { - ownerEmail := normalizeEmail(req.OwnerEmail) - if ownerEmail == "" { - ownerEmail = address - } - if !strings.Contains(ownerEmail, "@") { - badRequest(w, errors.New("invalid owner email")) + ownerLoginName, err := cleanLoginName(req.OwnerLoginName, req.OwnerEmail, address) + if err != nil { + badRequest(w, err) return } - err = tx.QueryRowContext(r.Context(), `SELECT id FROM users WHERE email=? AND disabled=0`, ownerEmail).Scan(&userID) + err = tx.QueryRowContext(r.Context(), `SELECT id FROM users WHERE email=? AND disabled=0`, ownerLoginName).Scan(&userID) if errors.Is(err, sql.ErrNoRows) { passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) if err != nil { @@ -608,11 +607,11 @@ func (a *App) handleCreateMailbox(w http.ResponseWriter, r *http.Request) { } userID = newID("usr") ownerDisplayName := displayName - if !strings.EqualFold(ownerEmail, address) { - ownerDisplayName = ownerEmail + if !strings.EqualFold(ownerLoginName, address) { + ownerDisplayName = ownerLoginName } _, err = tx.ExecContext(r.Context(), `INSERT INTO users(id,email,display_name,role,password_hash,disabled,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?)`, userID, ownerEmail, ownerDisplayName, role, string(passwordHash), 0, now, now) + VALUES(?,?,?,?,?,?,?,?)`, userID, ownerLoginName, ownerDisplayName, role, string(passwordHash), 0, now, now) if err != nil { badRequest(w, err) return diff --git a/apps/api/internal/app/auth_handlers.go b/apps/api/internal/app/auth_handlers.go index 4848e2f..e3e4829 100644 --- a/apps/api/internal/app/auth_handlers.go +++ b/apps/api/internal/app/auth_handlers.go @@ -11,6 +11,7 @@ import ( func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) { var req struct { + LoginName string `json:"loginName"` Email string `json:"email"` Password string `json:"password"` TurnstileToken string `json:"turnstileToken"` @@ -49,14 +50,18 @@ func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusUnauthorized, "人机验证失败,请重试") return } - email := normalizeEmail(req.Email) - user, passwordHash, err := a.userByEmail(r.Context(), email) + loginName, err := cleanLoginName(req.LoginName, req.Email) + if err != nil { + respondError(w, http.StatusUnauthorized, "账号或密码错误") + return + } + user, passwordHash, err := a.userByEmail(r.Context(), loginName) if err != nil || user.Disabled { - respondError(w, http.StatusUnauthorized, "邮箱或密码错误") + respondError(w, http.StatusUnauthorized, "账号或密码错误") return } if err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(req.Password)); err != nil { - respondError(w, http.StatusUnauthorized, "邮箱或密码错误") + respondError(w, http.StatusUnauthorized, "账号或密码错误") return } if a.cfg.TwoFactorEnabled && user.TwoFactorEnabled { diff --git a/apps/api/internal/app/permissions.go b/apps/api/internal/app/permissions.go index 2f81449..82140ba 100644 --- a/apps/api/internal/app/permissions.go +++ b/apps/api/internal/app/permissions.go @@ -616,6 +616,9 @@ func (a *App) attachUserAuthorization(ctx context.Context, u *User) error { if u == nil { return nil } + if u.LoginName == "" { + u.LoginName = u.Email + } permissions, err := a.permissionsForUser(ctx, u.ID, u.Role) if err != nil { return err diff --git a/apps/api/internal/app/types.go b/apps/api/internal/app/types.go index f77b990..005ec12 100644 --- a/apps/api/internal/app/types.go +++ b/apps/api/internal/app/types.go @@ -3,19 +3,20 @@ package app import "time" type User struct { - ID string `json:"id"` - Email string `json:"email"` - DisplayName string `json:"displayName"` - Role string `json:"role"` - Disabled bool `json:"disabled"` - Protected bool `json:"protected"` - TwoFactorEnabled bool `json:"twoFactorEnabled"` - MailboxLimitOverride *int `json:"mailboxLimitOverride,omitempty"` - Permissions []string `json:"permissions"` - Limits PermissionLimits `json:"limits"` - PermissionGroupIDs []string `json:"permissionGroupIds"` - PermissionGroups []PermissionGroupSummary `json:"permissionGroups"` - CreatedAt time.Time `json:"createdAt"` + ID string `json:"id"` + LoginName string `json:"loginName"` + Email string `json:"email"` + DisplayName string `json:"displayName"` + Role string `json:"role"` + Disabled bool `json:"disabled"` + Protected bool `json:"protected"` + TwoFactorEnabled bool `json:"twoFactorEnabled"` + MailboxLimitOverride *int `json:"mailboxLimitOverride,omitempty"` + Permissions []string `json:"permissions"` + Limits PermissionLimits `json:"limits"` + PermissionGroupIDs []string `json:"permissionGroupIds"` + PermissionGroups []PermissionGroupSummary `json:"permissionGroups"` + CreatedAt time.Time `json:"createdAt"` } type AdminUser struct { diff --git a/apps/api/internal/app/util.go b/apps/api/internal/app/util.go index 038ccd1..2aff503 100644 --- a/apps/api/internal/app/util.go +++ b/apps/api/internal/app/util.go @@ -149,6 +149,44 @@ func normalizeEmail(s string) string { return normalizeLocalPart(parts[0]) + "@" + normalizeDomain(parts[1]) } +func normalizeLoginName(s string) string { + s = strings.ToLower(strings.TrimSpace(s)) + if s == "" { + return "" + } + if strings.Contains(s, "@") { + return normalizeEmail(s) + } + return normalizeLocalPart(s) +} + +func cleanLoginName(value string, fallbacks ...string) (string, error) { + loginName := normalizeLoginName(value) + for _, fallback := range fallbacks { + if loginName != "" { + break + } + loginName = normalizeLoginName(fallback) + } + if loginName == "" { + return "", errors.New("登录名不能为空") + } + if len([]rune(loginName)) > 80 { + return "", errors.New("登录名不能超过 80 个字符") + } + if strings.Contains(loginName, "@") { + parts := strings.SplitN(loginName, "@", 2) + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", errors.New("登录名格式无效") + } + return loginName, nil + } + if len([]rune(loginName)) < 2 { + return "", errors.New("登录名至少需要 2 个字符") + } + return loginName, nil +} + func dedupeEmails(items []string) []string { seen := map[string]bool{} out := make([]string, 0, len(items)) diff --git a/apps/web/package.json b/apps/web/package.json index fade335..8e92ffb 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,5 +1,5 @@ { - "name": "lanqin-email-web", + "name": "newszxcn-email-web", "version": "0.1.0", "private": true, "type": "module", diff --git a/apps/web/src/components/protected-layout.tsx b/apps/web/src/components/protected-layout.tsx index cf7d37e..513673f 100644 --- a/apps/web/src/components/protected-layout.tsx +++ b/apps/web/src/components/protected-layout.tsx @@ -104,7 +104,7 @@ function ProtectedContent() {
{user.displayName} - {user.email} + {user.loginName || user.email}
{user.role === "admin" ? "管理员" : "普通用户"} diff --git a/apps/web/src/lib/api-types.ts b/apps/web/src/lib/api-types.ts index a087dca..9118e8d 100644 --- a/apps/web/src/lib/api-types.ts +++ b/apps/web/src/lib/api-types.ts @@ -50,7 +50,7 @@ export type PermissionInfo = { key: PermissionKey; label: string; description: s export type PermissionLimits = { maxAttachmentMb: number; maxMailboxCount: number; smtpDailyLimit: number; smtpMinuteLimit: number; imapMinuteLimit: number; pop3MinuteLimit: number } export type PermissionGroupSummary = { id: string; name: string } export type PermissionGroup = { id: string; name: string; description: string; permissions: PermissionKey[]; limits: PermissionLimits; system: boolean; userCount: number; createdAt: string; updatedAt: string } -export type User = { id: string; email: string; displayName: string; role: "admin" | "user"; disabled: boolean; protected: boolean; twoFactorEnabled: boolean; mailboxLimitOverride?: number | null; permissions: PermissionKey[]; limits: PermissionLimits; permissionGroupIds: string[]; permissionGroups: PermissionGroupSummary[]; createdAt: string } +export type User = { id: string; loginName?: string; email: string; displayName: string; role: "admin" | "user"; disabled: boolean; protected: boolean; twoFactorEnabled: boolean; mailboxLimitOverride?: number | null; permissions: PermissionKey[]; limits: PermissionLimits; permissionGroupIds: string[]; permissionGroups: PermissionGroupSummary[]; createdAt: string } export type APIToken = { id: string; name: string; lastUsedAt?: string; expiresAt?: string; disabled: boolean; scopes: string[]; createdAt: string; updatedAt: string } export type AdminUser = User & { mailboxCount: number; mailboxes?: string[] } export type AdminOverview = { users: number; activeUsers: number; domains: number; mailboxes: number; activeMailboxes: number; aliases: number; messages: number; unreadMessages: number; storageBytes: number } @@ -199,6 +199,6 @@ export type SystemSettings = { export type SystemSettingsPayload = Omit & { smtpPassword: string; turnstileSecretKey: string; externalImapSecretKey: string; externalImapGmailClientSecret: string; externalImapOutlookClientSecret: string } export type PublicDomain = { id: string; name: string } export type PublicSettings = { openRegistration: boolean; turnstileEnabled: boolean; turnstileSiteKey: string; publicHostname: string; mailAutoRefresh: boolean; mailRefreshMs: number; externalImapEnabled: boolean; mailboxDomains?: PublicDomain[] } -export type LoginPayload = { email?: string; password?: string; turnstileToken?: string; challengeToken?: string; twoFactorCode?: string } +export type LoginPayload = { loginName?: string; email?: string; password?: string; turnstileToken?: string; challengeToken?: string; twoFactorCode?: string } export type LoginResponse = { user?: User; twoFactorRequired?: boolean; challengeToken?: string } export type RegisterPayload = { email: string; displayName: string; password: string; turnstileToken?: string; domainId?: string; localPart?: string } diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index c11a8a2..51c4ad3 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -125,7 +125,7 @@ export const api = { updatePermissionGroup: (id: string, payload: { name: string; description: string; permissions: PermissionKey[]; limits: PermissionLimits }) => request(`/api/admin/permission-groups/${id}`, { method: "POST", body: JSON.stringify(payload) }), defaultPermissionLimits: () => request("/api/admin/permission-limits/defaults"), deletePermissionGroup: (id: string) => request<{ ok: boolean }>(`/api/admin/permission-groups/${id}`, { method: "DELETE" }), - createUser: (payload: { email: string; displayName: string; role: "admin" | "user"; password: string; disabled: boolean; mailboxLimitOverride?: number; permissionGroupIds?: string[] }) => request("/api/admin/users", { method: "POST", body: JSON.stringify(payload) }), + createUser: (payload: { loginName: string; displayName: string; role: "admin" | "user"; password: string; disabled: boolean; mailboxLimitOverride?: number; permissionGroupIds?: string[] }) => request("/api/admin/users", { method: "POST", body: JSON.stringify(payload) }), updateUser: (id: string, payload: { displayName: string; role: "admin" | "user"; disabled: boolean; mailboxLimitOverride?: number; permissionGroupIds?: string[] }) => request(`/api/admin/users/${id}`, { method: "POST", body: JSON.stringify(payload) }), resetUserPassword: (id: string, password: string) => request<{ ok: boolean }>(`/api/admin/users/${id}/password`, { method: "POST", body: JSON.stringify({ password }) }), deleteUser: (id: string) => request<{ ok: boolean }>(`/api/admin/users/${id}`, { method: "DELETE" }), @@ -134,7 +134,7 @@ export const api = { updateDomain: (id: string, payload: { status: string }) => request(`/api/admin/domains/${id}`, { method: "POST", body: JSON.stringify(payload) }), deleteDomain: (id: string) => request<{ ok: boolean }>(`/api/admin/domains/${id}`, { method: "DELETE" }), mailboxes: () => request>("/api/admin/mailboxes"), - createMailbox: (payload: { domainId: string; localPart: string; displayName: string; password: string; quotaMb: number; role: "admin" | "user"; ownerEmail?: string; userId?: string }) => request("/api/admin/mailboxes", { method: "POST", body: JSON.stringify(payload) }), + createMailbox: (payload: { domainId: string; localPart: string; displayName: string; password: string; quotaMb: number; role: "admin" | "user"; ownerLoginName?: string; ownerEmail?: string; userId?: string }) => request("/api/admin/mailboxes", { method: "POST", body: JSON.stringify(payload) }), updateMailbox: (id: string, payload: { userId: string; displayName: string; quotaMb: number; status: string }) => request(`/api/admin/mailboxes/${id}`, { method: "POST", body: JSON.stringify(payload) }), deleteMailbox: (id: string) => request<{ ok: boolean }>(`/api/admin/mailboxes/${id}`, { method: "DELETE" }), aliases: () => request>("/api/admin/aliases"), diff --git a/apps/web/src/pages/admin.tsx b/apps/web/src/pages/admin.tsx index 508147c..a3558d0 100644 --- a/apps/web/src/pages/admin.tsx +++ b/apps/web/src/pages/admin.tsx @@ -58,6 +58,7 @@ const projectTag = import.meta.env.VITE_APP_VERSION || "" const projectReleaseUrl = import.meta.env.VITE_RELEASE_URL || (projectTag ? `${projectRepositoryUrl}/releases/tag/${projectTag}` : "") const defaultPermissionLimits: PermissionLimits = { maxAttachmentMb: 25, maxMailboxCount: 9, smtpDailyLimit: 200, smtpMinuteLimit: 20, imapMinuteLimit: 200, pop3MinuteLimit: 150 } const defaultMailboxLimitOverride = 9 +const accountLoginName = (user: Pick) => user.loginName || user.email export function AdminPage() { const me = useMe() @@ -221,7 +222,8 @@ function UsersSection({ users, permissionGroups }: { users: AdminUser[]; permiss const canDelete = hasPermission(user, "admin.users.delete") const filteredUsers = users.filter((user) => { const keyword = query.trim().toLowerCase() - const matchesKeyword = !keyword || [user.email, user.displayName, ...(user.mailboxes || [])].some((value) => value.toLowerCase().includes(keyword)) + const loginName = accountLoginName(user) + const matchesKeyword = !keyword || [loginName, user.email, user.displayName, ...(user.mailboxes || [])].some((value) => value.toLowerCase().includes(keyword)) const matchesRole = roleFilter === "all" || user.role === roleFilter const matchesStatus = statusFilter === "all" || (statusFilter === "active" ? !user.disabled : user.disabled) return matchesKeyword && matchesRole && matchesStatus @@ -264,9 +266,9 @@ function UsersSection({ users, permissionGroups }: { users: AdminUser[]; permiss
{user.displayName}
-
{user.email}
+
{accountLoginName(user)}
- setPendingConfirm({ title: "删除账号?", description: `将删除 ${user.email} 及其关联数据。`, confirmText: "删除账号", onConfirm: () => remove.mutate(user.id) }) : undefined} /> + setPendingConfirm({ title: "删除账号?", description: `将删除 ${accountLoginName(user)} 及其关联数据。`, confirmText: "删除账号", onConfirm: () => remove.mutate(user.id) }) : undefined} />
@@ -286,14 +288,14 @@ function UsersSection({ users, permissionGroups }: { users: AdminUser[]; permiss
{user.displayName}
-
{user.email}
+
{accountLoginName(user)}
{user.disabled ? "停用" : "正常"} {new Date(user.createdAt).toLocaleDateString()} - setPendingConfirm({ title: "删除账号?", description: `将删除 ${user.email} 及其关联数据。`, confirmText: "删除账号", onConfirm: () => remove.mutate(user.id) }) : undefined} /> + setPendingConfirm({ title: "删除账号?", description: `将删除 ${accountLoginName(user)} 及其关联数据。`, confirmText: "删除账号", onConfirm: () => remove.mutate(user.id) }) : undefined} />
))} @@ -1714,7 +1716,7 @@ function invalidateAdmin(qc: ReturnType) { qc.invalidateQ function UserMailboxCell({ user }: { user: AdminUser }) { const { toast } = useToast() - const loginAddress = user.email + const loginAddress = accountLoginName(user) const mailboxes = user.mailboxes || [] const [mailboxQuery, setMailboxQuery] = React.useState("") const normalizedQuery = mailboxQuery.trim().toLowerCase() @@ -1880,7 +1882,7 @@ function CreateUserDialog({ permissionGroups }: { permissionGroups: PermissionGr const [permissionGroupIds, setPermissionGroupIds] = React.useState([]) const create = useMutation({ mutationFn: (form: FormData) => api.createUser({ - email: String(form.get("email") || ""), + loginName: String(form.get("loginName") || ""), displayName: String(form.get("displayName") || ""), password: String(form.get("password") || ""), role, @@ -1897,7 +1899,7 @@ function CreateUserDialog({ permissionGroups }: { permissionGroups: PermissionGr 创建账号
{ event.preventDefault(); create.mutate(new FormData(event.currentTarget)) }}> - +
@@ -1951,7 +1953,7 @@ function EditUserDialog({ user, permissionGroups, open, onOpenChange }: { user: 编辑账号 { e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}> - +
setRole(value as "admin" | "user")} items={[["user", "普通用户"], ["admin", "管理员"]]} disabled={user.protected} /> @@ -1968,7 +1970,7 @@ function EditUserDialog({ user, permissionGroups, open, onOpenChange }: { user: function ResetPasswordDialog({ user, open, onOpenChange }: { user: AdminUser; open: boolean; onOpenChange: (open: boolean) => void }) { const { toast } = useToast(); const mut = useMutation({ mutationFn: (form: FormData) => api.resetUserPassword(user.id, String(form.get("password") || "")), onSuccess: () => { onOpenChange(false); toast({ title: "密码已重置" }) }, onError: (e) => toast({ title: "重置失败", description: e.message }) }) - return 重置密码 { e.preventDefault(); mut.mutate(new FormData(e.currentTarget)); e.currentTarget.reset() }}> + return 重置密码
{ e.preventDefault(); mut.mutate(new FormData(e.currentTarget)); e.currentTarget.reset() }}>
} function EditMailboxDialog({ mailbox, users, open, onOpenChange }: { mailbox: MailboxType; users: AdminUser[]; open: boolean; onOpenChange: (open: boolean) => void }) { @@ -1987,8 +1989,8 @@ function CreateDomainDialog() { function CreateMailboxDialog({ domains, users }: { domains: Domain[]; users: AdminUser[] }) { const qc = useQueryClient(); const { toast } = useToast(); const [open, setOpen] = React.useState(false); const [domainId, setDomainId] = React.useState(""); const [role, setRole] = React.useState("user"); const [ownerMode, setOwnerMode] = React.useState("new"); const [userId, setUserId] = React.useState("") React.useEffect(() => { if (!domainId && domains[0]) setDomainId(domains[0].id); if (!userId && users[0]) setUserId(users[0].id) }, [domains, domainId, users, userId]) - const mut = useMutation({ mutationFn: (form: FormData) => api.createMailbox({ domainId, localPart: String(form.get("localPart")), displayName: String(form.get("displayName")), password: String(form.get("password")), quotaMb: Number(form.get("quotaMb") || 1024), role: role as "admin" | "user", ownerEmail: String(form.get("ownerEmail") || ""), userId: ownerMode === "existing" ? userId : "" }), onSuccess: () => { invalidateAdmin(qc); setOpen(false); toast({ title: "邮箱已创建" }) }, onError: (e) => toast({ title: "创建失败", description: e.message }) }) - return 创建邮箱
{ e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}>
{ownerMode === "existing" ? !u.disabled).map((u) => [u.id, u.email])} /> : }
+ const mut = useMutation({ mutationFn: (form: FormData) => api.createMailbox({ domainId, localPart: String(form.get("localPart")), displayName: String(form.get("displayName")), password: String(form.get("password")), quotaMb: Number(form.get("quotaMb") || 1024), role: role as "admin" | "user", ownerLoginName: String(form.get("ownerLoginName") || ""), userId: ownerMode === "existing" ? userId : "" }), onSuccess: () => { invalidateAdmin(qc); setOpen(false); toast({ title: "邮箱已创建" }) }, onError: (e) => toast({ title: "创建失败", description: e.message }) }) + return 创建邮箱
{ e.preventDefault(); mut.mutate(new FormData(e.currentTarget)) }}>
{ownerMode === "existing" ? !u.disabled).map((u) => [u.id, accountLoginName(u)])} /> : }
} function CreateAliasDialog({ domains }: { domains: Domain[] }) { diff --git a/apps/web/src/pages/login.tsx b/apps/web/src/pages/login.tsx index 2fe9c7d..5c64d98 100644 --- a/apps/web/src/pages/login.tsx +++ b/apps/web/src/pages/login.tsx @@ -21,7 +21,7 @@ export function LoginPage() { const login = useMutation({ mutationFn: (form: FormData) => challengeToken ? api.login({ challengeToken, twoFactorCode: String(form.get("twoFactorCode") || "") }) - : api.login({ email: String(form.get("email") || ""), password: String(form.get("password") || ""), turnstileToken }), + : api.login({ loginName: String(form.get("loginName") || ""), password: String(form.get("password") || ""), turnstileToken }), onSuccess: async (data) => { if (data.twoFactorRequired && data.challengeToken) { setChallengeToken(data.challengeToken) @@ -49,8 +49,8 @@ export function LoginPage() { {!challengeToken ? ( <>
- - + +
diff --git a/deploy/all-in-one/Dockerfile b/deploy/all-in-one/Dockerfile index 53063c4..c996e89 100644 --- a/deploy/all-in-one/Dockerfile +++ b/deploy/all-in-one/Dockerfile @@ -16,7 +16,7 @@ COPY pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/web/package.json apps/web/package.json RUN corepack enable && corepack prepare pnpm@10.28.2 --activate RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ - pnpm install --frozen-lockfile --filter lanqin-email-web... + pnpm install --frozen-lockfile --filter newszxcn-email-web... COPY apps/web apps/web ARG VITE_APP_VERSION="" ARG VITE_RELEASE_URL="" diff --git a/deploy/postfix/main.cf b/deploy/postfix/main.cf index 596115a..9f67603 100644 --- a/deploy/postfix/main.cf +++ b/deploy/postfix/main.cf @@ -13,7 +13,7 @@ virtual_alias_maps = sqlite:/etc/postfix/sqlite-aliases.cf virtual_transport = lmtp:inet:dovecot:24 virtual_mailbox_base = /var/mail/vhosts -smtpd_banner = $myhostname ESMTP LanQin Email +smtpd_banner = $myhostname ESMTP NewSzxcn Email smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination diff --git a/deploy/web.Dockerfile b/deploy/web.Dockerfile index 25e73da..d89b2a1 100644 --- a/deploy/web.Dockerfile +++ b/deploy/web.Dockerfile @@ -6,7 +6,7 @@ COPY pnpm-lock.yaml pnpm-workspace.yaml ./ COPY apps/web/package.json apps/web/package.json RUN corepack enable && corepack prepare pnpm@10.28.2 --activate RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ - pnpm install --frozen-lockfile --filter lanqin-email-web... + pnpm install --frozen-lockfile --filter newszxcn-email-web... COPY apps/web apps/web ARG VITE_APP_VERSION="" ARG VITE_RELEASE_URL=""