diff --git a/Dockerfile b/Dockerfile index ebbee2b..5ffa0e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,9 @@ COPY --from=build /app/drizzle/migrations /app/drizzle/migrations # Copy entrypoint script COPY --from=build /app/scripts/start.ts /app/scripts/start.ts +# Copy the account recovery script (bun run recover ) +COPY --from=build /app/scripts/recover.ts /app/scripts/recover.ts + # Copy package.json and lockfile for production dependency install COPY --from=build /app/package.json /app/package.json COPY --from=build /app/bun.lock* ./ diff --git a/app/components/Dialog/CreateUser.vue b/app/components/Dialog/CreateUser.vue index 61014cc..d5a3acb 100644 --- a/app/components/Dialog/CreateUser.vue +++ b/app/components/Dialog/CreateUser.vue @@ -1,46 +1,36 @@ @@ -90,66 +80,80 @@ const submit = async () => {
-

Create user

+

+ {{ link ? 'Account created' : 'Create user' }} +

- Creates the account right away. Nothing is sent to them, so pass the password on yourself. + {{ link + ? `Send ${form.name.trim()} this link. They open it, choose a password and are in.` + : 'Creates the account without a password. You get a link for them to choose their own, so you never know it.' }}

-
- - -
- -
- - -
- -
- -
- - - + -
- - -
+ diff --git a/app/components/Dialog/RecoverAccount.vue b/app/components/Dialog/RecoverAccount.vue new file mode 100644 index 0000000..ccf12e2 --- /dev/null +++ b/app/components/Dialog/RecoverAccount.vue @@ -0,0 +1,174 @@ + + + diff --git a/app/components/Dialog/index.vue b/app/components/Dialog/index.vue index f6447c0..d3e51bd 100644 --- a/app/components/Dialog/index.vue +++ b/app/components/Dialog/index.vue @@ -7,6 +7,7 @@ import DeleteAccount from './DeleteAccount.vue'; import Confirm from './Confirm.vue'; import CreateUser from './CreateUser.vue'; import CreateInvite from './CreateInvite.vue'; +import RecoverAccount from './RecoverAccount.vue'; import { DialogType } from '~/composables/useDialog'; @@ -74,6 +75,8 @@ onUnmounted(() => { +
diff --git a/app/composables/useDialog.ts b/app/composables/useDialog.ts index 4553615..2e6e355 100644 --- a/app/composables/useDialog.ts +++ b/app/composables/useDialog.ts @@ -9,6 +9,7 @@ export enum DialogType { DeleteAccount = 'delete-account', CreateUser = 'create-user', CreateInvite = 'create-invite', + RecoverAccount = 'recover-account', } interface DialogOptions { diff --git a/app/pages/auth/login.vue b/app/pages/auth/login.vue index ea41cc0..bde3d3e 100644 --- a/app/pages/auth/login.vue +++ b/app/pages/auth/login.vue @@ -10,6 +10,10 @@ definePageMeta({ const to = useRoute().query.to as string | undefined; +// set by the recovery page after a new password was chosen: 'keys' when saved +// API keys were deleted along the way, anything else when there were none +const recovered = useRoute().query.recovered as string | undefined; + if (session.value !== null) { await navigateTo(to ?? '/'); } @@ -236,6 +240,20 @@ const submitTwoFactor = async () => {

+
+ + + Your password is changed. + + Log in with the new one. + + + +
+
@@ -308,7 +326,14 @@ const submitTwoFactor = async () => {
-

+

+ + Forgot your password? + +

+ +

New to Veridian? diff --git a/app/pages/auth/recover/[token].vue b/app/pages/auth/recover/[token].vue new file mode 100644 index 0000000..3f6d9d9 --- /dev/null +++ b/app/pages/auth/recover/[token].vue @@ -0,0 +1,231 @@ + + + diff --git a/app/pages/auth/recover/index.vue b/app/pages/auth/recover/index.vue new file mode 100644 index 0000000..dd4caec --- /dev/null +++ b/app/pages/auth/recover/index.vue @@ -0,0 +1,69 @@ + + + diff --git a/app/pages/auth/setup.vue b/app/pages/auth/setup.vue index 5202dea..fb7874c 100644 --- a/app/pages/auth/setup.vue +++ b/app/pages/auth/setup.vue @@ -157,7 +157,7 @@ const submit = async () => { Someone else just created the first account, so this instance already has its administrator. Nothing was created for you.

- + Go to log in
diff --git a/app/pages/settings/instance/users.vue b/app/pages/settings/instance/users.vue index 65ee780..22a0da0 100644 --- a/app/pages/settings/instance/users.vue +++ b/app/pages/settings/instance/users.vue @@ -9,6 +9,8 @@ type UserRow = { role: 'admin' | 'user'; banned: boolean; twoFactorEnabled: boolean; + // created by an admin and has not set a password yet + pending: boolean; createdAt: string; lastActiveAt: string | null; }; @@ -139,6 +141,12 @@ const signOutEverywhere = (user: UserRow) => run(user, `${user.name} could not b return !error; }); +// The dialog makes the link and shows it, nothing in the list changes. For an +// account that has no password yet it is a new setup link instead. +const recoverAccount = (user: UserRow) => { + openDialog(DialogType.RecoverAccount, undefined, { user }); +}; + const resetTwoFactor = (user: UserRow) => confirmThen({ title: `Reset two-factor for ${user.name}?`, message: 'Turns two-factor sign-in off for this account and throws away its backup codes. They sign in with only their password until they set it up again.', @@ -196,9 +204,8 @@ const createInvite = () => { }; const openCreateUser = () => { - openDialog(DialogType.CreateUser, async (result) => { - if (result.ok === false) return; - + // the dialog can be closed after the account was made, so always reload + openDialog(DialogType.CreateUser, async () => { await loadUsers(); }); }; @@ -278,9 +285,9 @@ const openCreateUser = () => { + :class="u.banned ? 'text-red-400' : u.pending ? 'text-amber-400' : 'text-green-400'"> - {{ u.banned ? 'Disabled' : 'Active' }} + {{ u.banned ? 'Disabled' : u.pending ? 'Invited' : 'Active' }} { Make admin +