Skip to content

Commit

Permalink
add password changing
Browse files Browse the repository at this point in the history
  • Loading branch information
ZerNico committed May 30, 2023
1 parent 2fd1943 commit ac46240
Show file tree
Hide file tree
Showing 15 changed files with 285 additions and 41 deletions.
30 changes: 30 additions & 0 deletions apps/api/src/logto/management-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,34 @@ export class ManagementApiClient {
})
return response
}

public async verifyPassword(userId: string, password: string) {
const response = await ofetch<LogtoUser>(joinURL(env.LOGTO_URL, 'api', 'users', userId, 'password', 'verify'), {
method: 'POST',
headers: {
Authorization: `Bearer ${await this.getToken()}`,
},
body: {
password,
},
}).catch((err) => {
throw new LogtoError(err.data)
})
return response
}

public async updatePassword(userId: string, password: string) {
const response = await ofetch<LogtoUser>(joinURL(env.LOGTO_URL, 'api', 'users', userId, 'password'), {
method: 'PATCH',
headers: {
Authorization: `Bearer ${await this.getToken()}`,
},
body: {
password: password,
},
}).catch((err) => {
throw new LogtoError(err.data)
})
return response
}
}
8 changes: 5 additions & 3 deletions apps/api/src/trpc/middlewares/logto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,17 @@ import { TRPCError } from '@trpc/server'
import { middleware, publicProcedure } from '../trpc'
import { verifyLogtoJwt } from '../../logto/jwt'

const INVALID_TOKEN_MESSAGE = 'error.invalid_token'

const isLogtoAuthed = middleware(async ({ ctx, next }) => {
const token = ctx.req.headers.authorization?.split(' ')[1]

if (!token) throw new TRPCError({ code: 'UNAUTHORIZED' })
if (!token) throw new TRPCError({ code: 'UNAUTHORIZED', message: INVALID_TOKEN_MESSAGE })

const payload = await verifyLogtoJwt(token).catch(() => {
throw new TRPCError({ code: 'UNAUTHORIZED' })
throw new TRPCError({ code: 'UNAUTHORIZED', message: INVALID_TOKEN_MESSAGE })
})
if (!payload.sub) throw new TRPCError({ code: 'UNAUTHORIZED' })
if (!payload.sub) throw new TRPCError({ code: 'UNAUTHORIZED', message: INVALID_TOKEN_MESSAGE })

const user = await ctx.prisma.user.findUnique({
where: {
Expand Down
34 changes: 33 additions & 1 deletion apps/api/src/trpc/routes/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const userRouter = router({
}
}

throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', cause: err })
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'error.unknown_error', cause: err })
})

const user = await ctx.prisma.user.update({
Expand All @@ -81,4 +81,36 @@ export const userRouter = router({
user,
}
}),

updatePassword: logtoAuthedProcedure
.input(
z
.object({
currentPassword: z.string(),
newPassword: z
.string()
.min(9, { message: 'error.password_too_short' })
.max(8196, { message: 'error.password_too_long' }),
newPasswordConfirm: z.string(),
})
.refine((data) => data.newPassword === data.newPasswordConfirm, {
message: 'error.passwords_do_not_match',
path: ['confirmPassword'],
})
)
.mutation(async ({ ctx, input }) => {
try {
await ctx.mm.verifyPassword(ctx.user.id, input.currentPassword)
await ctx.mm.updatePassword(ctx.user.id, input.newPassword)
} catch (err) {
if (err instanceof LogtoError && err.code === 'session.invalid_credentials') {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'error.password_incorrect' })
}
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'error.unknown_error', cause: err })
}

return {
success: true,
}
}),
})
7 changes: 5 additions & 2 deletions apps/web/components/ui/Button.vue
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script setup lang="ts">
const props = withDefaults(
defineProps<{
variant?: 'default' | 'gradient'
variant?: 'default' | 'gradient' | 'outline'
size?: 'default' | 'sm' | 'lg'
disabled?: boolean
loading?: boolean
type?: 'button' | 'submit' | 'reset'
}>(),
{
variant: 'default',
Expand All @@ -16,6 +17,7 @@ const props = withDefaults(
const variants = new Map([
['default', 'bg-primary text-primary-foreground hover:bg-foreground/90'],
['outline', 'border hover:bg-accent'],
['gradient', 'bg-gradient-to-r from-indigo-500 to-blue-500 hover:from-indigo-500/90 hover:to-blue-500/90 '],
])
const sizes = new Map([
Expand All @@ -33,11 +35,12 @@ const classes = computed(() => {

<template>
<button
:type="props.type"
:disabled="props.disabled"
class="relative inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background"
:class="classes"
>
<div :class="{ 'opacity-0': props.loading }"><slot /></div>
<div class="w-full inline-flex items-center justify-center" :class="{ 'opacity-0': props.loading }"><slot /></div>
<div
v-if="props.loading"
class="absolute mx-auto my-auto i-svg-spinners-180-ring-with-bg text-xl"
Expand Down
13 changes: 12 additions & 1 deletion apps/web/components/ui/Card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,24 @@
const props = defineProps<{
title?: string
description?: string
back?: boolean
}>()
const emit = defineEmits<{
(e: 'back'): void
}>()
</script>

<template>
<div class="rounded-lg border bg-card text-card-foreground shadow-sm">
<div v-if="props.title || props.description" class="flex flex-col space-y-1.5 px-6 pt-6">
<h3 v-if="props.title" class="text-lg font-semibold leading-none tracking-tight">{{ props.title }}</h3>
<div class="flex items-center">
<UiIconButton v-if="props.back" type="button" class="mr-2" @click="emit('back')">
<div class="i-carbon-arrow-left"></div>
</UiIconButton>
<h3 v-if="props.title" class="text-lg font-semibold leading-none tracking-tight">{{ props.title }}</h3>
</div>

<p v-if="props.description" class="text-sm text-muted-foreground">{{ props.description }}</p>
</div>
<div v-if="$slots.default" class="p-6 grid gap-4">
Expand Down
14 changes: 14 additions & 0 deletions apps/web/components/ui/IconButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script setup lang="ts">
const props = defineProps<{
type?: 'button' | 'submit' | 'reset'
}>()
</script>

<template>
<button
:type="props.type"
class="h-8 w-8 transition-colors hover:bg-accent text-xl rounded-full flex items-center justify-center"
>
<slot />
</button>
</template>
7 changes: 7 additions & 0 deletions apps/web/components/ui/Input.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
<script setup lang="ts">
const props = defineProps<{
type?: 'text' | 'password'
autocomplete?: string
}>()
const { modelValue } = defineModels<{
modelValue?: string
}>()
Expand All @@ -7,6 +12,8 @@ const { modelValue } = defineModels<{
<template>
<input
v-model="modelValue"
:autocomplete="props.autocomplete"
:type="props.type"
class="flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
/>
</template>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/components/ui/NavBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const props = defineProps<{
<template>
<div
:class="{ 'border-t': props.bottom, 'border-b': !props.bottom }"
class="flex justify-between flex-shrink-0 h-16 px-10 items-center bg-background/30 backdrop-blur"
class="flex justify-between flex-shrink-0 h-16 px-6 md:px-10 items-center bg-background/30 backdrop-blur"
>
<slot />
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/layouts/auth.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const menuItems = computed(() => {
})
const isActive = (to: string) => {
return router.currentRoute.value.path === to
return router.currentRoute.value.path.startsWith(to)
}
const leaveLobby = async () => {
Expand Down
6 changes: 5 additions & 1 deletion apps/web/lib/utils/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { TRPCClientError } from '@trpc/client'

export const isUnauthorizedError = (error: unknown) => {
return error instanceof TRPCClientError && error.data.code === 'UNAUTHORIZED'
return (
error instanceof TRPCClientError &&
error.data.code === 'UNAUTHORIZED' &&
error.data.message === 'error.invalid_token'
)
}
20 changes: 18 additions & 2 deletions apps/web/locales/de-DE.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,19 @@
"edit_username_label": "Benutzername",
"edit_username_placeholder": "Gib deinen Benutzernamen ein",
"edit_profile_submit": "Speichern",
"edit_profile_success": "Profil erfolgreich aktualisiert"
"edit_profile_success": "Profil erfolgreich aktualisiert",
"edit_password_title": "Passwort ändern",
"edit_password_message": "Gib dein aktuelles und dein neues Passwort ein",
"edit_password_button": "Passwort ändern",
"edit_password_label": "Passwort",
"edit_password_current_label": "Aktuelles Passwort",
"edit_password_current_placeholder": "Gib dein aktuelles Passwort ein",
"edit_password_new_label": "Neues Passwort",
"edit_password_new_placeholder": "Gib dein neues Passwort ein",
"edit_password_confirm_label": "Passwort bestätigen",
"edit_password_confirm_placeholder": "Bestätige dein neues Passwort",
"edit_password_submit": "Passwort ändern",
"edit_password_success": "Passwort erfolgreich geändert"
},
"error": {
"username_too_long": "Benutzername ist zu lang",
Expand All @@ -27,7 +39,11 @@
"invalid_input": "Ungültige Eingabe",
"unknown_error": "Unbekannter Fehler",
"lobby_not_found": "Lobby nicht gefunden",
"page_not_found": "Seite nicht gefunden"
"page_not_found": "Seite nicht gefunden",
"password_too_short": "Passwort ist zu kurz",
"password_too_long": "Passwort ist zu lang",
"passwords_do_not_match": "Passwörter stimmen nicht überein",
"password_incorrect": "Falsches Passwort"
},
"notification": {
"error_title": "Fehler",
Expand Down
20 changes: 18 additions & 2 deletions apps/web/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,19 @@
"edit_username_label": "Username",
"edit_username_placeholder": "Enter your username",
"edit_profile_submit": "Save",
"edit_profile_success": "Profile updated successfully"
"edit_profile_success": "Profile updated successfully",
"edit_password_title": "Change Password",
"edit_password_message": "Enter your current and your new password",
"edit_password_button": "Change Password",
"edit_password_label": "Password",
"edit_password_current_label": "Current Password",
"edit_password_current_placeholder": "Enter your current password",
"edit_password_new_label": "New Password",
"edit_password_new_placeholder": "Enter your new password",
"edit_password_confirm_label": "Confirm Password",
"edit_password_confirm_placeholder": "Confirm your new password",
"edit_password_submit": "Change Password",
"edit_password_success": "Password changed successfully"
},
"error": {
"username_too_long": "Username is too long",
Expand All @@ -27,7 +39,11 @@
"invalid_input": "Invalid input",
"unknown_error": "Unknown error",
"lobby_not_found": "Lobby not found",
"page_not_found": "Page not found"
"page_not_found": "Page not found",
"password_too_short": "Password is too short",
"password_too_long": "Password is too long",
"passwords_do_not_match": "Passwords do not match",
"password_incorrect": "Password is incorrect"
},
"notification": {
"error_title": "Error",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { t } = useI18n()
</script>

<template>
<div class="flex flex-col items-center justify-center gap-4">
<div class="flex flex-col items-center justify-center gap-4 p-8">
<h1 class="text-5xl font-bold">Tune Perfect</h1>
<p class="text-lg">{{ t('index.tagline') }}</p>
<div class="mt-10">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,33 +89,42 @@ const { isLoading, mutate } = useMutation({

<template>
<div class="w-full h-full flex items-center justify-center p-8">
<div class="w-full h-full flex items-center justify-center p-8">
<input ref="avatarInputEl" type="file" class="hidden" accept="image/*" @input="onAvatarSelected" />
<form class="w-full h-full grid place-items-center" @submit="mutate">
<UiCard class="w-full max-w-100" :title="t('user.edit_profile_title')">
<div class="flex justify-center">
<div class="relative">
<button type="button" class="rounded-full" @click="onSelectAvatar">
<UiAvatar
class="w-30 h-30 hover:opacity-70 text-3xl"
:src="avatarUrl"
:fallback="claims?.username?.at(0)"
></UiAvatar>
</button>
<div class="absolute bottom-0 right-0 bg-primary rounded-full p-1.5">
<div class="i-carbon-edit text-xl text-primary-foreground"></div>
</div>
<input ref="avatarInputEl" type="file" class="hidden" accept="image/*" @input="onAvatarSelected" />
<form class="w-full h-full grid place-items-center" @submit="mutate">
<UiCard class="w-full max-w-100" :title="t('user.edit_profile_title')">
<div class="flex justify-center">
<div class="relative">
<button type="button" class="rounded-full" @click="onSelectAvatar">
<UiAvatar
class="w-30 h-30 hover:opacity-70 text-3xl"
:src="avatarUrl"
:fallback="claims?.username?.at(0)"
></UiAvatar>
</button>
<div class="absolute bottom-0 right-0 bg-primary rounded-full p-1.5">
<div class="i-carbon-edit text-xl text-primary-foreground"></div>
</div>
</div>
<div class="flex flex-col gap-2">
<UiLabel for="username">{{ t('user.edit_username_label') }}</UiLabel>
<UiInput id="username" v-model="username" :placeholder="t('user.edit_username_placeholder')" />
</div>
<template #footer>
<UiButton class="w-full" :loading="isLoading">{{ t('user.edit_profile_submit') }}</UiButton>
</template>
</UiCard>
</form>
</div>
</div>
<div class="flex flex-col gap-2">
<UiLabel for="username">{{ t('user.edit_username_label') }}</UiLabel>
<UiInput id="username" v-model="username" :placeholder="t('user.edit_username_placeholder')" />
</div>
<div class="flex flex-col gap-2">
<UiLabel for="password">{{ t('user.edit_password_label') }}</UiLabel>
<NuxtLink id="password" to="/user/edit-profile/password" class="w-full">
<UiButton variant="outline" type="button" class="w-full">
<div class="inline-flex items-center justify-between w-full">
{{ t('user.edit_password_button') }}
<div class="i-carbon-arrow-right text-2xl"></div>
</div>
</UiButton>
</NuxtLink>
</div>
<template #footer>
<UiButton class="w-full" :loading="isLoading">{{ t('user.edit_profile_submit') }}</UiButton>
</template>
</UiCard>
</form>
</div>
</template>
Loading

0 comments on commit ac46240

Please sign in to comment.