Skip to content

Commit 5b203cb

Browse files
committed
feat(auth): implement atproto password reset flow
1 parent 1674841 commit 5b203cb

4 files changed

Lines changed: 173 additions & 10 deletions

File tree

app/pages/account/settings.vue

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,73 @@
11
<script setup lang="ts">
22
const { user } = useAtproto()
3+
4+
const isResetting = ref(false)
5+
const resetEmail = ref('')
6+
const isCodeSent = ref(false)
7+
const resetToken = ref('')
8+
const newPassword = ref('')
9+
const isConfirming = ref(false)
10+
11+
const errorMessage = ref('')
12+
const successMessage = ref('')
13+
14+
async function handlePasswordReset() {
15+
errorMessage.value = ''
16+
successMessage.value = ''
17+
18+
if (!resetEmail.value) {
19+
errorMessage.value = 'Please enter your email first.'
20+
return
21+
}
22+
23+
isResetting.value = true
24+
try {
25+
await $fetch('/api/atproto/password-reset', {
26+
method: 'POST',
27+
body: { email: resetEmail.value },
28+
})
29+
30+
isCodeSent.value = true
31+
successMessage.value = 'Code sent! Check your email.'
32+
} catch (e: any) {
33+
errorMessage.value = e.statusMessage || 'Something went wrong. Please try again.'
34+
} finally {
35+
isResetting.value = false
36+
}
37+
}
38+
39+
async function handleConfirmReset() {
40+
errorMessage.value = ''
41+
successMessage.value = ''
42+
43+
if (!resetToken.value || !newPassword.value) {
44+
errorMessage.value = 'Please enter both the code and your new password.'
45+
return
46+
}
47+
48+
isConfirming.value = true
49+
try {
50+
await $fetch('/api/atproto/password-reset-confirm', {
51+
method: 'POST',
52+
body: {
53+
token: resetToken.value,
54+
password: newPassword.value,
55+
},
56+
})
57+
58+
successMessage.value = 'Password updated successfully!'
59+
60+
// Reset the form
61+
isCodeSent.value = false
62+
resetToken.value = ''
63+
newPassword.value = ''
64+
resetEmail.value = ''
65+
} catch (e: any) {
66+
errorMessage.value = e.statusMessage || 'Failed to update password. Check your code.'
67+
} finally {
68+
isConfirming.value = false
69+
}
70+
}
371
</script>
472

573
<template>
@@ -42,23 +110,63 @@ const { user } = useAtproto()
42110
>
43111
<div>
44112
<h2 class="font-mono text-xl mb-1">Email Address</h2>
45-
<p class="text-sm text-fg-muted">Update the email address used for account recovery.</p>
113+
<p class="text-sm text-fg-muted">Update the email address of your account.</p>
46114
</div>
47115
<ButtonBase variant="secondary">Request Email Change</ButtonBase>
48116
</section>
49117

50118
<section
51-
class="p-6 bg-bg-subtle border border-border rounded-lg flex flex-col sm:flex-row sm:items-center justify-between gap-4"
119+
class="flex flex-col gap-2 max-w-sm p-4 border border-border rounded-lg bg-bg-subtle"
52120
>
53-
<div>
54-
<h2 class="font-mono text-xl mb-1">Password Reset</h2>
55-
<p class="text-sm text-fg-muted">
56-
Send a secure password reset link to your registered email.
121+
<h3 class="font-mono text-lg text-fg">Reset Password</h3>
122+
123+
<p v-if="errorMessage" class="text-sm text-red-500 mb-2">{{ errorMessage }}</p>
124+
<p v-if="successMessage" class="text-sm text-green-500 mb-2">{{ successMessage }}</p>
125+
126+
<div v-if="!isCodeSent">
127+
<p class="text-sm text-fg-muted mb-2">
128+
Enter your atmosphere email to receive a reset link.
57129
</p>
130+
<input
131+
v-model="resetEmail"
132+
type="email"
133+
placeholder="you@example.com"
134+
class="w-full bg-bg border border-border rounded-md px-3 py-2 text-sm text-fg focus:border-accent outline-none mb-2"
135+
/>
136+
<ButtonBase
137+
variant="secondary"
138+
class="text-red-500 mt-2"
139+
:disabled="isResetting || !resetEmail"
140+
@click="handlePasswordReset"
141+
>
142+
{{ isResetting ? 'Sending...' : 'Send Reset Code' }}
143+
</ButtonBase>
144+
</div>
145+
146+
<div v-else class="flex flex-col gap-2">
147+
<input
148+
v-model="resetToken"
149+
type="text"
150+
placeholder="Reset Code (e.g. ABC-DEF)"
151+
class="w-full bg-bg border border-border rounded-md px-3 py-2 text-sm text-fg focus:border-accent outline-none"
152+
/>
153+
154+
<input
155+
v-model="newPassword"
156+
type="password"
157+
placeholder="New Password"
158+
class="w-full bg-bg border border-border rounded-md px-3 py-2 text-sm text-fg focus:border-accent outline-none"
159+
/>
160+
161+
<ButtonBase
162+
variant="secondary"
163+
class="text-red-500 mt-2"
164+
:disabled="isConfirming || !resetToken || !newPassword"
165+
@click="handleConfirmReset"
166+
>
167+
{{ isConfirming ? 'Saving...' : 'Save New Password' }}
168+
</ButtonBase>
58169
</div>
59-
<ButtonBase variant="secondary" class="text-red-500 hover:bg-red-500/10 border-red-500/20">
60-
Reset Password
61-
</ButtonBase>
62170
</section>
63171
</div>
64172
</main>

app/pages/profile/[identity]/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ defineOgImageComponent('Default', {
161161
{{ profile.displayName }}
162162
</h1>
163163
<LinkBase
164-
v-if="user?.handle === handle"
164+
v-if="user?.handle === profile?.handle"
165165
to="/account/settings"
166166
variant="button-secondary"
167167
classicon="i-lucide:settings"
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Agent } from '@atproto/api'
2+
3+
export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
4+
if (!oAuthSession) {
5+
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
6+
}
7+
8+
const body = await readBody(event)
9+
if (!body || !body.token || !body.password) {
10+
throw createError({ statusCode: 400, statusMessage: 'Token and new password are required' })
11+
}
12+
13+
const agent = new Agent(oAuthSession)
14+
15+
try {
16+
await agent.com.atproto.server.resetPassword({
17+
token: body.token.trim(),
18+
password: body.password,
19+
})
20+
21+
return { success: true }
22+
} catch (err: any) {
23+
throw createError({
24+
statusCode: 500,
25+
statusMessage: err.message || 'Failed to update password. Check your code.',
26+
})
27+
}
28+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Agent } from '@atproto/api'
2+
3+
export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
4+
if (!oAuthSession) {
5+
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
6+
}
7+
8+
const body = await readBody(event)
9+
if (!body || !body.email) {
10+
throw createError({ statusCode: 400, statusMessage: 'Email is required' })
11+
}
12+
13+
const agent = new Agent(oAuthSession)
14+
15+
try {
16+
await agent.com.atproto.server.requestPasswordReset({
17+
email: body.email,
18+
})
19+
20+
return { success: true }
21+
} catch (err: any) {
22+
throw createError({
23+
statusCode: 500,
24+
statusMessage: err.message || 'Failed to request password reset.',
25+
})
26+
}
27+
})

0 commit comments

Comments
 (0)