Implement password change function

This commit is contained in:
2025-07-16 08:42:09 +02:00
parent 37f381c884
commit f518723a0e
10 changed files with 219 additions and 48 deletions

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
import type { Ref } from 'vue'
import SlInput from '@shoelace-style/shoelace/dist/components/input/input.js'
export function setFieldValidation(field: Ref<SlInput>, errorMessage: string = '') {
// Set validation on a field
field.value?.setCustomValidity(errorMessage)
field.value?.reportValidity()
}

View File

@ -1,7 +1,7 @@
// This file is auto-generated by @hey-api/openapi-ts
import { type Options as ClientOptions, type TDataShape, type Client, urlSearchParamsBodySerializer } from './client';
import type { GetHealthHealthGetData, GetHealthHealthGetResponses, GetAuditLogApiV1AuditGetData, GetAuditLogApiV1AuditGetResponses, GetAuditLogApiV1AuditGetErrors, LoginForAccessTokenApiV1TokenPostData, LoginForAccessTokenApiV1TokenPostResponses, LoginForAccessTokenApiV1TokenPostErrors, RefreshTokenApiV1RefreshPostData, RefreshTokenApiV1RefreshPostResponses, RefreshTokenApiV1RefreshPostErrors, GetClientsApiV1ClientsGetData, GetClientsApiV1ClientsGetResponses, CreateClientApiV1ClientsPostData, CreateClientApiV1ClientsPostResponses, CreateClientApiV1ClientsPostErrors, GetClientsTerseApiV1ClientsTerseGetData, GetClientsTerseApiV1ClientsTerseGetResponses, QueryClientsApiV1QueryClientsGetData, QueryClientsApiV1QueryClientsGetResponses, QueryClientsApiV1QueryClientsGetErrors, DeleteClientApiV1ClientsIdDeleteData, DeleteClientApiV1ClientsIdDeleteResponses, DeleteClientApiV1ClientsIdDeleteErrors, GetClientApiV1ClientsIdGetData, GetClientApiV1ClientsIdGetResponses, GetClientApiV1ClientsIdGetErrors, UpdateClientApiV1ClientsIdPutData, UpdateClientApiV1ClientsIdPutResponses, UpdateClientApiV1ClientsIdPutErrors, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteData, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteResponses, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteErrors, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutData, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutResponses, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutErrors, UpdateClientPoliciesApiV1ClientsIdPoliciesPutData, UpdateClientPoliciesApiV1ClientsIdPoliciesPutResponses, UpdateClientPoliciesApiV1ClientsIdPoliciesPutErrors, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutData, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutErrors, GetSystemStatsApiV1StatsGetData, GetSystemStatsApiV1StatsGetResponses, GetSecretNamesApiV1SecretsGetData, GetSecretNamesApiV1SecretsGetResponses, AddSecretApiV1SecretsPostData, AddSecretApiV1SecretsPostResponses, AddSecretApiV1SecretsPostErrors, DeleteSecretApiV1SecretsNameDeleteData, DeleteSecretApiV1SecretsNameDeleteResponses, DeleteSecretApiV1SecretsNameDeleteErrors, GetSecretApiV1SecretsNameGetData, GetSecretApiV1SecretsNameGetResponses, GetSecretApiV1SecretsNameGetErrors, UpdateSecretApiV1SecretsNamePutData, UpdateSecretApiV1SecretsNamePutResponses, UpdateSecretApiV1SecretsNamePutErrors, GetSecretGroupsApiV1SecretsGroupsGetData, GetSecretGroupsApiV1SecretsGroupsGetResponses, GetSecretGroupsApiV1SecretsGroupsGetErrors, AddSecretGroupApiV1SecretsGroupsPostData, AddSecretGroupApiV1SecretsGroupsPostResponses, AddSecretGroupApiV1SecretsGroupsPostErrors, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors, GetSecretGroupApiV1SecretsGroupsGroupPathGetData, GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses, GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors, DeleteGroupIdApiV1SecretsGroupIdDeleteData, DeleteGroupIdApiV1SecretsGroupIdDeleteResponses, DeleteGroupIdApiV1SecretsGroupIdDeleteErrors, AssignSecretGroupApiV1SecretsSetGroupPostData, AssignSecretGroupApiV1SecretsSetGroupPostResponses, AssignSecretGroupApiV1SecretsSetGroupPostErrors, MoveGroupApiV1SecretsMoveGroupGroupNamePostData, MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses, MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors } from './types.gen';
import type { GetHealthHealthGetData, GetHealthHealthGetResponses, GetAuditLogApiV1AuditGetData, GetAuditLogApiV1AuditGetResponses, GetAuditLogApiV1AuditGetErrors, LoginForAccessTokenApiV1TokenPostData, LoginForAccessTokenApiV1TokenPostResponses, LoginForAccessTokenApiV1TokenPostErrors, RefreshTokenApiV1RefreshPostData, RefreshTokenApiV1RefreshPostResponses, RefreshTokenApiV1RefreshPostErrors, ChangePasswordApiV1PasswordPostData, ChangePasswordApiV1PasswordPostResponses, ChangePasswordApiV1PasswordPostErrors, GetClientsApiV1ClientsGetData, GetClientsApiV1ClientsGetResponses, CreateClientApiV1ClientsPostData, CreateClientApiV1ClientsPostResponses, CreateClientApiV1ClientsPostErrors, GetClientsTerseApiV1ClientsTerseGetData, GetClientsTerseApiV1ClientsTerseGetResponses, QueryClientsApiV1QueryClientsGetData, QueryClientsApiV1QueryClientsGetResponses, QueryClientsApiV1QueryClientsGetErrors, DeleteClientApiV1ClientsIdDeleteData, DeleteClientApiV1ClientsIdDeleteResponses, DeleteClientApiV1ClientsIdDeleteErrors, GetClientApiV1ClientsIdGetData, GetClientApiV1ClientsIdGetResponses, GetClientApiV1ClientsIdGetErrors, UpdateClientApiV1ClientsIdPutData, UpdateClientApiV1ClientsIdPutResponses, UpdateClientApiV1ClientsIdPutErrors, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteData, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteResponses, DeleteSecretFromClientApiV1ClientsIdSecretsSecretNameDeleteErrors, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutData, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutResponses, AddSecretToClientApiV1ClientsIdSecretsSecretNamePutErrors, UpdateClientPoliciesApiV1ClientsIdPoliciesPutData, UpdateClientPoliciesApiV1ClientsIdPoliciesPutResponses, UpdateClientPoliciesApiV1ClientsIdPoliciesPutErrors, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutData, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutResponses, UpdateClientPublicKeyApiV1ClientsIdPublicKeyPutErrors, GetSystemStatsApiV1StatsGetData, GetSystemStatsApiV1StatsGetResponses, GetSecretNamesApiV1SecretsGetData, GetSecretNamesApiV1SecretsGetResponses, AddSecretApiV1SecretsPostData, AddSecretApiV1SecretsPostResponses, AddSecretApiV1SecretsPostErrors, DeleteSecretApiV1SecretsNameDeleteData, DeleteSecretApiV1SecretsNameDeleteResponses, DeleteSecretApiV1SecretsNameDeleteErrors, GetSecretApiV1SecretsNameGetData, GetSecretApiV1SecretsNameGetResponses, GetSecretApiV1SecretsNameGetErrors, UpdateSecretApiV1SecretsNamePutData, UpdateSecretApiV1SecretsNamePutResponses, UpdateSecretApiV1SecretsNamePutErrors, GetSecretGroupsApiV1SecretsGroupsGetData, GetSecretGroupsApiV1SecretsGroupsGetResponses, GetSecretGroupsApiV1SecretsGroupsGetErrors, AddSecretGroupApiV1SecretsGroupsPostData, AddSecretGroupApiV1SecretsGroupsPostResponses, AddSecretGroupApiV1SecretsGroupsPostErrors, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteData, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteResponses, DeleteSecretGroupApiV1SecretsGroupsGroupPathDeleteErrors, GetSecretGroupApiV1SecretsGroupsGroupPathGetData, GetSecretGroupApiV1SecretsGroupsGroupPathGetResponses, GetSecretGroupApiV1SecretsGroupsGroupPathGetErrors, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutData, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutResponses, UpdateSecretGroupApiV1SecretsGroupsGroupPathPutErrors, DeleteGroupIdApiV1SecretsGroupIdDeleteData, DeleteGroupIdApiV1SecretsGroupIdDeleteResponses, DeleteGroupIdApiV1SecretsGroupIdDeleteErrors, AssignSecretGroupApiV1SecretsSetGroupPostData, AssignSecretGroupApiV1SecretsSetGroupPostResponses, AssignSecretGroupApiV1SecretsSetGroupPostErrors, MoveGroupApiV1SecretsMoveGroupGroupNamePostData, MoveGroupApiV1SecretsMoveGroupGroupNamePostResponses, MoveGroupApiV1SecretsMoveGroupGroupNamePostErrors } from './types.gen';
import { client as _heyApiClient } from './client.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = ClientOptions<TData, ThrowOnError> & {
@ -83,6 +83,28 @@ export class SshecretAdmin {
});
}
/**
* Change Password
* Change user password
*/
public static changePasswordApiV1PasswordPost<ThrowOnError extends boolean = false>(options: Options<ChangePasswordApiV1PasswordPostData, ThrowOnError>) {
return (options.client ?? _heyApiClient).post<ChangePasswordApiV1PasswordPostResponses, ChangePasswordApiV1PasswordPostErrors, ThrowOnError>({
responseType: 'json',
security: [
{
scheme: 'bearer',
type: 'http'
}
],
url: '/api/v1/password',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
}
/**
* Get Clients
* Get clients.

View File

@ -568,6 +568,25 @@ export type UpdatePoliciesRequest = {
sources: Array<string>;
};
/**
* UserPasswordChange
* Model for changing the password of a user.
*/
export type UserPasswordChange = {
/**
* Current Password
*/
current_password: string;
/**
* New Password
*/
new_password: string;
/**
* New Password Confirm
*/
new_password_confirm: string;
};
/**
* ValidationError
*/
@ -712,6 +731,29 @@ export type RefreshTokenApiV1RefreshPostResponses = {
export type RefreshTokenApiV1RefreshPostResponse = RefreshTokenApiV1RefreshPostResponses[keyof RefreshTokenApiV1RefreshPostResponses];
export type ChangePasswordApiV1PasswordPostData = {
body: UserPasswordChange;
path?: never;
query?: never;
url: '/api/v1/password';
};
export type ChangePasswordApiV1PasswordPostErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ChangePasswordApiV1PasswordPostError = ChangePasswordApiV1PasswordPostErrors[keyof ChangePasswordApiV1PasswordPostErrors];
export type ChangePasswordApiV1PasswordPostResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type GetClientsApiV1ClientsGetData = {
body?: never;
path?: never;

View File

@ -0,0 +1,113 @@
<template>
<div class="space-y-4">
<form @submit.prevent="changePassword" ref="passwordChangeForm">
<sl-input
label="Current password"
type="password"
autocomplete="current-password"
placeholder="Current Password"
required
help-text="Enter your current password"
:value="currentPassword"
@input="checkCurrent"
@sl-input="currentPassword = $event.target.value"
ref="currentPasswordField"
password-toggle
></sl-input>
<sl-input
label="New Password"
type="password"
required
placeholder="New Password"
autocomplete="new-password"
help-text="Enter your new password"
:value="newPassword"
@sl-input="newPassword = $event.target.value"
ref="newPasswordField"
password-toggle
></sl-input>
<sl-input
label="New Password (repeat)"
type="password"
required
placeholder="New Password"
autocomplete="new-password"
help-text="Confirm your new password by typing it again"
:value="newPasswordConfirm"
@sl-input="newPasswordConfirm = $event.target.value"
@blur="checkPasswords"
ref="newPasswordFieldConfirm"
password-toggle
></sl-input>
<sl-button class="mr-4" type="submit" variant="primary">Change Password</sl-button>
<sl-button variant="default" @click="cancelChangePassword">Cancel</sl-button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useAlertsStore } from '@/store/useAlertsStore'
import { SshecretAdmin } from '@/client'
import type { UserPasswordChange } from '@/client'
import { assertSdkResponseOk } from '@/api/assertSdkResponseOk'
import { ApiError, ValidationError } from '@/api/errors'
import { setFieldValidation } from '@/api/validation'
const currentPassword = ref<string>('')
const newPassword = ref<string>('')
const newPasswordConfirm = ref<string>('')
const passwordChangeForm = ref<HTMLFormElement>()
const currentPasswordField = ref<SlInput>()
const newPasswordField = ref<SlInput>()
const newPasswordFieldConfirm = ref<Slinput>()
const alerts = useAlertsStore()
const emit = defineEmits<{ (e: 'changed'): void; (e: 'cancel'): void }>()
function checkCurrent() {
setFieldValidation(currentPasswordField)
currentPasswordField.value.reportValidity()
}
function checkPasswords() {
if (newPassword.value !== newPasswordConfirm.value) {
setFieldValidation(newPasswordFieldConfirm, 'Passwords do not match match')
} else {
setFieldValidation(newPasswordFieldConfirm)
}
}
function resetForm() {
passwordChangeForm.value?.reset()
}
function cancelChangePassword() {
resetForm()
emit('cancel')
}
async function changePassword() {
const data: UserPasswordChange = {
current_password: currentPassword.value,
new_password: newPassword.value,
new_password_confirm: newPasswordConfirm.value,
}
const response = await SshecretAdmin.changePasswordApiV1PasswordPost({ body: data })
try {
assertSdkResponseOk(response)
emit('changed')
resetForm()
} catch (err) {
if (err instanceof ValidationError) {
// This should be caught in js, but we might as well
setFieldValidation(newPasswordFieldConfirm, 'Passwords do not match match')
} else if (err instanceof ApiError && err.message.includes('Invalid current password')) {
setFieldValidation(currentPasswordField, 'Invalid current password')
} else {
alerts.showAlert(err.message, 'error', 'Error changing password')
}
}
}
</script>

View File

@ -69,11 +69,8 @@ import type { Ref } from 'vue'
import { isIP } from 'is-ip'
import isCidr from 'is-cidr'
import '@shoelace-style/shoelace/dist/components/button/button.js'
import '@shoelace-style/shoelace/dist/components/input/input.js'
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
import '@shoelace-style/shoelace/dist/components/tag/tag.js'
import '@shoelace-style/shoelace/dist/components/textarea/textarea.js'
import { setFieldValidation } from '@/api/validation'
import type { ClientCreate } from '@/client/types.gen'
const name = ref('')
@ -111,12 +108,6 @@ watch(
{ immediate: true },
)
function setFieldValidation(field: Ref<HTMLSlInputElement>, errorMessage: string = '') {
// Set validation on a field
field.value?.setCustomValidity(errorMessage)
field.value?.reportValidity()
}
function addPolicy() {
if (!sourcePrefix.value) {
setFieldValidation(sourceField)

View File

@ -69,6 +69,7 @@ import isCidr from 'is-cidr'
import { assertSdkResponseOk } from '@/api/AssertSdkResponseOk'
import type { ClientCreate } from '@/client/types.gen'
import { SshecretAdmin } from '@/client/sdk.gen'
import { setFieldValidation } from '@/api/validation'
const name = ref('')
const description = ref('')
@ -81,12 +82,6 @@ const sourceField = ref<HTMLSlInputElement>()
const publicKeyField = ref<HTMLSlInputElement>()
const clientCreateForm = ref<HTMLElement>()
function setFieldValidation(field: Ref<HTMLSlInputElement>, errorMessage: string = '') {
// Set validation on a field
field.value?.setCustomValidity(errorMessage)
field.value?.reportValidity()
}
function addPolicy() {
if (!sourcePrefix.value) {
setFieldValidation(sourceField)

View File

@ -15,44 +15,43 @@
<sl-button variant="default" size="small" circle slot="trigger">
<sl-avatar label="User avatar"></sl-avatar>
</sl-button>
<sl-menu-item>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
>Change Password</a
>
</sl-menu-item>
<sl-menu-item>
<a
href="#"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white"
role="menuitem"
@click="logout"
>Logout</a
>
</sl-menu-item>
<sl-menu>
<sl-menu-item>{{ auth.username }}</sl-menu-item>
<sl-divider></sl-divider>
<sl-menu-item @click="showDrawer">Change Password</sl-menu-item>
<sl-menu-item @click="logout"> Logout </sl-menu-item>
</sl-menu>
</sl-dropdown>
</div>
</div>
</nav>
</header>
<Drawer label="Change Password" :open="showPasswordDrawer" @hide="showPasswordDrawer = false">
<ChangePassword @changed="logout" @cancel="showPasswordDrawer = false" />
</Drawer>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import '@shoelace-style/shoelace/dist/components/button/button.js'
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
import '@shoelace-style/shoelace/dist/components/dropdown/dropdown.js'
import '@shoelace-style/shoelace/dist/components/avatar/avatar.js'
import { useAuthTokenStore } from '@/store/auth'
import Drawer from '@/components/common/Drawer.vue'
import Dialog from '@/components/common/Dialog.vue'
import ChangePassword from '@/components/auth/ChangePassword.vue'
const auth = useAuthTokenStore()
const router = useRouter()
const showPasswordDrawer = ref<boolean>(false)
function logout() {
auth.logout()
router.push('/login')
}
function showDrawer() {
showPasswordDrawer.value = true
}
</script>
<style scoped>

View File

@ -63,6 +63,7 @@ import { ref } from 'vue'
import { generateRandomPassword } from '@/api/password'
import AddSecretsToClients from '@/components/secrets/AddSecretToClients.vue'
import ClientSelectDropdown from '@/components/clients/ClientSelectDropdown.vue'
import { setFieldValidation } from '@/api/validation'
const props = defineProps<{ group?: string }>()
@ -77,12 +78,6 @@ const secretLength = ref(8)
const autoGenerate = ref(false)
const selectedClients = ref<string[]>()
function setFieldValidation(field: Ref<HTMLSlInputElement>, errorMessage: string = '') {
// Set validation on a field
field.value?.setCustomValidity(errorMessage)
field.value?.reportValidity()
}
function generatePassword() {
const password = generateRandomPassword(secretLength.value)
secretValue.value = password

View File

@ -15,6 +15,7 @@ export const useAuthTokenStore = defineStore('authtoken', {
accessToken: '' as string,
refreshToken: '' as string,
isLoggedIn: false,
username: '' as string,
}),
actions: {
async login(username: string, password: string): Promise<boolean> {
@ -28,6 +29,7 @@ export const useAuthTokenStore = defineStore('authtoken', {
this.isLoggedIn = true
localStorage.setItem('accessToken', accessToken)
localStorage.setItem('refreshToken', refreshToken)
localStorage.setItem('username', username)
setAuthToken(this.accessToken)
return true
}
@ -59,9 +61,11 @@ export const useAuthTokenStore = defineStore('authtoken', {
// Load token from user storage.
const accessToken = localStorage.getItem('accessToken')
const refreshToken = localStorage.getItem('refreshToken')
if (accessToken && refreshToken) {
const username = localStorage.getItem('username')
if (accessToken && refreshToken && username) {
this.accessToken = accessToken
this.refreshToken = refreshToken
this.username = username
this.isLoggedIn = true
setAuthToken(accessToken)
}
@ -69,6 +73,7 @@ export const useAuthTokenStore = defineStore('authtoken', {
logout() {
this.accessToken = ''
this.refreshToken = ''
this.username = ''
this.isLoggedIn = false
localStorage.removeItem('accessToken')
localStorage.removeItem('refreshToken')