feat: added change password to about/account view

This commit is contained in:
2026-08-31 17:46:37 +02:00
parent d981cde758
commit f82dc34723
6 changed files with 72 additions and 23 deletions
+9 -9
View File
@@ -33,7 +33,7 @@
"prettier": "3.9.5", "prettier": "3.9.5",
"typescript": "~6.0.0", "typescript": "~6.0.0",
"vite": "^8.1.5", "vite": "^8.1.5",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.3.0",
"vite-plugin-vue-devtools": "^8.1.5", "vite-plugin-vue-devtools": "^8.1.5",
"vitest": "^4.1.10", "vitest": "^4.1.10",
"vue-eslint-parser": "^10.4.1", "vue-eslint-parser": "^10.4.1",
@@ -9563,17 +9563,17 @@
} }
}, },
"node_modules/vite-plugin-pwa": { "node_modules/vite-plugin-pwa": {
"version": "1.2.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz",
"integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"debug": "^4.3.6", "debug": "^4.3.6",
"pretty-bytes": "^6.1.1", "pretty-bytes": "^6.1.1",
"tinyglobby": "^0.2.10", "tinyglobby": "^0.2.10",
"workbox-build": "^7.4.0", "workbox-build": "^7.4.1",
"workbox-window": "^7.4.0" "workbox-window": "^7.4.1"
}, },
"engines": { "engines": {
"node": ">=16.0.0" "node": ">=16.0.0"
@@ -9583,9 +9583,9 @@
}, },
"peerDependencies": { "peerDependencies": {
"@vite-pwa/assets-generator": "^1.0.0", "@vite-pwa/assets-generator": "^1.0.0",
"vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
"workbox-build": "^7.4.0", "workbox-build": "^7.4.1",
"workbox-window": "^7.4.0" "workbox-window": "^7.4.1"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@vite-pwa/assets-generator": { "@vite-pwa/assets-generator": {
+1 -1
View File
@@ -41,7 +41,7 @@
"prettier": "3.9.5", "prettier": "3.9.5",
"typescript": "~6.0.0", "typescript": "~6.0.0",
"vite": "^8.1.5", "vite": "^8.1.5",
"vite-plugin-pwa": "^1.2.0", "vite-plugin-pwa": "^1.3.0",
"vite-plugin-vue-devtools": "^8.1.5", "vite-plugin-vue-devtools": "^8.1.5",
"vitest": "^4.1.10", "vitest": "^4.1.10",
"vue-eslint-parser": "^10.4.1", "vue-eslint-parser": "^10.4.1",
+2 -6
View File
@@ -17,8 +17,8 @@ const listsStore = useListsStore()
}}</span> }}</span>
</RouterLink> </RouterLink>
<RouterLink to="/about" class="nav-item" active-class="is-active"> <RouterLink to="/about" class="nav-item" active-class="is-active">
<span class="nav-icon"></span> <span class="nav-icon"></span>
<span class="nav-label">About</span> <span class="nav-label">Account</span>
</RouterLink> </RouterLink>
</nav> </nav>
</template> </template>
@@ -55,10 +55,6 @@ const listsStore = useListsStore()
line-height: 1; line-height: 1;
} }
.nav-item.is-active {
color: var(--c-accent-strong);
}
.nav-badge { .nav-badge {
position: absolute; position: absolute;
top: 0.35rem; top: 0.35rem;
+11 -1
View File
@@ -1,7 +1,8 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { loginApi, refreshApi, logoutApi, logoutAllApi } from '@/api/auth' import { loginApi, refreshApi, logoutApi, logoutAllApi } from '@/api/auth'
import type { LoginPayload, TokenPair } from '@/types/auth' import type { AccessTokenClaims, LoginPayload, TokenPair } from '@/types/auth'
import { decodeJwtPayload } from '@/utils/jwt'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
const accessToken = ref<string | null>(localStorage.getItem('access_token')) const accessToken = ref<string | null>(localStorage.getItem('access_token'))
@@ -11,6 +12,12 @@ export const useAuthStore = defineStore('auth', () => {
const isAuthenticated = computed(() => !!accessToken.value) const isAuthenticated = computed(() => !!accessToken.value)
const currentUser = computed(() =>
accessToken.value ? decodeJwtPayload<AccessTokenClaims>(accessToken.value) : null,
)
const email = computed(() => currentUser.value?.email ?? null)
const username = computed(() => currentUser.value?.name ?? null)
function setTokens(tokens: TokenPair) { function setTokens(tokens: TokenPair) {
accessToken.value = tokens.access_token accessToken.value = tokens.access_token
refreshToken.value = tokens.refresh_token refreshToken.value = tokens.refresh_token
@@ -89,6 +96,9 @@ export const useAuthStore = defineStore('auth', () => {
isLoading, isLoading,
error, error,
isAuthenticated, isAuthenticated,
currentUser,
email,
username,
setTokens, setTokens,
clearTokens, clearTokens,
login, login,
+12
View File
@@ -11,3 +11,15 @@ export interface TokenPair {
access_token: string access_token: string
refresh_token: string refresh_token: string
} }
export interface AccessTokenClaims {
user_id: string
email: string
name: string
exp: number
}
export interface ChangePasswordPayload {
old_password: string
new_password: string
}
+37 -6
View File
@@ -1,8 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from 'vue' import { onMounted, ref } from 'vue'
import { useListsStore } from '@/stores/lists' import { useListsStore } from '@/stores/lists'
import { useAuthStore } from '@/stores/auth'
import ChangePasswordModal from '@/components/ChangePasswordModal.vue'
const listsStore = useListsStore() const listsStore = useListsStore()
const authStore = useAuthStore()
const showChangePassword = ref(false)
onMounted(() => { onMounted(() => {
listsStore.loadLists() listsStore.loadLists()
@@ -11,11 +16,25 @@ onMounted(() => {
<template> <template>
<main class="page about"> <main class="page about">
<h1>About dttmr</h1> <h1 v-if="authStore.username">Hi, {{ authStore.username }}</h1>
<p> <h1 v-else>Account</h1>
An offline-first progressive web app for shared lists. Every change you make is saved
instantly on this device and pushed to the server as soon as you're back online. <section class="card info-card">
</p> <h4>Users</h4>
<p class="row">
<span>Signed in as</span>
<strong>{{ authStore.email ?? 'Unknown' }}</strong>
</p>
<button
type="button"
class="btn btn-secondary change-password-btn"
@click="showChangePassword = true"
>
Change password
</button>
</section>
<h1>Pending changes</h1>
<section class="card info-card"> <section class="card info-card">
<h4>Sync status</h4> <h4>Sync status</h4>
@@ -28,6 +47,8 @@ onMounted(() => {
<strong>{{ listsStore.isSyncing ? 'Yes' : 'No' }}</strong> <strong>{{ listsStore.isSyncing ? 'Yes' : 'No' }}</strong>
</p> </p>
</section> </section>
<ChangePasswordModal v-if="showChangePassword" @close="showChangePassword = false" />
</main> </main>
</template> </template>
@@ -47,6 +68,10 @@ onMounted(() => {
padding: 1rem 1.1rem; padding: 1rem 1.1rem;
} }
.info-card + .info-card {
margin-top: 1rem;
}
.info-card h4 { .info-card h4 {
font-size: 0.85rem; font-size: 0.85rem;
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
@@ -63,4 +88,10 @@ onMounted(() => {
.row strong { .row strong {
color: var(--c-heading); color: var(--c-heading);
} }
.change-password-btn {
width: auto;
margin-top: 0.6rem;
padding: 0.5rem 1rem;
}
</style> </style>