diff --git a/app/pages/account/settings.vue b/app/pages/account/settings.vue
new file mode 100644
index 000000000..1644fbaa5
--- /dev/null
+++ b/app/pages/account/settings.vue
@@ -0,0 +1,398 @@
+
+
+
+
+
+
+
+
+ Change Handle
+
+ Your handle is your unique identifier on the AT Protocol.
+
+
+ {{ handleError }}
+ {{ handleSuccess }}
+
+
+
+
+
+
+
+ {{ isUpdatingHandle ? 'Updating...' : 'Save' }}
+
+
+
+
+ Yes, change it
+
+
+ Cancel
+
+
+
+
+
+
+
+
+ {{ isUpdatingHandle ? 'Updating...' : 'Save' }}
+
+
+
+
+ Yes, change it
+
+
+ Cancel
+
+
+
+
+
How to verify your domain:
+
+ - Go to your domain registrar (e.g., Namecheap, GoDaddy).
+ - Add a TXT record.
+ - Set the Host/Name to:
_atproto
+ -
+ Set the Value to:
did={{ user?.did }}
+
+
+
+
+
+
+
+
+
+
Email Address
+
Update the email address of your account.
+
+
+
+ {{ isRequestingEmail ? 'Requesting...' : 'Request Email Change' }}
+
+
+
+ {{ emailError }}
+ {{ emailSuccess }}
+
+
+
+
+
+
+
+
diff --git a/app/pages/profile/[identity]/index.vue b/app/pages/profile/[identity]/index.vue
index 45c9c0f57..1f61832e2 100644
--- a/app/pages/profile/[identity]/index.vue
+++ b/app/pages/profile/[identity]/index.vue
@@ -156,9 +156,20 @@ defineOgImageComponent('Default', {
-
- {{ profile.displayName }}
-
+
+
+ {{ profile.displayName }}
+
+
+ Account Settings
+
+
{{ profile.description }}
@{{ profile.handle ?? identity }}
diff --git a/server/api/atproto/email-update-confirm.post.ts b/server/api/atproto/email-update-confirm.post.ts
new file mode 100644
index 000000000..35f63811d
--- /dev/null
+++ b/server/api/atproto/email-update-confirm.post.ts
@@ -0,0 +1,21 @@
+import { Agent } from '@atproto/api'
+
+export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
+ if (!oAuthSession) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+ const body = await readBody(event)
+ if (!body || !body.email || !body.token) {
+ throw createError({ statusCode: 400, statusMessage: 'Email and token required' })
+ }
+ const agent = new Agent(oAuthSession)
+ try {
+ await agent.com.atproto.server.updateEmail({
+ email: body.email,
+ emailAuthFactor: body.token.trim(),
+ })
+ return { success: true }
+ } catch (err: any) {
+ throw createError({ statusCode: 500, statusMessage: err.message })
+ }
+})
diff --git a/server/api/atproto/email-update-request.post.ts b/server/api/atproto/email-update-request.post.ts
new file mode 100644
index 000000000..7688ca250
--- /dev/null
+++ b/server/api/atproto/email-update-request.post.ts
@@ -0,0 +1,14 @@
+import { Agent } from '@atproto/api'
+
+export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
+ if (!oAuthSession) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+ const agent = new Agent(oAuthSession)
+ try {
+ await agent.com.atproto.server.requestEmailUpdate()
+ return { success: true }
+ } catch (err: any) {
+ throw createError({ statusCode: 500, statusMessage: err.message })
+ }
+})
diff --git a/server/api/atproto/handle-update.post.ts b/server/api/atproto/handle-update.post.ts
new file mode 100644
index 000000000..000af7c7f
--- /dev/null
+++ b/server/api/atproto/handle-update.post.ts
@@ -0,0 +1,26 @@
+import { Agent } from '@atproto/api'
+
+export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
+ if (!oAuthSession) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+
+ const body = await readBody(event)
+ if (!body || !body.handle) {
+ throw createError({ statusCode: 400, statusMessage: 'Handle is required' })
+ }
+
+ const agent = new Agent(oAuthSession)
+
+ try {
+ await agent.com.atproto.identity.updateHandle({
+ handle: body.handle,
+ })
+ return { success: true }
+ } catch (err: any) {
+ throw createError({
+ statusCode: 500,
+ statusMessage: err.message || 'Failed to update handle.',
+ })
+ }
+})
diff --git a/server/api/atproto/password-reset-confirm.post.ts b/server/api/atproto/password-reset-confirm.post.ts
new file mode 100644
index 000000000..d83a5071d
--- /dev/null
+++ b/server/api/atproto/password-reset-confirm.post.ts
@@ -0,0 +1,28 @@
+import { Agent } from '@atproto/api'
+
+export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
+ if (!oAuthSession) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+
+ const body = await readBody(event)
+ if (!body || !body.token || !body.password) {
+ throw createError({ statusCode: 400, statusMessage: 'Token and new password are required' })
+ }
+
+ const agent = new Agent(oAuthSession)
+
+ try {
+ await agent.com.atproto.server.resetPassword({
+ token: body.token.trim(),
+ password: body.password,
+ })
+
+ return { success: true }
+ } catch (err: any) {
+ throw createError({
+ statusCode: 500,
+ statusMessage: err.message || 'Failed to update password. Check your code.',
+ })
+ }
+})
diff --git a/server/api/atproto/password-reset.post.ts b/server/api/atproto/password-reset.post.ts
new file mode 100644
index 000000000..632ee5b92
--- /dev/null
+++ b/server/api/atproto/password-reset.post.ts
@@ -0,0 +1,27 @@
+import { Agent } from '@atproto/api'
+
+export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
+ if (!oAuthSession) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+
+ const body = await readBody(event)
+ if (!body || !body.email) {
+ throw createError({ statusCode: 400, statusMessage: 'Email is required' })
+ }
+
+ const agent = new Agent(oAuthSession)
+
+ try {
+ await agent.com.atproto.server.requestPasswordReset({
+ email: body.email,
+ })
+
+ return { success: true }
+ } catch (err: any) {
+ throw createError({
+ statusCode: 500,
+ statusMessage: err.message || 'Failed to request password reset.',
+ })
+ }
+})
diff --git a/server/api/atproto/server-info.get.ts b/server/api/atproto/server-info.get.ts
new file mode 100644
index 000000000..99deefcb3
--- /dev/null
+++ b/server/api/atproto/server-info.get.ts
@@ -0,0 +1,19 @@
+import { Agent } from '@atproto/api'
+
+export default eventHandlerWithOAuthSession(async (event, oAuthSession) => {
+ if (!oAuthSession) {
+ throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
+ }
+
+ const agent = new Agent(oAuthSession)
+
+ try {
+ const response = await agent.com.atproto.server.describeServer()
+ return response.data.availableUserDomains
+ } catch (err: any) {
+ throw createError({
+ statusCode: 500,
+ statusMessage: err.message || 'Failed to fetch server info.',
+ })
+ }
+})
diff --git a/server/utils/atproto/oauth.ts b/server/utils/atproto/oauth.ts
index 58152fd2b..4c6476c62 100644
--- a/server/utils/atproto/oauth.ts
+++ b/server/utils/atproto/oauth.ts
@@ -15,7 +15,7 @@ import type { UserServerSession } from '#shared/types/userSession'
import { clientUri } from '#oauth/config'
// TODO: If you add writing a new record you will need to add a scope for it
-export const scope = `atproto ${LIKES_SCOPE} ${PROFILE_SCOPE}`
+export const scope = `atproto ${LIKES_SCOPE} ${PROFILE_SCOPE} identity:handle account:email?action=manage`
/**
* Resolves a did to a handle via DoH or via the http website calls