@webx-ui/module-auth 0.2.1 → 0.3.0
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.
- package/dist/AdminDialog.d.ts +24 -0
- package/dist/AdminList.d.ts +62 -0
- package/dist/AdminPicker.d.ts +104 -0
- package/dist/AdminsPage.d.ts +104 -0
- package/dist/admins.d.ts +18 -0
- package/dist/i18n.d.ts +8 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +906 -203
- package/dist/index.js.map +1 -1
- package/dist/module.d.ts +22 -0
- package/dist/selectAdmin.d.ts +27 -0
- package/dist/style.css +1 -1
- package/dist/types.d.ts +70 -0
- package/package.json +3 -3
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/session.ts","../src/LoginCard.vue","../src/messages.ts","../src/UserMenu.vue","../src/index.ts"],"sourcesContent":["import { inject, type App, type InjectionKey } from 'vue'\nimport type { AdminContext, AdminUser } from '@webx-ui/admin'\n\nexport interface Credentials {\n email: string\n password: string\n remember?: boolean\n}\n\nexport interface AuthSession {\n /** Who is signed in, or null. A 401 is an answer, not a failure. */\n me(): Promise<AdminUser | null>\n /** Signs in and tells the panel to load itself. Throws `HttpError` on a refusal. */\n login(credentials: Credentials): Promise<AdminUser>\n logout(): Promise<void>\n /**\n * Change the language this administrator reads the panel in.\n *\n * Stored on the person rather than in this browser, so it follows them to the next machine\n * and so the server writes its own messages — a 422 under a field — in the same language as\n * the label above it.\n */\n setLocale(code: string): Promise<void>\n}\n\nexport const authKey: InjectionKey<AuthSession> = Symbol('webx-auth')\n\nexport function useAuth(): AuthSession {\n const session = inject(authKey, null)\n\n if (session === null) {\n throw new Error('useAuth() was called outside a panel that installed the auth plugin.')\n }\n\n return session\n}\n\n/**\n * The three calls `webx-ui/module-auth` answers, and nothing else.\n *\n * The session lives in a cookie, so there is nothing to keep here: the source of truth is the\n * server, and the panel asks it rather than remembering.\n */\nexport function createAuthSession(admin: AdminContext): AuthSession {\n const base = `${admin.apiPath}/auth`\n\n return {\n async me() {\n try {\n const body = await admin.http.get<{ data: AdminUser | null }>(`${base}/me`)\n\n return body.data\n } catch (error) {\n if (isUnauthenticated(error)) {\n return null\n }\n\n throw error\n }\n },\n\n async login(credentials) {\n const body = await admin.http.post<{ data: AdminUser }>(`${base}/login`, {\n email: credentials.email,\n password: credentials.password,\n remember: credentials.remember ?? false,\n })\n\n admin.setUser(body.data)\n\n // Signing in is the moment the panel becomes knowable: the manifest needs a session,\n // so it could not have been loaded before now.\n await admin.reload()\n\n return body.data\n },\n\n async setLocale(code) {\n // The panel redraws only after the server has accepted the choice: a language that\n // half took — menus switched, error messages not — is worse than one that did not.\n const body = await admin.http.put<{ data: AdminUser }>(`${base}/locale`, { locale: code })\n\n admin.setUser(body.data)\n\n await admin.setLocale(code)\n },\n\n async logout() {\n try {\n await admin.http.post(`${base}/logout`)\n } finally {\n // Whatever the server said, this browser is done: leaving the panel drawn behind a\n // failed sign-out would be worse than signing out optimistically.\n admin.setUser(null)\n admin.state.status = 'unauthenticated'\n admin.state.manifest = null\n }\n },\n }\n}\n\nexport function provideAuth(app: App, session: AuthSession): void {\n app.provide(authKey, session)\n}\n\nfunction isUnauthenticated(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status: unknown }).status === 401\n )\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\nimport { HttpError, useTranslate } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The card a panel opens on.\n *\n * No logo, no product name, no \"welcome back\" — an admin panel's front door should say as\n * little as possible to somebody who has no business behind it, and the people who do have\n * business behind it already know where they are.\n *\n * Every string is still a prop and a prop given still wins. What changed is what happens when\n * one is not: the label comes from the panel's dictionary, which the server assembles from the\n * same `lang` files it answers its own messages from. A panel gets its language without being\n * told every label twice, and a card placed outside a panel still speaks English.\n */\nconst props = withDefaults(\n defineProps<{\n emailLabel?: string\n passwordLabel?: string\n rememberLabel?: string\n submitLabel?: string\n capsLockWarning?: string\n /** `:seconds` is replaced with however long the server said to wait. */\n throttleMessage?: string\n revealLabel?: string\n hideLabel?: string\n /** Offer to stay signed in. The server honours it with a long-lived cookie. */\n remember?: boolean\n /** Put the cursor in the email field. Off for a card that is not the only thing on screen. */\n autofocus?: boolean\n }>(),\n {\n // Undefined rather than a string, so \"not given\" is tellable from \"given as English\" and\n // the dictionary gets its turn.\n emailLabel: undefined,\n passwordLabel: undefined,\n rememberLabel: undefined,\n submitLabel: undefined,\n capsLockWarning: undefined,\n throttleMessage: undefined,\n revealLabel: undefined,\n hideLabel: undefined,\n remember: true,\n autofocus: true,\n },\n)\n\nconst emit = defineEmits<{ success: [] }>()\n\nconst auth = useAuth()\nconst t = useTranslate('webx-auth')\n\nconst labels = computed(() => ({\n email: props.emailLabel ?? t('card.email'),\n password: props.passwordLabel ?? t('card.password'),\n remember: props.rememberLabel ?? t('card.remember'),\n submit: props.submitLabel ?? t('card.submit'),\n capsLock: props.capsLockWarning ?? t('card.caps-lock'),\n reveal: props.revealLabel ?? t('card.reveal'),\n hide: props.hideLabel ?? t('card.hide'),\n}))\n\nconst email = ref('')\nconst password = ref('')\nconst rememberMe = ref(false)\n\nconst busy = ref(false)\nconst revealed = ref(false)\nconst capsLock = ref(false)\nconst message = ref('')\nconst errors = ref<Record<string, string[]>>({})\nconst secondsLeft = ref(0)\n\nconst emailField = useTemplateRef<HTMLInputElement>('emailField')\n\nlet countdown: ReturnType<typeof setInterval> | null = null\n\nonBeforeUnmount(stopCountdown)\n\nconst throttled = computed(() => secondsLeft.value > 0)\n\nconst throttleNotice = computed(() =>\n props.throttleMessage === undefined\n ? t('card.throttled', { seconds: secondsLeft.value })\n : props.throttleMessage.replace(':seconds', String(secondsLeft.value)),\n)\n\nasync function submit(): Promise<void> {\n if (busy.value || throttled.value) {\n return\n }\n\n busy.value = true\n message.value = ''\n errors.value = {}\n\n try {\n await auth.login({\n email: email.value,\n password: password.value,\n remember: rememberMe.value,\n })\n\n password.value = ''\n emit('success')\n } catch (error) {\n handle(error)\n } finally {\n busy.value = false\n }\n}\n\nfunction handle(error: unknown): void {\n if (!(error instanceof HttpError)) {\n message.value = error instanceof Error ? error.message : String(error)\n\n return\n }\n\n // A bad password comes back as a 422 against the email field, the same as an address\n // nobody owns — the server refuses to say which, and so does this.\n if (error.isValidation) {\n errors.value = error.errors\n\n return\n }\n\n if (error.isThrottled) {\n startCountdown(error.retryAfter ?? 60)\n\n return\n }\n\n message.value = error.message\n}\n\nfunction startCountdown(seconds: number): void {\n stopCountdown()\n secondsLeft.value = seconds\n\n countdown = setInterval(() => {\n secondsLeft.value -= 1\n\n if (secondsLeft.value <= 0) {\n stopCountdown()\n }\n }, 1000)\n}\n\nfunction stopCountdown(): void {\n if (countdown !== null) {\n clearInterval(countdown)\n countdown = null\n }\n\n secondsLeft.value = 0\n}\n\n/**\n * Worth saying out loud: a password typed in capitals fails with the same unhelpful message\n * as a wrong one, and people retype it three times before noticing.\n */\nfunction trackCapsLock(event: KeyboardEvent): void {\n capsLock.value = event.getModifierState('CapsLock')\n}\n\ndefineExpose({ focus: () => emailField.value?.focus() })\n</script>\n\n<template>\n <wx-card class=\"wx-login\" :padding=\"24\">\n <wx-form :errors=\"errors\" :disabled=\"busy\" gap=\"md\" @submit.prevent=\"submit\">\n <wx-alert\n v-if=\"message !== ''\"\n type=\"danger\"\n variant=\"soft\"\n live\n :description=\"message\"\n class=\"wx-login__message\"\n />\n\n <wx-alert\n v-else-if=\"throttled\"\n type=\"warning\"\n variant=\"soft\"\n live\n :description=\"throttleNotice\"\n class=\"wx-login__message\"\n />\n\n <wx-form-item name=\"email\">\n <wx-input\n ref=\"emailField\"\n v-model=\"email\"\n type=\"email\"\n :placeholder=\"labels.email\"\n :aria-label=\"labels.email\"\n autocomplete=\"username\"\n :autofocus=\"autofocus\"\n inputmode=\"email\"\n size=\"lg\"\n >\n <template #prefix><wx-icon name=\"mail\" /></template>\n </wx-input>\n </wx-form-item>\n\n <wx-form-item name=\"password\" :help=\"capsLock ? labels.capsLock : undefined\">\n <wx-input\n v-model=\"password\"\n :type=\"revealed ? 'text' : 'password'\"\n :placeholder=\"labels.password\"\n :aria-label=\"labels.password\"\n autocomplete=\"current-password\"\n size=\"lg\"\n @keydown=\"trackCapsLock\"\n @keyup=\"trackCapsLock\"\n >\n <template #prefix><wx-icon name=\"lock\" /></template>\n <template #suffix>\n <!-- A bare icon rather than a button with a surface: it sits inside the field, and\n a second box in there reads as a second control. Out of the tab order because\n it is a convenience, not a step. -->\n <button\n type=\"button\"\n class=\"wx-login__reveal\"\n :title=\"revealed ? labels.hide : labels.reveal\"\n :aria-label=\"revealed ? labels.hide : labels.reveal\"\n :aria-pressed=\"revealed\"\n tabindex=\"-1\"\n @click=\"revealed = !revealed\"\n >\n <wx-icon :name=\"revealed ? 'eye-off' : 'eye'\" />\n </button>\n </template>\n </wx-input>\n </wx-form-item>\n\n <wx-checkbox v-if=\"remember\" v-model=\"rememberMe\" :label=\"labels.remember\" />\n\n <wx-button\n type=\"primary\"\n native-type=\"submit\"\n size=\"lg\"\n block\n :loading=\"busy\"\n :disabled=\"throttled\"\n >\n {{ labels.submit }}\n </wx-button>\n </wx-form>\n </wx-card>\n</template>\n\n<style scoped>\n.wx-login {\n width: 100%;\n /* Wide enough for an email address, narrow enough to stay a card rather than a page. */\n max-width: 26rem;\n}\n\n.wx-login__message {\n margin-block-end: var(--wx-space-4);\n}\n\n.wx-login__reveal {\n display: inline-flex;\n align-items: center;\n padding: 0;\n color: var(--wx-text-muted);\n background: none;\n border: 0;\n cursor: pointer;\n}\n\n.wx-login__reveal:hover,\n.wx-login__reveal[aria-pressed='true'] {\n color: var(--wx-text-default);\n}\n</style>\n","import type { Messages } from '@webx-ui/admin'\n\n/**\n * What this package says, in English.\n *\n * The same keys `webx-ui/module-auth` ships as `lang/en/*.php`. Kept here so a card placed by\n * hand, with no WebX UI server behind it, still has words — and so a key never reaches the\n * screen when a translation is missing.\n */\nexport const authMessages: Record<string, Messages> = {\n card: {\n email: 'Email',\n password: 'Password',\n remember: 'Stay signed in',\n submit: 'Sign in',\n 'caps-lock': 'Caps Lock is on.',\n reveal: 'Show the password',\n hide: 'Hide the password',\n throttled: 'Too many attempts. Try again in :seconds s.',\n failed: 'Those details do not match an account.',\n },\n menu: {\n 'sign-out': 'Sign out',\n language: 'Interface language',\n },\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useAdmin, useI18n, useTranslate } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The corner of the header: who this is, which language they read the panel in, and the way\n * out.\n *\n * The language picker lives here rather than in a settings screen because it is a property of\n * the person, not of the site — and because somebody who has landed in a language they cannot\n * read needs it within reach, not three clicks into a section they cannot navigate.\n */\nwithDefaults(defineProps<{ signOutLabel?: string }>(), { signOutLabel: undefined })\n\nconst admin = useAdmin()\nconst auth = useAuth()\nconst i18n = useI18n()\nconst t = useTranslate('webx-auth')\n\nconst user = computed(() => admin.state.user)\n\n// One language is not a choice, and a menu that offers it is noise.\nconst languages = computed(() =>\n i18n.state.panelLocales.length > 1 ? i18n.state.panelLocales : [],\n)\n\nconst initials = computed(() => {\n const name = user.value?.name ?? ''\n\n return name\n .split(/\\s+/)\n .filter((part) => part !== '')\n .slice(0, 2)\n .map((part) => part[0]?.toUpperCase() ?? '')\n .join('')\n})\n\nasync function choose(code: string): Promise<void> {\n if (code === i18n.state.locale) {\n return\n }\n\n await auth.setLocale(code)\n}\n</script>\n\n<template>\n <wx-dropdown v-if=\"user !== null\">\n <template #trigger>\n <wx-action :title=\"user.name\">\n <wx-avatar :label=\"initials\" size=\"sm\" />\n </wx-action>\n </template>\n\n <wx-dropdown-item disabled>\n <wx-text size=\"sm\">{{ user.email }}</wx-text>\n </wx-dropdown-item>\n\n <template v-if=\"languages.length > 0\">\n <wx-divider :spacing=\"4\" />\n\n <wx-dropdown-item disabled>\n <wx-text size=\"sm\" tone=\"muted\">{{ t('menu.language') }}</wx-text>\n </wx-dropdown-item>\n\n <wx-dropdown-item\n v-for=\"language in languages\"\n :key=\"language.code\"\n :icon=\"language.code === i18n.state.locale ? 'check' : undefined\"\n @click=\"choose(language.code)\"\n >\n {{ language.nativeName }}\n </wx-dropdown-item>\n\n <wx-divider :spacing=\"4\" />\n </template>\n\n <wx-dropdown-item icon=\"logout\" @click=\"auth.logout()\">\n {{ signOutLabel ?? t('menu.sign-out') }}\n </wx-dropdown-item>\n </wx-dropdown>\n</template>\n","import { h, watch } from 'vue'\nimport type { Admin, AdminPlugin } from '@webx-ui/admin'\nimport LoginCard from './LoginCard.vue'\nimport { authMessages } from './messages'\nimport { createAuthSession, provideAuth } from './session'\n\nexport interface AuthOptions {\n /** Where the sign-in screen lives, inside the panel. */\n path?: string\n /** Props handed to the card — the labels, mostly. */\n card?: Record<string, unknown>\n}\n\n/**\n * Signing in, as a plugin rather than a section: it owns a route and a guard, and it has no\n * place in the menu.\n *\n * Installing it also tells the panel how to find out who is signed in, which is why the panel\n * itself knows nothing about authentication — it holds the answer, not the question.\n */\nexport function auth(options: AuthOptions = {}): AdminPlugin {\n const path = options.path ?? '/login'\n\n return {\n install(admin: Admin) {\n const session = createAuthSession(admin.context)\n\n // The floor under this module's labels: what the server has not translated, and what\n // a panel assembled without a server has to fall back on.\n admin.i18n.defaults('webx-auth', authMessages)\n\n provideAuth(admin.app, session)\n admin.context.useSessionLoader(() => session.me())\n\n admin.router.addRoute({\n path,\n name: 'webx.login',\n meta: { public: true },\n component: { render: () => h(LoginCard, { ...options.card }) },\n })\n\n admin.router.beforeEach((to) => {\n if (to.meta.public === true) {\n return true\n }\n\n if (admin.context.state.status !== 'unauthenticated') {\n return true\n }\n\n return { path, query: to.fullPath === '/' ? {} : { next: to.fullPath } }\n })\n\n // Both directions are driven by the state rather than by an event, because the card\n // cannot be relied on to announce its own success: signing in flips the panel to\n // `ready`, the shell swaps the branch it renders, and the card is unmounted before the\n // line after `await` runs.\n watch(\n () => admin.context.state.status,\n (status) => {\n const current = admin.router.currentRoute.value\n\n // A session can also end between requests — a sign-out in another tab, an account\n // switched off. The panel learns that from a 401 on whatever it asked for next,\n // which is not a navigation, so nothing would move without this.\n if (status === 'unauthenticated' && current.path !== path) {\n void admin.router.replace({ path, query: { next: current.fullPath } })\n\n return\n }\n\n if (status === 'ready' && current.path === path) {\n const next = current.query.next\n\n void admin.router.replace(typeof next === 'string' ? next : '/')\n }\n },\n )\n },\n }\n}\n\nexport { createAuthSession, provideAuth, useAuth, authKey } from './session'\nexport type { AuthSession, Credentials } from './session'\nexport { default as WxLoginCard } from './LoginCard.vue'\nexport { default as WxUserMenu } from './UserMenu.vue'\nexport { authMessages } from './messages'\n"],"names":["authKey","useAuth","session","inject","createAuthSession","admin","base","error","isUnauthenticated","credentials","body","code","provideAuth","app","props","__props","emit","__emit","auth","t","useTranslate","labels","computed","email","ref","password","rememberMe","busy","revealed","capsLock","message","errors","secondsLeft","emailField","useTemplateRef","countdown","onBeforeUnmount","stopCountdown","throttled","throttleNotice","submit","handle","HttpError","startCountdown","seconds","trackCapsLock","event","__expose","_createBlock","_component_wx_card","_createVNode","_component_wx_form","_component_wx_alert","_component_wx_form_item","_component_wx_input","$event","_component_wx_icon","_createElementVNode","_cache","_component_wx_checkbox","_component_wx_button","_createTextVNode","_toDisplayString","authMessages","useAdmin","i18n","useI18n","user","languages","initials","part","choose","_component_wx_dropdown","_component_wx_action","_component_wx_avatar","_component_wx_dropdown_item","_component_wx_text","_createElementBlock","_Fragment","_component_wx_divider","_unref","_renderList","language","options","path","h","LoginCard","to","watch","status","current","next"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/session.ts","../src/LoginCard.vue","../src/messages.ts","../src/admins.ts","../src/i18n.ts","../src/AdminDialog.vue","../src/AdminList.vue","../src/AdminsPage.vue","../src/module.ts","../src/AdminPicker.vue","../src/selectAdmin.ts","../src/UserMenu.vue","../src/index.ts"],"sourcesContent":["import { inject, type App, type InjectionKey } from 'vue'\nimport type { AdminContext, AdminUser } from '@webx-ui/admin'\n\nexport interface Credentials {\n email: string\n password: string\n remember?: boolean\n}\n\nexport interface AuthSession {\n /** Who is signed in, or null. A 401 is an answer, not a failure. */\n me(): Promise<AdminUser | null>\n /** Signs in and tells the panel to load itself. Throws `HttpError` on a refusal. */\n login(credentials: Credentials): Promise<AdminUser>\n logout(): Promise<void>\n /**\n * Change the language this administrator reads the panel in.\n *\n * Stored on the person rather than in this browser, so it follows them to the next machine\n * and so the server writes its own messages — a 422 under a field — in the same language as\n * the label above it.\n */\n setLocale(code: string): Promise<void>\n}\n\nexport const authKey: InjectionKey<AuthSession> = Symbol('webx-auth')\n\nexport function useAuth(): AuthSession {\n const session = inject(authKey, null)\n\n if (session === null) {\n throw new Error('useAuth() was called outside a panel that installed the auth plugin.')\n }\n\n return session\n}\n\n/**\n * The three calls `webx-ui/module-auth` answers, and nothing else.\n *\n * The session lives in a cookie, so there is nothing to keep here: the source of truth is the\n * server, and the panel asks it rather than remembering.\n */\nexport function createAuthSession(admin: AdminContext): AuthSession {\n const base = `${admin.apiPath}/auth`\n\n return {\n async me() {\n try {\n const body = await admin.http.get<{ data: AdminUser | null }>(`${base}/me`)\n\n return body.data\n } catch (error) {\n if (isUnauthenticated(error)) {\n return null\n }\n\n throw error\n }\n },\n\n async login(credentials) {\n const body = await admin.http.post<{ data: AdminUser }>(`${base}/login`, {\n email: credentials.email,\n password: credentials.password,\n remember: credentials.remember ?? false,\n })\n\n admin.setUser(body.data)\n\n // Signing in is the moment the panel becomes knowable: the manifest needs a session,\n // so it could not have been loaded before now.\n await admin.reload()\n\n return body.data\n },\n\n async setLocale(code) {\n // The panel redraws only after the server has accepted the choice: a language that\n // half took — menus switched, error messages not — is worse than one that did not.\n const body = await admin.http.put<{ data: AdminUser }>(`${base}/locale`, { locale: code })\n\n admin.setUser(body.data)\n\n await admin.setLocale(code)\n },\n\n async logout() {\n try {\n await admin.http.post(`${base}/logout`)\n } finally {\n // Whatever the server said, this browser is done: leaving the panel drawn behind a\n // failed sign-out would be worse than signing out optimistically.\n admin.setUser(null)\n admin.state.status = 'unauthenticated'\n admin.state.manifest = null\n }\n },\n }\n}\n\nexport function provideAuth(app: App, session: AuthSession): void {\n app.provide(authKey, session)\n}\n\nfunction isUnauthenticated(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n (error as { status: unknown }).status === 401\n )\n}\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\nimport { HttpError, useTranslate } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The card a panel opens on.\n *\n * No logo, no product name, no \"welcome back\" — an admin panel's front door should say as\n * little as possible to somebody who has no business behind it, and the people who do have\n * business behind it already know where they are.\n *\n * Every string is still a prop and a prop given still wins. What changed is what happens when\n * one is not: the label comes from the panel's dictionary, which the server assembles from the\n * same `lang` files it answers its own messages from. A panel gets its language without being\n * told every label twice, and a card placed outside a panel still speaks English.\n */\nconst props = withDefaults(\n defineProps<{\n emailLabel?: string\n passwordLabel?: string\n rememberLabel?: string\n submitLabel?: string\n capsLockWarning?: string\n /** `:seconds` is replaced with however long the server said to wait. */\n throttleMessage?: string\n revealLabel?: string\n hideLabel?: string\n /** Offer to stay signed in. The server honours it with a long-lived cookie. */\n remember?: boolean\n /** Put the cursor in the email field. Off for a card that is not the only thing on screen. */\n autofocus?: boolean\n }>(),\n {\n // Undefined rather than a string, so \"not given\" is tellable from \"given as English\" and\n // the dictionary gets its turn.\n emailLabel: undefined,\n passwordLabel: undefined,\n rememberLabel: undefined,\n submitLabel: undefined,\n capsLockWarning: undefined,\n throttleMessage: undefined,\n revealLabel: undefined,\n hideLabel: undefined,\n remember: true,\n autofocus: true,\n },\n)\n\nconst emit = defineEmits<{ success: [] }>()\n\nconst auth = useAuth()\nconst t = useTranslate('webx-auth')\n\nconst labels = computed(() => ({\n email: props.emailLabel ?? t('card.email'),\n password: props.passwordLabel ?? t('card.password'),\n remember: props.rememberLabel ?? t('card.remember'),\n submit: props.submitLabel ?? t('card.submit'),\n capsLock: props.capsLockWarning ?? t('card.caps-lock'),\n reveal: props.revealLabel ?? t('card.reveal'),\n hide: props.hideLabel ?? t('card.hide'),\n}))\n\nconst email = ref('')\nconst password = ref('')\nconst rememberMe = ref(false)\n\nconst busy = ref(false)\nconst revealed = ref(false)\nconst capsLock = ref(false)\nconst message = ref('')\nconst errors = ref<Record<string, string[]>>({})\nconst secondsLeft = ref(0)\n\nconst emailField = useTemplateRef<HTMLInputElement>('emailField')\n\nlet countdown: ReturnType<typeof setInterval> | null = null\n\nonBeforeUnmount(stopCountdown)\n\nconst throttled = computed(() => secondsLeft.value > 0)\n\nconst throttleNotice = computed(() =>\n props.throttleMessage === undefined\n ? t('card.throttled', { seconds: secondsLeft.value })\n : props.throttleMessage.replace(':seconds', String(secondsLeft.value)),\n)\n\nasync function submit(): Promise<void> {\n if (busy.value || throttled.value) {\n return\n }\n\n busy.value = true\n message.value = ''\n errors.value = {}\n\n try {\n await auth.login({\n email: email.value,\n password: password.value,\n remember: rememberMe.value,\n })\n\n password.value = ''\n emit('success')\n } catch (error) {\n handle(error)\n } finally {\n busy.value = false\n }\n}\n\nfunction handle(error: unknown): void {\n if (!(error instanceof HttpError)) {\n message.value = error instanceof Error ? error.message : String(error)\n\n return\n }\n\n // A bad password comes back as a 422 against the email field, the same as an address\n // nobody owns — the server refuses to say which, and so does this.\n if (error.isValidation) {\n errors.value = error.errors\n\n return\n }\n\n if (error.isThrottled) {\n startCountdown(error.retryAfter ?? 60)\n\n return\n }\n\n message.value = error.message\n}\n\nfunction startCountdown(seconds: number): void {\n stopCountdown()\n secondsLeft.value = seconds\n\n countdown = setInterval(() => {\n secondsLeft.value -= 1\n\n if (secondsLeft.value <= 0) {\n stopCountdown()\n }\n }, 1000)\n}\n\nfunction stopCountdown(): void {\n if (countdown !== null) {\n clearInterval(countdown)\n countdown = null\n }\n\n secondsLeft.value = 0\n}\n\n/**\n * Worth saying out loud: a password typed in capitals fails with the same unhelpful message\n * as a wrong one, and people retype it three times before noticing.\n */\nfunction trackCapsLock(event: KeyboardEvent): void {\n capsLock.value = event.getModifierState('CapsLock')\n}\n\ndefineExpose({ focus: () => emailField.value?.focus() })\n</script>\n\n<template>\n <wx-card class=\"wx-login\" :padding=\"24\">\n <wx-form :errors=\"errors\" :disabled=\"busy\" gap=\"md\" @submit.prevent=\"submit\">\n <wx-alert\n v-if=\"message !== ''\"\n type=\"danger\"\n variant=\"soft\"\n live\n :description=\"message\"\n class=\"wx-login__message\"\n />\n\n <wx-alert\n v-else-if=\"throttled\"\n type=\"warning\"\n variant=\"soft\"\n live\n :description=\"throttleNotice\"\n class=\"wx-login__message\"\n />\n\n <wx-form-item name=\"email\">\n <wx-input\n ref=\"emailField\"\n v-model=\"email\"\n type=\"email\"\n :placeholder=\"labels.email\"\n :aria-label=\"labels.email\"\n autocomplete=\"username\"\n :autofocus=\"autofocus\"\n inputmode=\"email\"\n size=\"lg\"\n >\n <template #prefix><wx-icon name=\"mail\" /></template>\n </wx-input>\n </wx-form-item>\n\n <wx-form-item name=\"password\" :help=\"capsLock ? labels.capsLock : undefined\">\n <wx-input\n v-model=\"password\"\n :type=\"revealed ? 'text' : 'password'\"\n :placeholder=\"labels.password\"\n :aria-label=\"labels.password\"\n autocomplete=\"current-password\"\n size=\"lg\"\n @keydown=\"trackCapsLock\"\n @keyup=\"trackCapsLock\"\n >\n <template #prefix><wx-icon name=\"lock\" /></template>\n <template #suffix>\n <!-- A bare icon rather than a button with a surface: it sits inside the field, and\n a second box in there reads as a second control. Out of the tab order because\n it is a convenience, not a step. -->\n <button\n type=\"button\"\n class=\"wx-login__reveal\"\n :title=\"revealed ? labels.hide : labels.reveal\"\n :aria-label=\"revealed ? labels.hide : labels.reveal\"\n :aria-pressed=\"revealed\"\n tabindex=\"-1\"\n @click=\"revealed = !revealed\"\n >\n <wx-icon :name=\"revealed ? 'eye-off' : 'eye'\" />\n </button>\n </template>\n </wx-input>\n </wx-form-item>\n\n <wx-checkbox v-if=\"remember\" v-model=\"rememberMe\" :label=\"labels.remember\" />\n\n <wx-button\n type=\"primary\"\n native-type=\"submit\"\n size=\"lg\"\n block\n :loading=\"busy\"\n :disabled=\"throttled\"\n >\n {{ labels.submit }}\n </wx-button>\n </wx-form>\n </wx-card>\n</template>\n\n<style scoped>\n.wx-login {\n width: 100%;\n /* Wide enough for an email address, narrow enough to stay a card rather than a page. */\n max-width: 26rem;\n}\n\n.wx-login__message {\n margin-block-end: var(--wx-space-4);\n}\n\n.wx-login__reveal {\n display: inline-flex;\n align-items: center;\n padding: 0;\n color: var(--wx-text-muted);\n background: none;\n border: 0;\n cursor: pointer;\n}\n\n.wx-login__reveal:hover,\n.wx-login__reveal[aria-pressed='true'] {\n color: var(--wx-text-default);\n}\n</style>\n","import type { Messages } from '@webx-ui/admin'\n\n/**\n * What this package says, in English.\n *\n * The same keys `webx-ui/module-auth` ships as `lang/en/*.php`. Kept here so a card placed by\n * hand, with no WebX UI server behind it, still has words — and so a key never reaches the\n * screen when a translation is missing.\n */\nexport const authMessages: Record<string, Messages> = {\n card: {\n email: 'Email',\n password: 'Password',\n remember: 'Stay signed in',\n submit: 'Sign in',\n 'caps-lock': 'Caps Lock is on.',\n reveal: 'Show the password',\n hide: 'Hide the password',\n throttled: 'Too many attempts. Try again in :seconds s.',\n failed: 'Those details do not match an account.',\n },\n admins: {\n title: 'Administrators',\n search: 'Search administrators',\n new: 'New administrator',\n edit: 'Edit administrator',\n // The same two, short enough for a button on a phone and for a dialog's heading.\n 'new-short': 'Add',\n 'edit-short': 'Edit',\n name: 'Name',\n email: 'Email',\n password: 'Password',\n 'password-hint': 'At least 12 characters',\n 'password-keep': 'Leave blank to keep the current one',\n avatar: 'Photo',\n roles: 'Roles',\n 'no-roles': 'No roles',\n active: 'Active',\n super: 'Super administrator',\n 'super-hint': 'Answers yes to everything, whatever the roles say',\n locale: 'Panel language',\n 'locale-auto': 'Follow the browser',\n 'last-login': 'Last signed in',\n never: 'Never',\n save: 'Save',\n cancel: 'Cancel',\n delete: 'Delete',\n 'delete-title': 'Delete :name?',\n 'delete-text': 'They lose access at once. What they have written stays.',\n 'all-roles': 'All roles',\n 'any-state': 'Active and not',\n 'only-active': 'Active only',\n 'only-inactive': 'Switched off only',\n empty: 'Nobody matches that',\n 'select-one': 'Choose an administrator',\n 'select-many': 'Choose administrators',\n chosen: 'Chosen: :count',\n created: 'Administrator created',\n updated: 'Administrator saved',\n deleted: 'Administrator deleted',\n },\n errors: {\n unauthenticated: 'Unauthenticated.',\n inactive: 'This account is no longer active.',\n forbidden: 'This account may not do that.',\n 'not-yourself': 'You cannot do that to your own account.',\n 'last-super': 'There has to be one super administrator left.',\n },\n menu: {\n 'sign-out': 'Sign out',\n language: 'Interface language',\n },\n}\n","import type { AdminContext } from '@webx-ui/admin'\nimport type { Admin, AdminInput, AdminPage, AdminQuery, Role } from './types'\n\nexport interface AdminsApi {\n list(query?: AdminQuery): Promise<AdminPage>\n get(id: number): Promise<Admin>\n create(input: AdminInput): Promise<Admin>\n update(id: number, input: AdminInput): Promise<Admin>\n remove(id: number): Promise<void>\n roles(): Promise<Role[]>\n}\n\n/**\n * The calls behind the administrators screen, and behind the picker that opens from code.\n *\n * Reading is separate from managing on the server, so a module that only wants to say who\n * wrote something — or offer a list of people to assign work to — can use `list` and `roles`\n * without being allowed anywhere near the rest.\n */\nexport function createAdminsApi(admin: AdminContext): AdminsApi {\n const base = `${admin.apiPath}/auth`\n const data = <T>(body: { data: T }): T => body.data\n\n return {\n list: (query = {}) =>\n admin.http\n .get<{ data: Admin[]; meta: Omit<AdminPage, 'data'> }>(`${base}/admins`, {\n query: {\n q: query.q || undefined,\n role: query.role ?? undefined,\n active: query.active ?? undefined,\n sort: query.sort,\n page: query.page,\n per_page: query.per_page,\n },\n })\n .then((body) => ({ ...body.meta, data: body.data })),\n\n get: (id) => admin.http.get<{ data: Admin }>(`${base}/admins/${id}`).then(data),\n\n create: (input) => admin.http.post<{ data: Admin }>(`${base}/admins`, input).then(data),\n\n update: (id, input) =>\n admin.http.patch<{ data: Admin }>(`${base}/admins/${id}`, input).then(data),\n\n remove: (id) => admin.http.delete<void>(`${base}/admins/${id}`),\n\n roles: () => admin.http.get<{ data: Role[] }>(`${base}/roles`).then(data),\n }\n}\n","import { useAdmin } from '@webx-ui/admin'\nimport { authMessages } from './messages'\n\nconst seeded = new WeakSet<object>()\n\n/**\n * Put this package's English under the panel's dictionary, once per panel.\n *\n * The plugin does this too, but a panel can use the administrators screen — or just the picker\n * — without installing the sign-in plugin at all, and a screen showing `admins.title` instead\n * of a word is not a thing to leave to chance.\n */\nexport function useAuthMessages(): void {\n try {\n const { i18n } = useAdmin()\n\n if (!seeded.has(i18n)) {\n i18n.defaults('webx-auth', authMessages)\n seeded.add(i18n)\n }\n } catch {\n // Outside a panel there is no dictionary to seed, and `useTranslate` falls back on its own.\n }\n}\n","<script setup lang=\"ts\">\nimport { computed, ref, watch, type Component } from 'vue'\nimport { useAdmin, useI18n, useTranslate } from '@webx-ui/admin'\nimport {\n toast,\n useModal,\n WxButton,\n WxCheckbox,\n WxDialog,\n WxFormItem,\n WxInput,\n WxSelect,\n WxSpace,\n WxText,\n} from '@webx-ui/core'\n\nimport { createAdminsApi } from './admins'\nimport { useAuthMessages } from './i18n'\nimport type { Admin, AdminInput, Role } from './types'\n\n/**\n * Creating an administrator and editing one — the same form, because they differ in one field.\n *\n * The password is required the first time and optional afterwards, and a blank one on an edit\n * means \"leave it alone\" rather than \"set it to nothing\". That is the only conditional here.\n */\nconst props = withDefaults(\n defineProps<{\n /** Absent creates somebody; present edits them. */\n admin?: Admin | null\n /**\n * The field to pick the photograph with — `WxMediaField` in a panel that has the library.\n *\n * Handed in rather than imported: this package does not depend on the media one, and a\n * panel without a library still edits everything else about a person.\n */\n avatarField?: Component\n }>(),\n { admin: null, avatarField: undefined },\n)\n\nconst { resolve, dismiss, open } = useModal<Admin>()\n\nconst context = useAdmin()\nconst api = createAdminsApi(context)\nconst i18n = useI18n()\nuseAuthMessages()\n\nconst t = useTranslate('webx-auth')\n\nconst roles = ref<Role[]>([])\nconst saving = ref(false)\nconst errors = ref<Record<string, string[]>>({})\n\nconst form = ref<AdminInput & { avatar: string | null }>({\n name: '',\n email: '',\n password: '',\n avatar: null,\n is_active: true,\n is_super: false,\n locale: null,\n roles: [],\n})\n\n/** The field hands back `{ path, … }`; what is stored is the key. */\nconst photo = computed({\n get: () => (form.value.avatar ? { path: form.value.avatar } : null),\n set: (value: { path: string } | null) => {\n form.value.avatar = value?.path ?? null\n },\n})\n\nconst editing = computed(() => props.admin !== null)\n\nconst roleOptions = computed(() => roles.value.map((one) => ({ value: one.id, label: one.name })))\n\n/* A word, not an empty string: a select reads \"\" as nothing chosen and shows a blank control. */\nconst localeOptions = computed(() => [\n { value: 'auto', label: t('admins.locale-auto') },\n ...i18n.state.panelLocales.map((one) => ({ value: one.code, label: one.nativeName })),\n])\n\nvoid api.roles().then((all) => (roles.value = all))\n\nwatch(\n () => props.admin,\n (admin) => {\n form.value = {\n name: admin?.name ?? '',\n email: admin?.email ?? '',\n password: '',\n avatar: admin?.avatar ?? null,\n is_active: admin?.is_active ?? true,\n is_super: admin?.is_super ?? false,\n locale: admin?.locale ?? 'auto',\n roles: admin?.roles.map((one) => one.id) ?? [],\n }\n errors.value = {}\n },\n { immediate: true },\n)\n\nfunction errorOf(field: string): string | undefined {\n return errors.value[field]?.[0]\n}\n\nasync function save(): Promise<void> {\n saving.value = true\n errors.value = {}\n\n const payload: AdminInput = {\n name: form.value.name,\n email: form.value.email,\n avatar: form.value.avatar,\n is_active: form.value.is_active,\n is_super: form.value.is_super,\n locale: form.value.locale === 'auto' ? null : form.value.locale,\n roles: form.value.roles ?? [],\n }\n\n if (form.value.password) {\n payload.password = form.value.password\n }\n\n try {\n const saved = props.admin\n ? await api.update(props.admin.id, payload)\n : await api.create(payload)\n\n toast.success(t(editing.value ? 'admins.updated' : 'admins.created'))\n resolve(saved)\n } catch (error) {\n const body = (error as { body?: { errors?: Record<string, string[]>; message?: string } }).body\n\n if (body?.errors) {\n errors.value = body.errors\n } else if (body?.message) {\n // The two refusals that keep a panel reachable arrive as a message, not as a field.\n toast.danger(body.message)\n }\n } finally {\n saving.value = false\n }\n}\n</script>\n\n<template>\n <wx-dialog\n v-model:open=\"open\"\n :title=\"editing ? t('admins.edit-short') : t('admins.new-short')\"\n :width=\"720\"\n >\n <!--\n The photograph beside the fields rather than above them: it is square, the fields are a\n column, and stacking the two makes somebody scroll past a picture to reach a name. Under\n `--wx-admin-form-wrap` the sidebar goes back on top, which is the only shape that fits a\n phone.\n -->\n <div class=\"wx-admin-form\">\n <div class=\"wx-admin-form__grid\">\n <div v-if=\"avatarField\" class=\"wx-admin-form__aside\">\n <component\n :is=\"avatarField\"\n v-model=\"photo\"\n :label=\"t('admins.avatar')\"\n aspect=\"1/1\"\n :captions=\"false\"\n />\n </div>\n\n <div class=\"wx-admin-form__fields\">\n <wx-form-item :label=\"t('admins.name')\" :error=\"errorOf('name')\">\n <wx-input v-model=\"form.name\" name=\"name\" />\n </wx-form-item>\n\n <wx-form-item :label=\"t('admins.email')\" :error=\"errorOf('email')\">\n <wx-input v-model=\"form.email\" type=\"email\" name=\"email\" autocomplete=\"off\" />\n </wx-form-item>\n\n <wx-form-item\n :label=\"t('admins.password')\"\n :error=\"errorOf('password')\"\n :help=\"editing ? t('admins.password-keep') : t('admins.password-hint')\"\n >\n <wx-input\n v-model=\"form.password\"\n type=\"password\"\n name=\"password\"\n autocomplete=\"new-password\"\n />\n </wx-form-item>\n\n <wx-form-item :label=\"t('admins.roles')\" :error=\"errorOf('roles')\">\n <wx-select v-model=\"form.roles\" :options=\"roleOptions\" multiple />\n </wx-form-item>\n\n <wx-form-item :label=\"t('admins.locale')\">\n <wx-select v-model=\"form.locale\" :options=\"localeOptions\" />\n </wx-form-item>\n\n <wx-space size=\"md\">\n <wx-checkbox v-model=\"form.is_active\" :label=\"t('admins.active')\" />\n <wx-checkbox v-model=\"form.is_super\" :label=\"t('admins.super')\" />\n </wx-space>\n\n <wx-text size=\"sm\" tone=\"muted\">{{ t('admins.super-hint') }}</wx-text>\n </div>\n </div>\n </div>\n\n <template #footer>\n <wx-space size=\"sm\">\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('admins.cancel') }}</wx-button>\n <wx-button type=\"primary\" :loading=\"saving\" @click=\"save\">\n {{ t('admins.save') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n\n<style>\n/* The container is the wrapper, not the grid: an element cannot query its own width. */\n.wx-admin-form {\n container-type: inline-size;\n}\n\n.wx-admin-form__grid {\n display: grid;\n grid-template-columns: 200px minmax(0, 1fr);\n gap: var(--wx-space-24);\n}\n\n.wx-admin-form__fields {\n display: flex;\n flex-direction: column;\n gap: var(--wx-space-12);\n min-width: 0;\n}\n\n/* The dialog's own width decides this, not the window's. */\n@container (max-width: 520px) {\n .wx-admin-form__grid {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .wx-admin-form__aside {\n max-width: 200px;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, ref, watch } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/admin'\nimport {\n WxAction,\n WxActions,\n WxAvatar,\n WxBadge,\n WxSelect,\n WxSpace,\n WxTable,\n WxText,\n type TableColumn,\n type TableState,\n} from '@webx-ui/core'\nimport { createAdminsApi } from './admins'\nimport { useAuthMessages } from './i18n'\nimport type { Admin, AdminPage, Role } from './types'\n\n/**\n * The list of administrators — the screen, and the inside of the picker.\n *\n * One component for both so that searching, filtering and paging behave the same wherever\n * somebody is looking at these people. What changes is only what a row is for: opening it, or\n * choosing it.\n */\nconst props = withDefaults(\n defineProps<{\n /** Rows are chosen rather than opened. */\n picking?: boolean\n /** More than one may be chosen at a time. */\n multiple?: boolean\n /** Adds the column that removes somebody. */\n removable?: boolean\n /**\n * Offers the role and state filters.\n *\n * Off by default: a panel has half a dozen administrators, and a filter over six rows is a\n * control to read and decide about where a glance would have done.\n */\n filters?: boolean\n /** Turns an avatar key into an address. The panel wires this; this package cannot. */\n resolveAvatar?: (key: string) => Promise<string | null>\n }>(),\n {\n picking: false,\n multiple: false,\n removable: false,\n filters: false,\n resolveAvatar: undefined,\n },\n)\n\nconst emit = defineEmits<{\n open: [admin: Admin]\n chosen: [admins: Admin[]]\n /** Which rows are ticked, for a footer that counts them. */\n selection: [admins: Admin[]]\n remove: [admin: Admin]\n}>()\n\nconst admin = useAdmin()\nconst api = createAdminsApi(admin)\nuseAuthMessages()\n\nconst t = useTranslate('webx-auth')\n\nconst page = ref<AdminPage | null>(null)\nconst roles = ref<Role[]>([])\nconst loading = ref(false)\n/* A word rather than an empty string: a select reads \"\" as nothing chosen and shows a\n blank control where the option said \"all roles\". */\nconst role = ref('all')\nconst active = ref<'any' | 'yes' | 'no'>('any')\nconst selected = ref<Admin[]>([])\nconst avatars = ref<Record<string, string>>({})\n\nlet last: TableState = { page: 1, perPage: 20, sort: null, search: '' }\n\nconst roleOptions = computed(() => [\n { value: 'all', label: t('admins.all-roles') },\n ...roles.value.map((one) => ({ value: one.slug, label: one.name })),\n])\n\nconst stateOptions = computed(() => [\n { value: 'any', label: t('admins.any-state') },\n { value: 'yes', label: t('admins.only-active') },\n { value: 'no', label: t('admins.only-inactive') },\n])\n\n/*\n * No widths but the one the buttons need.\n *\n * A declared width is a promise the table has to keep, and five of them add up to more than the\n * screen — so it scrolls sideways while half the row is whitespace. Left alone, the columns take\n * what their contents need. What does not fit is dropped instead, in the order it can be spared.\n */\nconst columns = computed<TableColumn<Admin>[]>(() => [\n { key: 'name', label: t('admins.name'), sortable: true },\n { key: 'email', label: t('admins.email'), sortable: true, hideBelow: 560 },\n { key: 'roles', label: t('admins.roles'), hideBelow: 760 },\n { key: 'is_active', label: t('admins.active'), align: 'center', hideBelow: 660 },\n {\n key: 'last_login_at',\n label: t('admins.last-login'),\n sortable: true,\n hideBelow: 900,\n // The first thing to go when a row becomes a card: it is a date somebody scans down a\n // column, and a card has no column to scan.\n hideOnCards: true,\n },\n {\n key: 'actions',\n label: '',\n width: 56,\n align: 'right',\n hidden: !props.removable,\n // A card puts them along its top instead, through the `card-actions` slot.\n hideOnCards: true,\n },\n])\n\nvoid api.roles().then((all) => (roles.value = all))\n\nwatch([role, active], () => load(last))\n\nasync function load(state: TableState): Promise<void> {\n last = state\n loading.value = true\n\n try {\n page.value = await api.list({\n q: state.search,\n role: role.value === 'all' ? null : role.value,\n active: active.value === 'any' ? null : active.value,\n sort: state.sort ? `${state.sort.order === 'desc' ? '-' : ''}${state.sort.key}` : 'name',\n page: state.page,\n per_page: state.perPage,\n })\n\n void resolveAvatars()\n } finally {\n loading.value = false\n }\n}\n\n/**\n * A row stores the library's key, not an address, and this package has no business knowing how\n * to turn one into the other. The panel hands that in; without it a row shows initials, which\n * is what it would have shown anyway for everybody who never uploaded a photograph.\n */\nasync function resolveAvatars(): Promise<void> {\n const resolve = props.resolveAvatar\n\n if (!resolve) return\n\n for (const row of page.value?.data ?? []) {\n if (!row.avatar || avatars.value[row.avatar]) continue\n\n const url = await resolve(row.avatar)\n\n if (url) avatars.value = { ...avatars.value, [row.avatar]: url }\n }\n}\n\nfunction onRow(row: Admin): void {\n if (!props.picking) {\n emit('open', row)\n\n return\n }\n\n if (!props.multiple) {\n emit('chosen', [row])\n }\n}\n\nfunction onSelection(_keys: unknown, rows: Admin[]): void {\n selected.value = rows\n emit('selection', rows)\n}\n\n/** Reload without losing where somebody was, for a caller that changed something. */\ndefineExpose({ reload: () => load(last), chosen: () => selected.value })\n</script>\n\n<template>\n <div class=\"wx-admin-list\">\n <wx-table\n :data=\"page\"\n :columns=\"columns\"\n row-key=\"id\"\n searchable\n hover\n flush\n :loading=\"loading\"\n :search-placeholder=\"t('admins.search')\"\n :empty-text=\"t('admins.empty')\"\n :selectable=\"picking && multiple\"\n @state-change=\"load\"\n @row-click=\"onRow\"\n @selection-change=\"onSelection\"\n >\n <template v-if=\"filters\" #actions>\n <wx-space size=\"sm\">\n <wx-select v-model=\"role\" :options=\"roleOptions\" size=\"sm\" style=\"width: 180px\" />\n <wx-select v-model=\"active\" :options=\"stateOptions\" size=\"sm\" style=\"width: 180px\" />\n </wx-space>\n </template>\n\n <template #cell-name=\"{ row }\">\n <wx-space size=\"sm\" align=\"center\">\n <wx-avatar\n :name=\"row.name\"\n :src=\"row.avatar ? avatars[row.avatar] : undefined\"\n size=\"sm\"\n />\n <span>{{ row.name }}</span>\n <wx-badge v-if=\"row.is_super\" type=\"warning\">{{ t('admins.super') }}</wx-badge>\n </wx-space>\n </template>\n\n <template #cell-roles=\"{ row }\">\n <wx-space v-if=\"row.roles.length > 0\" size=\"xs\" wrap>\n <wx-badge v-for=\"one in row.roles\" :key=\"one.id\">{{ one.name }}</wx-badge>\n </wx-space>\n <wx-text v-else size=\"sm\" tone=\"muted\">{{ t('admins.no-roles') }}</wx-text>\n </template>\n\n <template #cell-is_active=\"{ row }\">\n <wx-badge :type=\"row.is_active ? 'success' : 'default'\" dot>\n {{ row.is_active ? t('admins.active') : t('admins.only-inactive') }}\n </wx-badge>\n </template>\n\n <template #card-actions=\"{ row }\">\n <wx-actions v-if=\"removable\" size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('admins.delete')\" @click=\"emit('remove', row)\" />\n </wx-actions>\n </template>\n\n <template #cell-actions=\"{ row }\">\n <!-- `.stop`: the row opens the form, and removing somebody is not opening them. -->\n <wx-actions size=\"sm\" @click.stop>\n <wx-action type=\"remove\" :title=\"t('admins.delete')\" @click=\"emit('remove', row)\" />\n </wx-actions>\n </template>\n\n <template #cell-last_login_at=\"{ row }\">\n <wx-text size=\"sm\" :tone=\"row.last_login_at ? 'default' : 'muted'\">\n {{ row.last_login_at ? new Date(row.last_login_at).toLocaleString() : t('admins.never') }}\n </wx-text>\n </template>\n </wx-table>\n </div>\n</template>\n\n<style>\n.wx-admin-list {\n min-width: 0;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { ref, useTemplateRef, type Component } from 'vue'\nimport { useAdmin, useTranslate } from '@webx-ui/admin'\nimport { confirm, createModal, toast, WxButton, WxCard } from '@webx-ui/core'\nimport AdminDialog from './AdminDialog.vue'\nimport AdminList from './AdminList.vue'\nimport { createAdminsApi } from './admins'\nimport { useAuthMessages } from './i18n'\nimport type { Admin } from './types'\n\n/**\n * The administrators section: the list, and the form over it.\n *\n * A dialog rather than a second route. Editing somebody is half a dozen fields, and a screen\n * that takes over the page for that loses the list you were reading — which is where you\n * decide who to open next.\n */\nconst props = withDefaults(\n defineProps<{\n avatarField?: Component\n resolveAvatar?: (key: string) => Promise<string | null>\n }>(),\n { avatarField: undefined, resolveAvatar: undefined },\n)\n\nconst context = useAdmin()\nconst api = createAdminsApi(context)\nuseAuthMessages()\n\nconst t = useTranslate('webx-auth')\n\nconst list = useTemplateRef<{ reload: () => void }>('list')\nconst editing = ref<Admin | null>(null)\n\nconst edit = createModal<Admin, { admin: Admin | null; avatarField?: Component }>(AdminDialog)\n\nconst canManage = context.can('users.manage')\n\nasync function open(admin: Admin | null): Promise<void> {\n editing.value = admin\n\n const saved = await edit({ admin, avatarField: props.avatarField })\n\n if (saved) {\n list.value?.reload()\n }\n}\n\nasync function remove(admin: Admin): Promise<void> {\n const agreed = await confirm({\n title: t('admins.delete-title', { name: admin.name }),\n message: t('admins.delete-text'),\n confirmText: t('admins.delete'),\n cancelText: t('admins.cancel'),\n tone: 'danger',\n })\n\n if (!agreed) return\n\n try {\n await api.remove(admin.id)\n toast.success(t('admins.deleted'))\n list.value?.reload()\n } catch (error) {\n const body = (error as { body?: { message?: string } }).body\n\n toast.danger(body?.message ?? t('errors.forbidden'))\n }\n}\n</script>\n\n<template>\n <wx-card>\n <template #header>{{ t('admins.title') }}</template>\n\n <template v-if=\"canManage\" #extra>\n <!-- Short enough for a phone, where the heading and the button share one line. -->\n <wx-button type=\"primary\" icon=\"add\" @click=\"open(null)\">\n {{ t('admins.new-short') }}\n </wx-button>\n </template>\n\n <admin-list\n ref=\"list\"\n :removable=\"canManage\"\n :resolve-avatar=\"props.resolveAvatar\"\n @open=\"open\"\n @remove=\"remove\"\n />\n </wx-card>\n</template>\n","import { h, type Component } from 'vue'\nimport type { AdminModule } from '@webx-ui/admin'\nimport AdminsPage from './AdminsPage.vue'\n\nexport interface AdminsOptions {\n /** Where the section lives inside the panel. */\n path?: string\n /**\n * The field the photograph is picked with — `WxMediaField` in a panel that has the library.\n *\n * Handed in rather than imported: this package does not depend on the media one, and a panel\n * without a library still edits everything else about a person.\n */\n avatarField?: Component\n /** Turns an avatar key into an address. Omitted, rows and forms show initials. */\n resolveAvatar?: (key: string) => Promise<string | null>\n}\n\n/**\n * The administrators, as a section of the panel.\n *\n * The id matches the module the server reports, which is what makes the entry appear in the\n * navigation: a section shows up when both halves are installed.\n */\nexport function admins(options: AdminsOptions = {}): AdminModule {\n const path = options.path ?? '/users'\n\n return {\n id: 'users',\n path,\n routes: [\n {\n path,\n name: 'webx.admins',\n component: {\n render: () =>\n h(AdminsPage, {\n avatarField: options.avatarField,\n resolveAvatar: options.resolveAvatar,\n }),\n },\n },\n ],\n }\n}\n","<script setup lang=\"ts\">\nimport { ref, useTemplateRef } from 'vue'\nimport { useTranslate } from '@webx-ui/admin'\nimport { useModal, WxButton, WxDialog, WxSpace, WxText } from '@webx-ui/core'\nimport AdminList from './AdminList.vue'\nimport { useAuthMessages } from './i18n'\nimport type { Admin } from './types'\n\n/**\n * The list of administrators as a dialog, resolving with whoever was chosen.\n *\n * The same list as the screen — one component, so that searching and filtering behave the same\n * wherever these people are being looked at.\n */\nconst props = withDefaults(\n defineProps<{\n multiple?: boolean\n title?: string\n resolveAvatar?: (key: string) => Promise<string | null>\n }>(),\n { multiple: false, title: undefined, resolveAvatar: undefined },\n)\n\nconst { open, resolve, dismiss } = useModal<Admin[]>()\n\nuseAuthMessages()\n\nconst t = useTranslate('webx-auth')\n\nconst list = useTemplateRef<{ chosen: () => Admin[] }>('list')\nconst count = ref(0)\n\n/** One is chosen by clicking the row; several are ticked and then confirmed. */\nfunction chosen(admins: Admin[]): void {\n resolve(admins)\n}\n\nfunction track(admins: Admin[]): void {\n count.value = admins.length\n}\n</script>\n\n<template>\n <wx-dialog\n v-model:open=\"open\"\n :title=\"title ?? t(multiple ? 'admins.select-many' : 'admins.select-one')\"\n :width=\"880\"\n >\n <admin-list\n ref=\"list\"\n picking\n :multiple=\"multiple\"\n :resolve-avatar=\"props.resolveAvatar\"\n @chosen=\"chosen\"\n @selection=\"track\"\n />\n\n <template v-if=\"multiple\" #footer>\n <wx-space size=\"sm\" align=\"center\">\n <wx-text size=\"sm\" tone=\"muted\">{{ t('admins.chosen', { count }) }}</wx-text>\n <wx-button variant=\"outline\" @click=\"dismiss()\">{{ t('admins.cancel') }}</wx-button>\n <wx-button type=\"primary\" :disabled=\"count === 0\" @click=\"resolve(list?.chosen() ?? [])\">\n {{ t('admins.save') }}\n </wx-button>\n </wx-space>\n </template>\n </wx-dialog>\n</template>\n","import { createModal } from '@webx-ui/core'\nimport AdminPicker from './AdminPicker.vue'\nimport type { Admin } from './types'\n\nexport interface AdminPickerOptions {\n /** Heading of the dialog, when the default is not specific enough. */\n title?: string\n /** Turns an avatar key into an address. Omitted, rows show initials. */\n resolveAvatar?: (key: string) => Promise<string | null>\n}\n\nconst pick = createModal<Admin[], AdminPickerOptions & { multiple?: boolean }>(AdminPicker)\n\n/**\n * One administrator, chosen from a dialog.\n *\n * ```ts\n * const owner = await selectAdmin()\n * if (owner) task.assignedTo = owner.id\n * ```\n *\n * Resolves with `undefined` when the dialog was closed without choosing. Needs `users.view`,\n * which is deliberately separate from managing them: assigning work to somebody is not the\n * same as being allowed to edit their account.\n */\nexport async function selectAdmin(options: AdminPickerOptions = {}): Promise<Admin | undefined> {\n const chosen = await pick({ ...options, multiple: false })\n\n return chosen?.[0]\n}\n\n/**\n * Several administrators, ticked and confirmed.\n *\n * Resolves with `undefined` when the dialog was closed, and never with an empty list — the\n * confirming button is disabled until somebody is ticked, so \"none\" can only mean \"cancelled\".\n */\nexport function selectAdmins(options: AdminPickerOptions = {}): Promise<Admin[] | undefined> {\n return pick({ ...options, multiple: true })\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useAdmin, useI18n, useTranslate } from '@webx-ui/admin'\nimport { useAuth } from './session'\n\n/**\n * The corner of the header: who this is, which language they read the panel in, and the way\n * out.\n *\n * The language picker lives here rather than in a settings screen because it is a property of\n * the person, not of the site — and because somebody who has landed in a language they cannot\n * read needs it within reach, not three clicks into a section they cannot navigate.\n */\nwithDefaults(defineProps<{ signOutLabel?: string }>(), { signOutLabel: undefined })\n\nconst admin = useAdmin()\nconst auth = useAuth()\nconst i18n = useI18n()\nconst t = useTranslate('webx-auth')\n\nconst user = computed(() => admin.state.user)\n\n// One language is not a choice, and a menu that offers it is noise.\nconst languages = computed(() =>\n i18n.state.panelLocales.length > 1 ? i18n.state.panelLocales : [],\n)\n\nasync function choose(code: string): Promise<void> {\n if (code === i18n.state.locale) {\n return\n }\n\n await auth.setLocale(code)\n}\n</script>\n\n<template>\n <wx-dropdown v-if=\"user !== null\">\n <!--\n The avatar is the button. Wrapped in an action it was a small picture inside a box the\n same size as every icon beside it, which reads as one more tool rather than as who is\n signed in.\n -->\n <template #trigger>\n <button type=\"button\" class=\"wx-user-menu__trigger\" :title=\"user.name\">\n <wx-avatar :name=\"user.name\" size=\"md\" />\n </button>\n </template>\n\n <wx-dropdown-item disabled>\n <wx-text size=\"sm\">{{ user.email }}</wx-text>\n </wx-dropdown-item>\n\n <template v-if=\"languages.length > 0\">\n <wx-divider :spacing=\"4\" />\n\n <wx-dropdown-item disabled>\n <wx-text size=\"sm\" tone=\"muted\">{{ t('menu.language') }}</wx-text>\n </wx-dropdown-item>\n\n <wx-dropdown-item\n v-for=\"language in languages\"\n :key=\"language.code\"\n :icon=\"language.code === i18n.state.locale ? 'check' : undefined\"\n @click=\"choose(language.code)\"\n >\n {{ language.nativeName }}\n </wx-dropdown-item>\n\n <wx-divider :spacing=\"4\" />\n </template>\n\n <wx-dropdown-item icon=\"logout\" @click=\"auth.logout()\">\n {{ signOutLabel ?? t('menu.sign-out') }}\n </wx-dropdown-item>\n </wx-dropdown>\n</template>\n\n<style scoped>\n/*\n * The avatar is the whole button: no border, no background, nothing of the browser's own —\n * a bordered box around a round picture reads as one more tool in the row of icons, which is\n * what wrapping it in an action looked like in the first place.\n */\n.wx-user-menu__trigger {\n display: inline-flex;\n padding: 0;\n border: none;\n border-radius: var(--wx-radius-full);\n background: transparent;\n cursor: pointer;\n transition: opacity var(--wx-duration-fast) var(--wx-easing-standard);\n}\n\n.wx-user-menu__trigger:hover {\n opacity: 0.85;\n}\n\n.wx-user-menu__trigger:focus-visible {\n outline: 2px solid var(--wx-border-focus);\n outline-offset: 2px;\n}\n</style>\n","import { h, watch } from 'vue'\nimport type { Admin, AdminPlugin } from '@webx-ui/admin'\nimport LoginCard from './LoginCard.vue'\nimport { authMessages } from './messages'\nimport { createAuthSession, provideAuth } from './session'\n\nexport interface AuthOptions {\n /** Where the sign-in screen lives, inside the panel. */\n path?: string\n /** Props handed to the card — the labels, mostly. */\n card?: Record<string, unknown>\n}\n\n/**\n * Signing in, as a plugin rather than a section: it owns a route and a guard, and it has no\n * place in the menu.\n *\n * Installing it also tells the panel how to find out who is signed in, which is why the panel\n * itself knows nothing about authentication — it holds the answer, not the question.\n */\nexport function auth(options: AuthOptions = {}): AdminPlugin {\n const path = options.path ?? '/login'\n\n return {\n install(admin: Admin) {\n const session = createAuthSession(admin.context)\n\n // The floor under this module's labels: what the server has not translated, and what\n // a panel assembled without a server has to fall back on.\n admin.i18n.defaults('webx-auth', authMessages)\n\n provideAuth(admin.app, session)\n admin.context.useSessionLoader(() => session.me())\n\n admin.router.addRoute({\n path,\n name: 'webx.login',\n meta: { public: true },\n component: { render: () => h(LoginCard, { ...options.card }) },\n })\n\n admin.router.beforeEach((to) => {\n if (to.meta.public === true) {\n return true\n }\n\n if (admin.context.state.status !== 'unauthenticated') {\n return true\n }\n\n return { path, query: to.fullPath === '/' ? {} : { next: to.fullPath } }\n })\n\n // Both directions are driven by the state rather than by an event, because the card\n // cannot be relied on to announce its own success: signing in flips the panel to\n // `ready`, the shell swaps the branch it renders, and the card is unmounted before the\n // line after `await` runs.\n watch(\n () => admin.context.state.status,\n (status) => {\n const current = admin.router.currentRoute.value\n\n // A session can also end between requests — a sign-out in another tab, an account\n // switched off. The panel learns that from a 401 on whatever it asked for next,\n // which is not a navigation, so nothing would move without this.\n if (status === 'unauthenticated' && current.path !== path) {\n void admin.router.replace({ path, query: { next: current.fullPath } })\n\n return\n }\n\n if (status === 'ready' && current.path === path) {\n const next = current.query.next\n\n void admin.router.replace(typeof next === 'string' ? next : '/')\n }\n },\n )\n },\n }\n}\n\nexport { admins, type AdminsOptions } from './module'\nexport { createAdminsApi, type AdminsApi } from './admins'\nexport { selectAdmin, selectAdmins, type AdminPickerOptions } from './selectAdmin'\nexport { default as WxAdminsPage } from './AdminsPage.vue'\nexport { default as WxAdminList } from './AdminList.vue'\nexport type { Admin, AdminInput, AdminPage, AdminQuery, AdminRole, Role } from './types'\nexport { createAuthSession, provideAuth, useAuth, authKey } from './session'\nexport type { AuthSession, Credentials } from './session'\nexport { default as WxLoginCard } from './LoginCard.vue'\nexport { default as WxUserMenu } from './UserMenu.vue'\nexport { authMessages } from './messages'\n"],"names":["authKey","useAuth","session","inject","createAuthSession","admin","base","error","isUnauthenticated","credentials","body","code","provideAuth","app","props","__props","emit","__emit","auth","t","useTranslate","labels","computed","email","ref","password","rememberMe","busy","revealed","capsLock","message","errors","secondsLeft","emailField","useTemplateRef","countdown","onBeforeUnmount","stopCountdown","throttled","throttleNotice","submit","handle","HttpError","startCountdown","seconds","trackCapsLock","event","__expose","_createBlock","_component_wx_card","_createVNode","_component_wx_form","_component_wx_alert","_component_wx_form_item","_component_wx_input","$event","_component_wx_icon","_createElementVNode","_cache","_component_wx_checkbox","_component_wx_button","_createTextVNode","_toDisplayString","authMessages","createAdminsApi","data","query","id","input","seeded","useAuthMessages","i18n","useAdmin","resolve","dismiss","open","useModal","context","api","useI18n","roles","saving","form","photo","value","editing","roleOptions","one","localeOptions","all","watch","errorOf","field","save","payload","saved","toast","_unref","WxDialog","WxSpace","WxButton","_hoisted_1","_hoisted_2","_openBlock","_createElementBlock","_hoisted_3","_resolveDynamicComponent","_hoisted_4","WxFormItem","WxInput","WxSelect","WxCheckbox","WxText","page","loading","role","active","selected","avatars","last","stateOptions","columns","load","state","resolveAvatars","row","url","onRow","onSelection","_keys","rows","WxTable","_withCtx","WxAvatar","WxBadge","_Fragment","_renderList","WxActions","WxAction","list","edit","createModal","AdminDialog","canManage","remove","confirm","WxCard","_createSlots","AdminList","admins","options","path","h","AdminsPage","count","chosen","track","pick","AdminPicker","selectAdmin","selectAdmins","user","languages","choose","_component_wx_dropdown","_component_wx_avatar","_component_wx_dropdown_item","_component_wx_text","_component_wx_divider","language","LoginCard","to","status","current","next"],"mappings":"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"}
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Component } from 'vue';
|
|
2
|
+
import { AdminModule } from '@webx-ui/admin';
|
|
3
|
+
export interface AdminsOptions {
|
|
4
|
+
/** Where the section lives inside the panel. */
|
|
5
|
+
path?: string;
|
|
6
|
+
/**
|
|
7
|
+
* The field the photograph is picked with — `WxMediaField` in a panel that has the library.
|
|
8
|
+
*
|
|
9
|
+
* Handed in rather than imported: this package does not depend on the media one, and a panel
|
|
10
|
+
* without a library still edits everything else about a person.
|
|
11
|
+
*/
|
|
12
|
+
avatarField?: Component;
|
|
13
|
+
/** Turns an avatar key into an address. Omitted, rows and forms show initials. */
|
|
14
|
+
resolveAvatar?: (key: string) => Promise<string | null>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The administrators, as a section of the panel.
|
|
18
|
+
*
|
|
19
|
+
* The id matches the module the server reports, which is what makes the entry appear in the
|
|
20
|
+
* navigation: a section shows up when both halves are installed.
|
|
21
|
+
*/
|
|
22
|
+
export declare function admins(options?: AdminsOptions): AdminModule;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Admin } from './types';
|
|
2
|
+
export interface AdminPickerOptions {
|
|
3
|
+
/** Heading of the dialog, when the default is not specific enough. */
|
|
4
|
+
title?: string;
|
|
5
|
+
/** Turns an avatar key into an address. Omitted, rows show initials. */
|
|
6
|
+
resolveAvatar?: (key: string) => Promise<string | null>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* One administrator, chosen from a dialog.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* const owner = await selectAdmin()
|
|
13
|
+
* if (owner) task.assignedTo = owner.id
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* Resolves with `undefined` when the dialog was closed without choosing. Needs `users.view`,
|
|
17
|
+
* which is deliberately separate from managing them: assigning work to somebody is not the
|
|
18
|
+
* same as being allowed to edit their account.
|
|
19
|
+
*/
|
|
20
|
+
export declare function selectAdmin(options?: AdminPickerOptions): Promise<Admin | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* Several administrators, ticked and confirmed.
|
|
23
|
+
*
|
|
24
|
+
* Resolves with `undefined` when the dialog was closed, and never with an empty list — the
|
|
25
|
+
* confirming button is disabled until somebody is ticked, so "none" can only mean "cancelled".
|
|
26
|
+
*/
|
|
27
|
+
export declare function selectAdmins(options?: AdminPickerOptions): Promise<Admin[] | undefined>;
|
package/dist/style.css
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
.wx-login[data-v-1224c1f8]{width:100%;max-width:26rem}.wx-login__message[data-v-1224c1f8]{margin-block-end:var(--wx-space-4)}.wx-login__reveal[data-v-1224c1f8]{display:inline-flex;align-items:center;padding:0;color:var(--wx-text-muted);background:none;border:0;cursor:pointer}.wx-login__reveal[data-v-1224c1f8]:hover,.wx-login__reveal[aria-pressed=true][data-v-1224c1f8]{color:var(--wx-text-default)}
|
|
1
|
+
.wx-login[data-v-1224c1f8]{width:100%;max-width:26rem}.wx-login__message[data-v-1224c1f8]{margin-block-end:var(--wx-space-4)}.wx-login__reveal[data-v-1224c1f8]{display:inline-flex;align-items:center;padding:0;color:var(--wx-text-muted);background:none;border:0;cursor:pointer}.wx-login__reveal[data-v-1224c1f8]:hover,.wx-login__reveal[aria-pressed=true][data-v-1224c1f8]{color:var(--wx-text-default)}.wx-admin-form{container-type:inline-size}.wx-admin-form__grid{display:grid;grid-template-columns:200px minmax(0,1fr);gap:var(--wx-space-24)}.wx-admin-form__fields{display:flex;flex-direction:column;gap:var(--wx-space-12);min-width:0}@container (max-width: 520px){.wx-admin-form__grid{grid-template-columns:minmax(0,1fr)}.wx-admin-form__aside{max-width:200px}}.wx-admin-list{min-width:0}.wx-user-menu__trigger[data-v-f4dbd95e]{display:inline-flex;padding:0;border:none;border-radius:var(--wx-radius-full);background:transparent;cursor:pointer;transition:opacity var(--wx-duration-fast) var(--wx-easing-standard)}.wx-user-menu__trigger[data-v-f4dbd95e]:hover{opacity:.85}.wx-user-menu__trigger[data-v-f4dbd95e]:focus-visible{outline:2px solid var(--wx-border-focus);outline-offset:2px}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** The shapes `webx-ui/module-auth` answers with on its administrators endpoints. */
|
|
2
|
+
export interface AdminRole {
|
|
3
|
+
id: number;
|
|
4
|
+
slug: string;
|
|
5
|
+
name: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Role extends AdminRole {
|
|
8
|
+
permissions: string[];
|
|
9
|
+
/** How many administrators hold it — the number that makes a role safe or risky to change. */
|
|
10
|
+
users: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Admin {
|
|
13
|
+
id: number;
|
|
14
|
+
name: string;
|
|
15
|
+
email: string;
|
|
16
|
+
/**
|
|
17
|
+
* The media library's key, or `null`.
|
|
18
|
+
*
|
|
19
|
+
* A key and not an address, like everything else stored about a picture: where it lives is
|
|
20
|
+
* worked out where it is drawn, so moving the library changes nothing here.
|
|
21
|
+
*/
|
|
22
|
+
avatar: string | null;
|
|
23
|
+
is_super: boolean;
|
|
24
|
+
is_active: boolean;
|
|
25
|
+
locale: string | null;
|
|
26
|
+
last_login_at: string | null;
|
|
27
|
+
created_at: string | null;
|
|
28
|
+
roles: AdminRole[];
|
|
29
|
+
/**
|
|
30
|
+
* What a table row is, as far as `WxTable` is concerned.
|
|
31
|
+
*
|
|
32
|
+
* Without it this type does not extend `TableRow`, the table's generic falls back to its
|
|
33
|
+
* default, and every cell slot hands back `unknown`.
|
|
34
|
+
*/
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface AdminQuery {
|
|
38
|
+
q?: string;
|
|
39
|
+
role?: string | null;
|
|
40
|
+
active?: 'yes' | 'no' | null;
|
|
41
|
+
sort?: string;
|
|
42
|
+
page?: number;
|
|
43
|
+
per_page?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A page, flat — the shape `->paginate()` serialises and `WxTable` reads as it arrives.
|
|
47
|
+
*
|
|
48
|
+
* A resource collection nests the same numbers under `meta`; the client here flattens them so
|
|
49
|
+
* that no screen has to translate between the two.
|
|
50
|
+
*/
|
|
51
|
+
export interface AdminPage {
|
|
52
|
+
data: Admin[];
|
|
53
|
+
current_page: number;
|
|
54
|
+
last_page: number;
|
|
55
|
+
per_page: number;
|
|
56
|
+
total: number;
|
|
57
|
+
from: number | null;
|
|
58
|
+
to: number | null;
|
|
59
|
+
}
|
|
60
|
+
/** What a form sends. `password` left out is a password left alone. */
|
|
61
|
+
export interface AdminInput {
|
|
62
|
+
name: string;
|
|
63
|
+
email: string;
|
|
64
|
+
password?: string;
|
|
65
|
+
avatar?: string | null;
|
|
66
|
+
is_active?: boolean;
|
|
67
|
+
is_super?: boolean;
|
|
68
|
+
locale?: string | null;
|
|
69
|
+
roles?: number[];
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webx-ui/module-auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Signing in to a WebX UI admin panel: the screen, the session and the router guard. Pairs with webx-ui/module-auth on the server.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"vue-router": "^4.5.0"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@webx-ui/admin": "^0.2.
|
|
48
|
-
"@webx-ui/core": "^0.
|
|
47
|
+
"@webx-ui/admin": "^0.2.4",
|
|
48
|
+
"@webx-ui/core": "^0.16.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^24.10.1",
|