Merge pull request #457 from PretendoNetwork/feat/password-editing

Feat: password editing
This commit is contained in:
limes.pink
2026-09-08 20:59:45 +02:00
committed by GitHub
9 changed files with 248 additions and 7 deletions

View File

@@ -0,0 +1,176 @@
<script setup lang="ts">
import { Dialog } from 'reka-ui/namespaced';
import type { ApiAccountPasswordUpdateRequest } from '~~/shared/api-types';
const toasts = useToasts();
const authUtils = useAuthUtils();
const { t } = useI18n();
const { dialogContainer } = defineProps<{
dialogContainer: HTMLElement | null;
}>();
const open = defineModel<boolean>();
const oldPassword = ref('');
const newPassword = ref('');
const newPasswordConfirm = ref('');
const hasRequiredInfo = computed(() => {
return oldPassword.value && newPassword.value && newPasswordConfirm.value;
});
const emit = defineEmits<{
change: [];
}>();
const {
execute: executeUpdatePassword,
isLoading: isLoadingUpdatePassword
} = useAsync({
async handler() {
await apiFetch('/api/account/update-password', {
method: 'PATCH',
body: {
oldPassword: oldPassword.value,
newPassword: newPassword.value,
newPasswordConfirm: newPasswordConfirm.value
} satisfies ApiAccountPasswordUpdateRequest
});
open.value = false;
toasts.publish({
type: 'success',
text: t('account.settings.passwordModal.successNotice')
});
await new Promise(r => setTimeout(r, 3000));
emit('change');
authUtils.logout();
},
onError(error) {
const err = getApiError(error);
toasts.publish({
type: 'error',
text: err.message
});
}
});
</script>
<template>
<Dialog.Root
v-model:open="open"
>
<Dialog.Portal :to="dialogContainer ?? undefined">
<Dialog.Overlay />
<Dialog.Content
class="modal"
@interact-outside="(e) => {
if (isLoadingUpdatePassword) {
return e.preventDefault();
}
}"
>
<Dialog.Title>
{{ $t("account.settings.passwordModal.title") }}.
</Dialog.Title>
<Dialog.Description class="modal-caption">
<p>
{{
$t("account.settings.settingCards.passwordResetNotice")
}}
</p>
</Dialog.Description>
<div class="fieldsets-container">
<fieldset class="account-edit">
<Label for="old-password">{{ $t("account.settings.passwordModal.oldPassword") }}</Label>
<input
id="old-password"
v-model="oldPassword"
name="old-password"
type="password"
autocomplete="current-password"
required
>
</fieldset>
<fieldset class="account-edit">
<Label for="new-password">{{ $t("account.settings.passwordModal.newPassword") }}</Label>
<input
id="new-password"
v-model="newPassword"
name="new-password"
type="password"
autocomplete="new-password"
passwordrules="minlength: 6; maxlength: 16; max-consecutive: 2; allowed: [-!-~];"
pattern="[-!-~]{6,16}"
required
>
</fieldset>
<fieldset class="account-edit">
<Label for="new-password-confirm">{{ $t("account.settings.passwordModal.newPasswordConfirm") }}</Label>
<input
id="new-password-confirm"
v-model="newPasswordConfirm"
name="new-password-confirm"
type="password"
autocomplete="new-password"
passwordrules="minlength: 6; maxlength: 16; max-consecutive: 2; allowed: [-!-~];"
pattern="[-!-~]{6,16}"
required
>
</fieldset>
</div>
<div class="modal-button-wrapper">
<Dialog.Close
class="cancel"
:disabled="isLoadingUpdatePassword"
>
{{ $t("modals.cancel") }}
</Dialog.Close>
<button
:class="{action: true, disabled: isLoadingUpdatePassword || !hasRequiredInfo }"
:disabled="isLoadingUpdatePassword || !hasRequiredInfo"
@click="executeUpdatePassword"
>
<Loader v-if="isLoadingUpdatePassword" />
<span v-else>{{
$t("modals.confirm")
}}</span>
</button>
</div>
</Dialog.Content>
</Dialog.Portal><Dialog.Trigger class="update-trigger">
{{ $t('account.settings.update') }}
</Dialog.Trigger>
</Dialog.Root>
</template>
<style scoped>
.fieldsets-container {
display: grid;
grid-auto-flow: row;
grid-auto-rows: 1fr;
gap: 1rem
}
.update-trigger {
all: unset;
color: var(--accent-shade-1);
font-weight: bold;
cursor: pointer;
}
.update-trigger:hover {
background: none;
text-decoration: underline;
}
fieldset {
position: relative;
display: flex;
flex-flow: column;
height: min-content;
padding: 0;
gap: .25rem;
border: none;
}
</style>

View File

@@ -272,6 +272,14 @@
"caption": "To verify your email address, click on the link in the verification email.",
"lostEmail": "Can't find it?",
"resend": "Resend verification email"
},
"passwordModal": {
"title": "Change password",
"caption": "Input your new password.",
"oldPassword": "Current password",
"newPassword": "New password",
"newPasswordConfirm": "Confirm new password",
"successNotice": "Password updated successfully"
}
},
"emailVerification": {

View File

@@ -66,6 +66,7 @@ const dialogContainer = ref(null);
const deleteModalOpen = ref(false);
const profileEditModalOpen = ref(false);
const emailEditModalOpen = ref(false);
const passwordEditModalOpen = ref(false);
const selectedServerEnv = ref<'dev' | 'test' | 'prod' | undefined>(
profile.value?.serverAccessLevel
);
@@ -477,6 +478,10 @@ customSeoMeta({ subsection: 'account' });
<p class="value">
●●●●●●●●
</p>
<UpdatePasswordModal
v-model="passwordEditModalOpen"
:dialog-container="dialogContainer"
/>
</li>
</ul>
</div>
@@ -554,7 +559,8 @@ customSeoMeta({ subsection: 'account' });
hidden: !(
deleteModalOpen ||
profileEditModalOpen ||
emailEditModalOpen
emailEditModalOpen ||
passwordEditModalOpen
),
}"
>