@podosoft/podokit 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +33 -2
  2. package/dist/add.d.ts +2 -0
  3. package/dist/add.js +5 -1
  4. package/dist/assemble.d.ts +14 -0
  5. package/dist/assemble.js +60 -0
  6. package/dist/create.d.ts +2 -0
  7. package/dist/create.js +7 -0
  8. package/dist/eject.d.ts +11 -0
  9. package/dist/eject.js +27 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +122 -1
  12. package/dist/inspect.d.ts +43 -0
  13. package/dist/inspect.js +82 -0
  14. package/dist/lockfile.d.ts +80 -0
  15. package/dist/lockfile.js +193 -0
  16. package/dist/templates/base/dot-env.example +11 -6
  17. package/dist/templates/fullstack-nest-svelte/README.md +2 -2
  18. package/dist/templates/fullstack-nest-svelte/apps/api/package.json +1 -0
  19. package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +6 -3
  20. package/dist/templates/fullstack-nest-svelte/apps/api/src/common/app-exception.ts +4 -12
  21. package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +1 -1
  22. package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +1 -1
  23. package/dist/templates/fullstack-nest-svelte/apps/web/package.json +2 -2
  24. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +1 -1
  25. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +1 -1
  26. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/index.ts +7 -0
  27. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
  28. package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +1 -1
  29. package/dist/templates/fullstack-nest-svelte/dot-env.example +10 -5
  30. package/dist/templates/fullstack-nest-svelte/infra/docker/docker-compose.yml +37 -0
  31. package/dist/templates/fullstack-nest-svelte/infra/docker/sms-sink.mjs +40 -0
  32. package/dist/templates/fullstack-nest-svelte/tests/README.md +1 -1
  33. package/dist/templates/fullstack-nest-svelte/tests/package.json +2 -1
  34. package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +1 -1
  35. package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +3 -0
  36. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +7 -1
  37. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +149 -0
  38. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-pagination.svelte +21 -22
  39. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +93 -0
  40. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +224 -8
  41. package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +22 -0
  42. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +19 -3
  43. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/accept-invitation/[id]/+page.svelte +36 -0
  44. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/+page.svelte +21 -1
  45. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +360 -49
  46. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.server.ts +2 -4
  47. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +82 -37
  48. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.server.ts +6 -0
  49. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +264 -0
  50. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.server.ts +3 -4
  51. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +79 -45
  52. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +26 -0
  53. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +322 -0
  54. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.server.ts +3 -5
  55. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +254 -108
  56. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/forgot-password/+page.svelte +8 -1
  57. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +14 -0
  58. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +127 -5
  59. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +18 -2
  60. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/verify-email/+page.svelte +63 -0
  61. package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +54 -0
  62. package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +51 -2
  63. package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +80 -0
  64. package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +264 -1
  65. package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +120 -0
  66. package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +10 -10
  67. package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +2 -2
  68. package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +49 -0
  69. package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +35 -0
  70. package/dist/templates/modules/admin-dashboard/files/tests/helpers/totp.ts +9 -0
  71. package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +1 -1
  72. package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +12 -1
  73. package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +77 -6
  74. package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +1 -1
  75. package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -0
  76. package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +60 -0
  77. package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +42 -0
  78. package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +1 -0
  79. package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +57 -0
  80. package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +55 -0
  81. package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +36 -0
  82. package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +9 -2
  83. package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +34 -0
  84. package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +32 -0
  85. package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +28 -12
  86. package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +35 -3
  87. package/dist/templates/modules/admin-dashboard/module.manifest.json +13 -7
  88. package/dist/templates/modules/api-key-auth/module.manifest.json +3 -3
  89. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +16 -0
  90. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +4 -0
  91. package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.interceptor.ts +2 -0
  92. package/dist/templates/modules/audit-log/module.manifest.json +15 -9
  93. package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +78 -13
  94. package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +2 -1
  95. package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +73 -0
  96. package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +197 -52
  97. package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +12 -0
  98. package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +56 -0
  99. package/dist/templates/modules/auth/files/apps/api/src/auth/org-permissions.ts +22 -0
  100. package/dist/templates/modules/auth/files/apps/api/src/auth/permissions.ts +23 -0
  101. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.controller.ts +33 -0
  102. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.entity.ts +26 -0
  103. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.module.ts +12 -0
  104. package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +118 -0
  105. package/dist/templates/modules/auth/files/apps/api/src/mail/mailer.ts +65 -0
  106. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +38 -0
  107. package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +27 -0
  108. package/dist/templates/modules/auth/files/apps/api/src/settings/app-setting.entity.ts +16 -0
  109. package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +25 -0
  110. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.module.ts +11 -0
  111. package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +57 -0
  112. package/dist/templates/modules/auth/files/apps/api/src/sms/sms.ts +23 -0
  113. package/dist/templates/modules/auth/module.manifest.json +54 -20
  114. package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +6 -3
  115. package/dist/templates/modules/bullmq/module.manifest.json +4 -4
  116. package/dist/templates/modules/file-upload/module.manifest.json +3 -3
  117. package/dist/templates/modules/job-progress/module.manifest.json +9 -9
  118. package/dist/templates/modules/logging/module.manifest.json +3 -3
  119. package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -3
  120. package/dist/templates/modules/object-storage-s3/module.manifest.json +5 -5
  121. package/dist/templates/modules/rate-limit/module.manifest.json +2 -2
  122. package/dist/templates/modules/redis/module.manifest.json +4 -4
  123. package/dist/templates/modules/sse/module.manifest.json +4 -4
  124. package/dist/templates/todo/README.md +2 -2
  125. package/dist/templates/todo/apps/api/package.json +1 -0
  126. package/dist/templates/todo/apps/api/src/app.module.ts +6 -3
  127. package/dist/templates/todo/apps/api/src/common/app-exception.ts +4 -12
  128. package/dist/templates/todo/apps/api/src/config/env.validation.ts +1 -1
  129. package/dist/templates/todo/apps/api/src/main.ts +1 -1
  130. package/dist/templates/todo/apps/web/package.json +2 -2
  131. package/dist/templates/todo/apps/web/src/lib/components/ui/switch/index.ts +7 -0
  132. package/dist/templates/todo/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
  133. package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +1 -1
  134. package/dist/templates/todo/dot-env.example +10 -5
  135. package/dist/templates/todo/infra/docker/docker-compose.yml +4 -0
  136. package/dist/templates/todo/tests/README.md +1 -1
  137. package/dist/templates/todo/tests/package.json +2 -1
  138. package/dist/templates/todo/tests/playwright.config.ts +1 -1
  139. package/dist/update.d.ts +49 -0
  140. package/dist/update.js +158 -0
  141. package/package.json +2 -2
@@ -12,15 +12,22 @@ const en = {
12
12
  common: { appName: "Admin" },
13
13
  landing: { title: "Welcome", subtitle: "This is your app's home page. Replace it with your own content — the admin area lives under /admin.", openAdmin: "Open admin" },
14
14
  language: { label: "Language" },
15
+ toolbar: { filter: "Filter", search: "Search", searchButton: "Search", all: "All" },
15
16
  theme: { toggle: "Toggle theme", light: "Light", dark: "Dark", system: "System" },
16
17
  auth: {
17
18
  signInTitle: "Sign in",
18
19
  signInDesc: "Enter your credentials to continue.",
19
20
  email: "Email",
21
+ emailOrUsername: "Email or username",
20
22
  password: "Password",
21
23
  forgot: "Forgot?",
22
24
  signIn: "Sign in",
23
25
  signingIn: "Signing in…",
26
+ magicLinkButton: "Email me a sign-in link",
27
+ passkeyButton: "Use a passkey",
28
+ magicLinkSent: "Check your email for a sign-in link.",
29
+ magicLinkNeedsEmail: "Enter your email first.",
30
+ otpButton: "Email me a code", otpCode: "One-time code", otpVerify: "Verify code",
24
31
  noAccount: "No account?",
25
32
  signUp: "Sign up",
26
33
  signInFailed: "Sign in failed",
@@ -44,10 +51,86 @@ const en = {
44
51
  updating: "Updating…",
45
52
  missingToken: "Missing or invalid reset token.",
46
53
  resetFailed: "Reset failed",
54
+ verifyTitle: "Verify your email",
55
+ verifyDesc: "We sent a verification link to {email}. Open it to activate your account.",
56
+ verifyDescNoEmail: "Enter your email to receive a new verification link.",
57
+ verifySent: "Verification email sent — check your inbox.",
58
+ verifyLinkError: "That verification link is invalid or has expired. Request a new one below.",
59
+ resendVerification: "Resend verification email",
60
+ emailNotVerified: "Verify your email before signing in.",
47
61
  },
48
- nav: { overview: "Overview", users: "Users", sessions: "Sessions", audit: "Audit log", account: "Account", signOut: "Sign out" },
49
- dashboard: { title: "Dashboard", welcome: "Welcome back, {name}.", yourRole: "Your role", signedInAs: "Signed in as" },
50
- audit: { title: "Audit log", when: "When", actor: "Actor", action: "Action", target: "Target", ip: "IP", empty: "No audit entries.", loadFailed: "Failed to load audit log", total: "{count} entries" },
62
+ nav: { overview: "Overview", users: "Users", sessions: "Sessions", organizations: "Organizations", audit: "Audit log", account: "Account", settings: "Settings", signOut: "Sign out" },
63
+ passkeys: {
64
+ title: "Passkeys", subtitle: "Passwordless sign-in with your device (Face ID, fingerprint, security key).",
65
+ name: "Name", namePlaceholder: "e.g. MacBook", add: "Add passkey", remove: "Remove", untitled: "Passkey",
66
+ added: "Passkey added", addFailed: "Failed to add passkey", removed: "Passkey removed", removeFailed: "Failed to remove passkey",
67
+ empty: "No passkeys yet.", created: "Created", total: "{count} passkeys",
68
+ },
69
+ acceptInvitation: {
70
+ title: "Organization invitation", pending: "Accepting your invitation…",
71
+ ok: "You have joined the organization.", error: "This invitation is invalid or has expired.",
72
+ goToOrgs: "Go to organizations",
73
+ },
74
+ organizations: {
75
+ title: "Organizations", subtitle: "Multi-tenant teams. Create organizations and invite members.",
76
+ create: "New organization", createTitle: "Create organization", name: "Name", slug: "Slug",
77
+ created: "Created", delete: "Delete", cancel: "Cancel", empty: "No organizations yet.", total: "{count} organizations",
78
+ submit: "Create", createdMsg: "Organization created", deletedMsg: "Organization deleted", actionFailed: "Action failed",
79
+ manage: "Manage", membersDesc: "Members and pending invitations.", members: "Members", pending: "Pending invitations",
80
+ parent: "Parent organization", noParent: "None (top level)", managers: "Managers", managersHint: "Existing users to add as managers (any number).",
81
+ inviteEmail: "Invite by email", invite: "Invite", invited: "Invitation sent", remove: "Remove", cancelInvite: "Cancel",
82
+ },
83
+ consent: {
84
+ title: "Authorize access", subtitle: "An application is requesting access to your account.",
85
+ requests: "It will be able to access:", allow: "Allow", deny: "Deny",
86
+ },
87
+ apiKeys: {
88
+ title: "API keys", subtitle: "Personal keys for programmatic access. Send as an x-api-key header.",
89
+ name: "Name", namePlaceholder: "e.g. CI pipeline", create: "Create key",
90
+ keyColumn: "Key", created: "Created", untitled: "Untitled", revoke: "Revoke",
91
+ revoked: "Key revoked", revokeFailed: "Failed to revoke key", empty: "No API keys yet.", total: "{count} API keys",
92
+ createdTitle: "API key created", createdDesc: "Copy it now — you will not be able to see it again.",
93
+ copy: "Copy", copied: "Copied to clipboard", done: "Done",
94
+ },
95
+ settings: {
96
+ title: "Settings", subtitle: "Which authentication features are enabled, and how they are configured.",
97
+ authFeaturesTitle: "Authentication features", authFeaturesSubtitle: "Toggle sign-in methods and security features. Changes apply immediately.",
98
+ serverConfigTitle: "Server configuration", serverConfigSubtitle: "Set through environment variables and applied at startup (read-only).",
99
+ enabled: "Enabled", disabled: "Disabled", configuredVia: "Configured via {env}", manage: "Manage",
100
+ saved: "Setting updated", saveFailed: "Failed to update setting",
101
+ emailPassword: "Email & password", emailPasswordDesc: "Sign in with an email address and password.",
102
+ emailVerification: "Email verification", emailVerificationDesc: "Require new sign-ups to confirm their address.",
103
+ magicLink: "Magic link", magicLinkDesc: "Passwordless sign-in via an emailed link.",
104
+ emailOtp: "Email one-time code", emailOtpDesc: "Passwordless sign-in with an emailed code.",
105
+ username: "Username sign-in", usernameDesc: "Let users sign in with a username instead of an email.",
106
+ multiSession: "Multi-account sessions", multiSessionDesc: "Let one browser stay signed into several accounts and switch between them.",
107
+ phoneNumber: "Phone number", phoneNumberDesc: "Register and verify a phone number via SMS (configure a provider for real delivery).",
108
+ apiKey: "API keys", apiKeyDesc: "Let users create personal API keys for programmatic access.",
109
+ passkey: "Passkeys", passkeyDesc: "Let users register passkeys for passwordless sign-in.",
110
+ organization: "Organizations", organizationDesc: "Multi-tenant organizations with members and email invitations.",
111
+ oidcProvider: "OIDC provider", oidcProviderDesc: "Act as an identity provider so other apps can sign in with this one.",
112
+ twoFactor: "Two-factor authentication", twoFactorDesc: "TOTP second factor with backup codes.",
113
+ socialProviders: "Social providers", socialProvidersDesc: "OAuth sign-in (Google, GitHub).",
114
+ breachCheck: "Breached-password check", breachCheckDesc: "Reject passwords found in known breaches (Have I Been Pwned).",
115
+ accountDeletion: "Account deletion", accountDeletionDesc: "Let users delete their own account.",
116
+ auditLog: "Audit log", auditLogDesc: "Record security-relevant actions.",
117
+ signInTitle: "Sign-in methods", signInSubtitle: "How users authenticate. Toggle methods on/off; social providers open a popup for credentials.",
118
+ emailTitle: "Email", emailSubtitle: "Outgoing mail (SMTP) and the email-verification policy.",
119
+ securityTitle: "Security & policy", securitySubtitle: "Second factor, breached-password rejection, account deletion, and audit logging.",
120
+ advancedTitle: "Advanced", advancedSubtitle: "API keys, organizations, and acting as an OIDC identity provider.",
121
+ socialTitle: "Social login", socialSubtitle: "Paste OAuth credentials to enable sign-in with a provider. Applied immediately — no restart. Secrets are stored encrypted.",
122
+ smtpTitle: "SMTP (email delivery)", smtpSubtitle: "Outgoing mail for sign-in links, verification, and invitations. Applied immediately.",
123
+ google: "Google", github: "GitHub",
124
+ clientId: "Client ID", clientSecret: "Client secret", redirectUri: "Redirect URI", secretSet: "•••••••• (saved — leave blank to keep)",
125
+ save: "Save", cancel: "Cancel", configure: "Configure", notConfigured: "Not configured", configuredOff: "Configured (off)",
126
+ socialDialogDesc: "Paste the OAuth client ID and secret from the provider's console, then enable it. Applied immediately.",
127
+ smtpEnabled: "Enabled", smtpHost: "Host", smtpPort: "Port", smtpUser: "Username", smtpPass: "Password", smtpFrom: "From address", smtpSecure: "TLS (secure)",
128
+ addProvider: "Add provider", provider: "Provider", remove: "Remove", smtpConfigure: "SMTP settings",
129
+ edit: "Edit", close: "Close", back: "Back", noProviders: "No providers added yet.",
130
+ socialCardDesc: "Sign in with Google, GitHub, Apple and other OAuth providers.",
131
+ },
132
+ dashboard: { title: "Dashboard", welcome: "Welcome back, {name}.", yourRole: "Your role", signedInAs: "Signed in as", unverifiedUsers: "Unverified users" },
133
+ audit: { title: "Audit log", when: "When", actor: "Actor", action: "Action", target: "Target", ip: "IP", empty: "No audit entries.", loadFailed: "Failed to load audit log", total: "{count} entries", searchField: "Actor or target" },
51
134
  users: {
52
135
  title: "Users",
53
136
  search: "Search by email…",
@@ -75,6 +158,10 @@ const en = {
75
158
  password: "Password",
76
159
  create: "Create",
77
160
  cancel: "Cancel",
161
+ close: "Close",
162
+ roleAdmin: "Admin",
163
+ roleUser: "User",
164
+ roleModerator: "Moderator",
78
165
  userCreated: "User created",
79
166
  setPassword: "Set password",
80
167
  newPassword: "New password",
@@ -94,10 +181,22 @@ const en = {
94
181
  banTitle: "Ban user",
95
182
  banReason: "Reason (optional)",
96
183
  banExpiryDays: "Expires in (days, optional)",
184
+ banExpiry: "Ban expires",
185
+ banPermanent: "Permanent ban",
186
+ joined: "Joined",
97
187
  stopImpersonating: "Stop impersonating",
98
188
  impersonatingAs: "Impersonating {email}",
99
189
  manage: "Manage",
100
190
  revokeSession: "Revoke",
191
+ verified: "Verified",
192
+ unverified: "Unverified",
193
+ sendVerification: "Send verification email",
194
+ verificationSent: "Verification email sent",
195
+ markVerified: "Mark verified",
196
+ markedVerified: "Marked as verified",
197
+ sendResetEmail: "Send password reset email",
198
+ resetEmailSent: "Password reset email sent",
199
+ sendVerificationOnCreate: "Send verification email",
101
200
  },
102
201
  sessions: {
103
202
  title: "Active sessions",
@@ -113,6 +212,7 @@ const en = {
113
212
  status: "Status",
114
213
  current: "Current",
115
214
  signOutOthers: "Sign out other sessions",
215
+ accountsTitle: "Signed-in accounts", switchAccount: "Switch", switchFailed: "Failed to switch account",
116
216
  othersRevoked: "Signed out other sessions",
117
217
  total: "{count} sessions",
118
218
  },
@@ -122,6 +222,8 @@ const en = {
122
222
  device: "Device",
123
223
  ip: "IP",
124
224
  since: "Since",
225
+ expires: "Expires",
226
+ impersonated: "Impersonated",
125
227
  revoke: "Revoke session",
126
228
  revokeAll: "Revoke all for user",
127
229
  unknown: "Unknown",
@@ -135,6 +237,8 @@ const en = {
135
237
  title: "Account",
136
238
  profile: "Profile",
137
239
  name: "Name",
240
+ username: "Username",
241
+ phone: "Phone number", phoneCode: "Verification code", phoneSendCode: "Send code", phoneVerify: "Verify", phoneCodeSent: "Code sent", phoneVerified: "Phone verified",
138
242
  email: "Email",
139
243
  role: "Role",
140
244
  changePassword: "Change password",
@@ -144,7 +248,7 @@ const en = {
144
248
  updating: "Updating…",
145
249
  changed: "Password changed",
146
250
  changeFailed: "Failed to change password",
147
- nav: { profile: "Profile", security: "Security", connected: "Connected", sessions: "Sessions", danger: "Danger zone" },
251
+ nav: { profile: "Profile", security: "Security", connected: "Connected", sessions: "Sessions", apiKeys: "API keys", danger: "Danger zone" },
148
252
  twoFactorTitle: "Two-factor authentication",
149
253
  twoFactorDesc: "Add a one-time code from your authenticator app at sign-in.",
150
254
  twoFactorOn: "Enabled",
@@ -174,6 +278,10 @@ const en = {
174
278
  save: "Save changes",
175
279
  saved: "Profile updated",
176
280
  saveFailed: "Failed to save profile",
281
+ saving: "Saving…",
282
+ changeEmail: "Change email",
283
+ emailChanged: "Email address updated",
284
+ emailChangePending: "Check your current inbox to approve the change",
177
285
  verified: "Verified",
178
286
  unverified: "Unverified",
179
287
  },
@@ -185,15 +293,22 @@ const ko: Messages = {
185
293
  common: { appName: "관리자" },
186
294
  landing: { title: "환영합니다", subtitle: "앱의 홈 페이지입니다. 원하는 내용으로 교체하세요 — 관리자 기능은 /admin 에 있습니다.", openAdmin: "관리자 열기" },
187
295
  language: { label: "언어" },
296
+ toolbar: { filter: "필터", search: "검색", searchButton: "검색", all: "전체" },
188
297
  theme: { toggle: "테마 전환", light: "라이트", dark: "다크", system: "시스템" },
189
298
  auth: {
190
299
  signInTitle: "로그인",
191
300
  signInDesc: "계속하려면 로그인하세요.",
192
301
  email: "이메일",
302
+ emailOrUsername: "이메일 또는 사용자명",
193
303
  password: "비밀번호",
194
304
  forgot: "비밀번호 찾기",
195
305
  signIn: "로그인",
196
306
  signingIn: "로그인 중…",
307
+ magicLinkButton: "로그인 링크 이메일로 받기",
308
+ passkeyButton: "패스키 사용",
309
+ magicLinkSent: "이메일의 로그인 링크를 확인하세요.",
310
+ magicLinkNeedsEmail: "먼저 이메일을 입력하세요.",
311
+ otpButton: "코드 이메일로 받기", otpCode: "일회용 코드", otpVerify: "코드 확인",
197
312
  noAccount: "계정이 없으신가요?",
198
313
  signUp: "회원가입",
199
314
  signInFailed: "로그인에 실패했습니다",
@@ -216,11 +331,87 @@ const ko: Messages = {
216
331
  updatePassword: "비밀번호 변경",
217
332
  updating: "변경 중…",
218
333
  missingToken: "재설정 토큰이 없거나 유효하지 않습니다.",
334
+ verifyTitle: "이메일 인증",
335
+ verifyDesc: "{email} 주소로 인증 링크를 보냈습니다. 링크를 열어 계정을 활성화하세요.",
336
+ verifyDescNoEmail: "새 인증 링크를 받을 이메일을 입력하세요.",
337
+ verifySent: "인증 메일을 보냈습니다 — 받은편지함을 확인하세요.",
338
+ verifyLinkError: "인증 링크가 유효하지 않거나 만료되었습니다. 아래에서 다시 요청하세요.",
339
+ resendVerification: "인증 메일 다시 보내기",
340
+ emailNotVerified: "로그인 전에 이메일을 인증해 주세요.",
219
341
  resetFailed: "재설정에 실패했습니다",
220
342
  },
221
- nav: { overview: "개요", users: "회원", sessions: "세션", audit: "감사 로그", account: "계정", signOut: "로그아웃" },
222
- dashboard: { title: "대시보드", welcome: "{name}님, 환영합니다.", yourRole: "내 권한", signedInAs: "로그인 계정" },
223
- audit: { title: "감사 로그", when: "시각", actor: "수행자", action: "동작", target: "대상", ip: "IP", empty: "감사 기록이 없습니다.", loadFailed: "감사 로그를 불러오지 못했습니다", total: "항목 {count}개" },
343
+ nav: { overview: "개요", users: "회원", sessions: "세션", organizations: "조직", audit: "감사 로그", account: "계정", settings: "설정", signOut: "로그아웃" },
344
+ passkeys: {
345
+ title: "패스키", subtitle: "기기로 비밀번호 없이 로그인(Face ID, 지문, 보안 키).",
346
+ name: "이름", namePlaceholder: "예: MacBook", add: "패스키 추가", remove: "삭제", untitled: "패스키",
347
+ added: "패스키를 추가했습니다", addFailed: "패스키 추가에 실패했습니다", removed: "패스키를 삭제했습니다", removeFailed: "패스키 삭제에 실패했습니다",
348
+ empty: "아직 패스키가 없습니다.", created: "생성일", total: "패스키 {count}개",
349
+ },
350
+ acceptInvitation: {
351
+ title: "조직 초대", pending: "초대를 수락하는 중…",
352
+ ok: "조직에 합류했습니다.", error: "유효하지 않거나 만료된 초대입니다.",
353
+ goToOrgs: "조직으로 이동",
354
+ },
355
+ organizations: {
356
+ title: "조직", subtitle: "멀티테넌트 팀. 조직을 만들고 멤버를 초대합니다.",
357
+ create: "새 조직", createTitle: "조직 만들기", name: "이름", slug: "슬러그",
358
+ created: "생성일", delete: "삭제", cancel: "취소", empty: "아직 조직이 없습니다.", total: "조직 {count}개",
359
+ submit: "만들기", createdMsg: "조직을 만들었습니다", deletedMsg: "조직을 삭제했습니다", actionFailed: "작업에 실패했습니다",
360
+ manage: "관리", membersDesc: "멤버와 대기 중인 초대.", members: "멤버", pending: "대기 중 초대",
361
+ parent: "상위 조직", noParent: "없음(최상위)", managers: "매니저", managersHint: "매니저로 추가할 기존 사용자(여러 명 가능).",
362
+ inviteEmail: "이메일로 초대", invite: "초대", invited: "초대를 보냈습니다", remove: "제거", cancelInvite: "취소",
363
+ },
364
+ consent: {
365
+ title: "접근 승인", subtitle: "한 애플리케이션이 계정 접근을 요청합니다.",
366
+ requests: "접근 가능 범위:", allow: "허용", deny: "거부",
367
+ },
368
+ apiKeys: {
369
+ title: "API 키", subtitle: "프로그래밍 방식 접근용 개인 키. x-api-key 헤더로 전송합니다.",
370
+ name: "이름", namePlaceholder: "예: CI 파이프라인", create: "키 생성",
371
+ keyColumn: "키", created: "생성일", untitled: "제목 없음", revoke: "폐기",
372
+ revoked: "키를 폐기했습니다", revokeFailed: "키 폐기에 실패했습니다", empty: "아직 API 키가 없습니다.", total: "API 키 {count}개",
373
+ createdTitle: "API 키 생성됨", createdDesc: "지금 복사하세요 — 다시 볼 수 없습니다.",
374
+ copy: "복사", copied: "클립보드에 복사됨", done: "완료",
375
+ },
376
+ settings: {
377
+ title: "설정", subtitle: "활성화된 인증 기능과 구성 방법입니다.",
378
+ authFeaturesTitle: "인증 기능", authFeaturesSubtitle: "로그인 수단과 보안 기능을 켜고 끕니다. 변경은 즉시 적용됩니다.",
379
+ serverConfigTitle: "서버 구성", serverConfigSubtitle: "환경변수로 설정되어 시작 시 적용됩니다(읽기 전용).",
380
+ enabled: "활성", disabled: "비활성", configuredVia: "{env} 로 구성", manage: "관리",
381
+ saved: "설정을 변경했습니다", saveFailed: "설정 변경에 실패했습니다",
382
+ emailPassword: "이메일·비밀번호", emailPasswordDesc: "이메일과 비밀번호로 로그인.",
383
+ emailVerification: "이메일 인증", emailVerificationDesc: "신규 가입 시 이메일 확인 요구.",
384
+ magicLink: "매직 링크", magicLinkDesc: "이메일 링크로 비밀번호 없이 로그인.",
385
+ emailOtp: "이메일 일회용 코드", emailOtpDesc: "이메일로 받은 코드로 비밀번호 없이 로그인.",
386
+ username: "사용자명 로그인", usernameDesc: "이메일 대신 사용자명으로 로그인.",
387
+ multiSession: "다중 계정 세션", multiSessionDesc: "한 브라우저에서 여러 계정에 로그인하고 전환.",
388
+ phoneNumber: "전화번호", phoneNumberDesc: "SMS로 전화번호 등록·인증(실발송은 프로바이더 연동 필요).",
389
+ apiKey: "API 키", apiKeyDesc: "프로그래밍 방식 접근을 위한 개인 API 키 발급을 허용.",
390
+ passkey: "패스키", passkeyDesc: "비밀번호 없는 로그인을 위한 패스키 등록을 허용.",
391
+ organization: "조직", organizationDesc: "멤버와 이메일 초대를 갖춘 멀티테넌트 조직.",
392
+ oidcProvider: "OIDC 제공자", oidcProviderDesc: "다른 앱이 이 앱으로 로그인하도록 신원 제공자 역할.",
393
+ twoFactor: "2단계 인증", twoFactorDesc: "TOTP 2차 인증 + 백업 코드.",
394
+ socialProviders: "소셜 로그인", socialProvidersDesc: "OAuth 로그인 (Google, GitHub).",
395
+ breachCheck: "유출 비밀번호 검사", breachCheckDesc: "유출된 비밀번호 거부 (Have I Been Pwned).",
396
+ accountDeletion: "계정 삭제", accountDeletionDesc: "사용자가 자신의 계정을 삭제.",
397
+ auditLog: "감사 로그", auditLogDesc: "보안 관련 동작 기록.",
398
+ signInTitle: "로그인 수단", signInSubtitle: "사용자가 인증하는 방법. 각 수단을 켜고 끄며, 소셜 로그인은 팝업에서 자격증명을 입력합니다.",
399
+ emailTitle: "이메일", emailSubtitle: "메일 발송(SMTP) 설정과 이메일 인증 정책.",
400
+ securityTitle: "보안·정책", securitySubtitle: "2단계 인증, 유출 비밀번호 거부, 계정 삭제, 감사 로그.",
401
+ advancedTitle: "고급", advancedSubtitle: "API 키, 조직, OIDC 공급자(자체 IdP) 역할.",
402
+ socialTitle: "소셜 로그인", socialSubtitle: "OAuth 자격증명을 붙여넣으면 해당 provider 로그인이 켜집니다. 재시작 없이 즉시 적용되며, 비밀은 암호화 저장됩니다.",
403
+ smtpTitle: "SMTP(이메일 발송)", smtpSubtitle: "로그인 링크·인증·초대 발송용. 즉시 적용됩니다.",
404
+ google: "Google", github: "GitHub",
405
+ clientId: "Client ID", clientSecret: "Client secret", redirectUri: "Redirect URI", secretSet: "•••••••• (저장됨 — 비우면 유지)",
406
+ save: "저장", cancel: "취소", configure: "설정", notConfigured: "미설정", configuredOff: "설정됨(꺼짐)",
407
+ socialDialogDesc: "provider 콘솔의 OAuth client ID와 secret을 붙여넣고 사용을 켜세요. 즉시 적용됩니다.",
408
+ smtpEnabled: "사용", smtpHost: "호스트", smtpPort: "포트", smtpUser: "사용자", smtpPass: "비밀번호", smtpFrom: "발신 주소", smtpSecure: "TLS(보안)",
409
+ addProvider: "공급자 추가", provider: "공급자", remove: "삭제", smtpConfigure: "SMTP 설정",
410
+ edit: "편집", close: "닫기", back: "뒤로", noProviders: "아직 추가된 공급자가 없습니다.",
411
+ socialCardDesc: "Google, GitHub, Apple 등 OAuth 공급자로 로그인.",
412
+ },
413
+ dashboard: { title: "대시보드", welcome: "{name}님, 환영합니다.", yourRole: "내 권한", signedInAs: "로그인 계정", unverifiedUsers: "미인증 사용자" },
414
+ audit: { title: "감사 로그", when: "시각", actor: "수행자", action: "동작", target: "대상", ip: "IP", empty: "감사 기록이 없습니다.", loadFailed: "감사 로그를 불러오지 못했습니다", total: "항목 {count}개", searchField: "수행자·대상" },
224
415
  users: {
225
416
  title: "회원",
226
417
  search: "이메일로 검색…",
@@ -248,6 +439,10 @@ const ko: Messages = {
248
439
  password: "비밀번호",
249
440
  create: "생성",
250
441
  cancel: "취소",
442
+ close: "닫기",
443
+ roleAdmin: "관리자",
444
+ roleUser: "사용자",
445
+ roleModerator: "중재자",
251
446
  userCreated: "사용자를 생성했습니다",
252
447
  setPassword: "비밀번호 설정",
253
448
  newPassword: "새 비밀번호",
@@ -267,10 +462,22 @@ const ko: Messages = {
267
462
  banTitle: "사용자 차단",
268
463
  banReason: "사유 (선택)",
269
464
  banExpiryDays: "만료 (일, 선택)",
465
+ banExpiry: "차단 만료",
466
+ banPermanent: "영구 차단",
467
+ joined: "가입일",
270
468
  stopImpersonating: "대리 로그인 종료",
271
469
  impersonatingAs: "{email} 사용자로 대리 로그인 중",
272
470
  manage: "관리",
273
471
  revokeSession: "종료",
472
+ verified: "인증됨",
473
+ unverified: "미인증",
474
+ sendVerification: "인증 메일 보내기",
475
+ verificationSent: "인증 메일을 보냈습니다",
476
+ markVerified: "인증됨으로 표시",
477
+ markedVerified: "인증 처리했습니다",
478
+ sendResetEmail: "비밀번호 재설정 메일 보내기",
479
+ resetEmailSent: "재설정 메일을 보냈습니다",
480
+ sendVerificationOnCreate: "인증 메일 보내기",
274
481
  },
275
482
  sessions: {
276
483
  title: "활성 세션",
@@ -286,6 +493,7 @@ const ko: Messages = {
286
493
  status: "상태",
287
494
  current: "현재",
288
495
  signOutOthers: "다른 세션 모두 종료",
496
+ accountsTitle: "로그인된 계정", switchAccount: "전환", switchFailed: "계정 전환에 실패했습니다",
289
497
  othersRevoked: "다른 세션을 모두 종료했습니다",
290
498
  total: "세션 {count}개",
291
499
  },
@@ -295,6 +503,8 @@ const ko: Messages = {
295
503
  device: "기기",
296
504
  ip: "IP",
297
505
  since: "시작",
506
+ expires: "만료",
507
+ impersonated: "임퍼스네이트",
298
508
  revoke: "세션 종료",
299
509
  revokeAll: "사용자 세션 모두 종료",
300
510
  unknown: "알 수 없음",
@@ -308,6 +518,8 @@ const ko: Messages = {
308
518
  title: "계정",
309
519
  profile: "프로필",
310
520
  name: "이름",
521
+ username: "사용자명",
522
+ phone: "전화번호", phoneCode: "인증 코드", phoneSendCode: "코드 전송", phoneVerify: "인증", phoneCodeSent: "코드를 전송했습니다", phoneVerified: "전화번호 인증됨",
311
523
  email: "이메일",
312
524
  role: "권한",
313
525
  changePassword: "비밀번호 변경",
@@ -317,7 +529,7 @@ const ko: Messages = {
317
529
  updating: "변경 중…",
318
530
  changed: "비밀번호를 변경했습니다",
319
531
  changeFailed: "비밀번호 변경에 실패했습니다",
320
- nav: { profile: "프로필", security: "보안", connected: "연결", sessions: "세션", danger: "위험 영역" },
532
+ nav: { profile: "프로필", security: "보안", connected: "연결", sessions: "세션", apiKeys: "API 키", danger: "위험 영역" },
321
533
  twoFactorTitle: "2단계 인증",
322
534
  twoFactorDesc: "로그인 시 인증 앱의 일회용 코드를 추가로 요구합니다.",
323
535
  twoFactorOn: "사용 중",
@@ -347,6 +559,10 @@ const ko: Messages = {
347
559
  save: "변경 저장",
348
560
  saved: "프로필을 저장했습니다",
349
561
  saveFailed: "프로필 저장에 실패했습니다",
562
+ saving: "저장 중…",
563
+ changeEmail: "이메일 변경",
564
+ emailChanged: "이메일 주소를 변경했습니다",
565
+ emailChangePending: "현재 메일함에서 변경을 승인해 주세요",
350
566
  verified: "인증됨",
351
567
  unverified: "미인증",
352
568
  },
@@ -0,0 +1,22 @@
1
+ import { error } from "@sveltejs/kit";
2
+
3
+ /** Roles that may reach the admin console. Kept as a set so custom roles can be
4
+ * added here (and, later, replaced by a permission check) without touching every
5
+ * page loader. better-auth stores a user's role(s) as a comma-separated string. */
6
+ const ADMIN_ROLES = ["admin"];
7
+
8
+ type WithRole = { role?: string | null } | null | undefined;
9
+
10
+ /** True when the user holds at least one admin role. */
11
+ export function isAdmin(user: WithRole): boolean {
12
+ const roles = (user?.role ?? "")
13
+ .split(",")
14
+ .map((r) => r.trim())
15
+ .filter(Boolean);
16
+ return roles.some((r) => ADMIN_ROLES.includes(r));
17
+ }
18
+
19
+ /** Guard an admin-only loader: throws 403 unless the user holds an admin role. */
20
+ export function requireAdmin(user: WithRole): void {
21
+ if (!isAdmin(user)) error(403, "Admins only");
22
+ }
@@ -1,9 +1,25 @@
1
1
  import type { LayoutServerLoad } from "./$types";
2
-
3
- type Capabilities = { twoFactor: boolean; providers: string[]; deleteAccount: boolean; auditLog: boolean };
2
+ import type { Capabilities } from "@podosoft/podokit-api-client";
4
3
 
5
4
  export const load: LayoutServerLoad = async ({ locals, fetch }) => {
6
- let capabilities: Capabilities = { twoFactor: false, providers: [], deleteAccount: false, auditLog: false };
5
+ let capabilities: Capabilities = {
6
+ twoFactor: false,
7
+ providers: [],
8
+ deleteAccount: false,
9
+ auditLog: false,
10
+ emailVerification: false,
11
+ passwordBreachCheck: false,
12
+ magicLink: false,
13
+ emailOtp: false,
14
+ username: false,
15
+ multiSession: false,
16
+ phoneNumber: false,
17
+ apiKey: false,
18
+ passkey: false,
19
+ organization: false,
20
+ oidcProvider: false,
21
+ roles: ["admin", "user"],
22
+ };
7
23
  try {
8
24
  const res = await fetch("/api/account/capabilities");
9
25
  if (res.ok) capabilities = (await res.json()) as Capabilities;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { page } from "$app/state";
3
+ import { goto } from "$app/navigation";
4
+ import { Button } from "$lib/components/ui/button";
5
+ import * as Card from "$lib/components/ui/card";
6
+ import { api } from "$lib/api";
7
+ import { getI18n } from "$lib/i18n";
8
+
9
+ const i18n = getI18n();
10
+ let status = $state<"pending" | "ok" | "error">("pending");
11
+
12
+ async function accept(): Promise<void> {
13
+ const { error } = await api.auth.organization.acceptInvitation({ invitationId: page.params.id ?? "" });
14
+ status = error ? "error" : "ok";
15
+ }
16
+
17
+ $effect(() => {
18
+ void accept();
19
+ });
20
+ </script>
21
+
22
+ <div class="mx-auto flex min-h-[60vh] max-w-md items-center">
23
+ <Card.Root class="w-full">
24
+ <Card.Header>
25
+ <Card.Title>{i18n.t.acceptInvitation.title}</Card.Title>
26
+ <Card.Description>
27
+ {#if status === "pending"}{i18n.t.acceptInvitation.pending}
28
+ {:else if status === "ok"}{i18n.t.acceptInvitation.ok}
29
+ {:else}{i18n.t.acceptInvitation.error}{/if}
30
+ </Card.Description>
31
+ </Card.Header>
32
+ <Card.Content>
33
+ <Button onclick={() => goto("/admin/organizations")}>{i18n.t.acceptInvitation.goToOrgs}</Button>
34
+ </Card.Content>
35
+ </Card.Root>
36
+ </div>
@@ -1,10 +1,24 @@
1
1
  <script lang="ts">
2
2
  import * as Card from "$lib/components/ui/card";
3
+ import { api } from "$lib/api";
3
4
  import { getI18n, fmt } from "$lib/i18n";
4
5
  import type { SessionUser } from "../../../app.d.ts";
5
6
 
6
- let { data }: { data: { user: SessionUser } } = $props();
7
+ type Capabilities = { emailVerification: boolean };
8
+ let { data }: { data: { user: SessionUser; capabilities: Capabilities } } = $props();
7
9
  const i18n = getI18n();
10
+
11
+ // Unverified-user count — only when email verification is enabled.
12
+ let unverifiedCount = $state<number | null>(null);
13
+ $effect(() => {
14
+ if (!data.capabilities.emailVerification) return;
15
+ void (async () => {
16
+ const { data: res } = await api.auth.admin.listUsers({
17
+ query: { filterField: "emailVerified", filterOperator: "eq", filterValue: false, limit: 1 },
18
+ });
19
+ unverifiedCount = res?.total ?? 0;
20
+ })();
21
+ });
8
22
  </script>
9
23
 
10
24
  <div class="flex flex-col gap-6">
@@ -21,5 +35,11 @@
21
35
  <Card.Header><Card.Title>{i18n.t.dashboard.signedInAs}</Card.Title></Card.Header>
22
36
  <Card.Content class="text-muted-foreground truncate text-sm">{data.user.email}</Card.Content>
23
37
  </Card.Root>
38
+ {#if data.capabilities.emailVerification}
39
+ <Card.Root>
40
+ <Card.Header><Card.Title>{i18n.t.dashboard.unverifiedUsers}</Card.Title></Card.Header>
41
+ <Card.Content class="text-2xl font-semibold">{unverifiedCount ?? "—"}</Card.Content>
42
+ </Card.Root>
43
+ {/if}
24
44
  </div>
25
45
  </div>