Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement an option in the dashboard to connect a Bluesky account #1001

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/@types/AuthenticatedUser.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { DiscordConnection } from './mongo/User/DiscordConnection'
import type { GitHubConnection } from './mongo/User/GitHubConnection'
import type { BlueskyConnection } from './mongo/User/BlueskyConnection'

Check failure on line 3 in src/@types/AuthenticatedUser.ts

View workflow job for this annotation

GitHub Actions / type-safety

Cannot find module './mongo/User/BlueskyConnection' or its corresponding type declarations.

export interface AuthenticatedUser {
/** ID in the database */
Expand All @@ -15,5 +16,6 @@
connections: {
github: GitHubConnection
discord: DiscordConnection
bluesky: BlueskyConnection
}
}
30 changes: 30 additions & 0 deletions src/components/connections/bluesky.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { FunctionComponent } from 'react'

import type { BlueskyConnection } from '$types/mongo/User/BlueskyConnection'

Check failure on line 3 in src/components/connections/bluesky.tsx

View workflow job for this annotation

GitHub Actions / type-safety

Cannot find module '$types/mongo/User/BlueskyConnection' or its corresponding type declarations.

export interface Props {
connection: BlueskyConnection
}

export const Bluesky: FunctionComponent<Props> = props => {
const { connection } = props

return (
<div className="flex items-center justify-between">
<div className="leading-none">
<p className="inline-flex items-center text-lg font-medium">Bluesky</p>
<p className="-mt-1 text-sm">
{connection
? 'Connected with @' + connection.username
: 'Not connected'}
</p>
</div>
<a
href="/auth/connect/bluesky"
className="rounded-lg bg-neutral-700 px-4 py-2 uppercase text-white"
>
{connection ? 'Reconnect' : 'Connect'}
</a>
</div>
)
}
2 changes: 2 additions & 0 deletions src/components/connections/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Discord } from './discord'
import { GitHub } from './github'
import { Bluesky } from './bluesky'

import type { FunctionComponent } from 'react'
import type { AuthenticatedUser } from '$types/AuthenticatedUser'
Expand All @@ -20,6 +21,7 @@ export const Connections: FunctionComponent<Props> = props => {
<div className="space-y-4">
<GitHub connection={connections.github} />
<Discord connection={connections.discord} />
<Bluesky connection={connections.bluesky} />
</div>
</div>
</section>
Expand Down
96 changes: 96 additions & 0 deletions src/packlets/backend/auth/authenticateBluesky.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { ObjectId } from 'mongodb'
import { AtpApi, AtpSessionData } from '@atproto/api'

Check failure on line 2 in src/packlets/backend/auth/authenticateBluesky.ts

View workflow job for this annotation

GitHub Actions / type-safety

Cannot find module '@atproto/api' or its corresponding type declarations.

import { collections } from '$constants/mongo'
import { blueskyClient } from '$constants/secrets/blueskyClient'

Check failure on line 5 in src/packlets/backend/auth/authenticateBluesky.ts

View workflow job for this annotation

GitHub Actions / type-safety

Cannot find module '$constants/secrets/blueskyClient' or its corresponding type declarations.

import { getAuthenticatedUser } from './getAuthenticatedUser'
import { finalizeAuthentication } from './finalizeAuthentication'

import type { User } from '$types/mongo/User'

interface BlueskyTokenResponse {
access_token: string
refresh_token: string
scope: string
token_type: string
}

interface BlueskyUserResponse {
did: string
handle: string
}

export const authenticateBluesky = async (
code: string,
existingAuthToken?: string
) => {
const currentAuthenticatedUser = await getAuthenticatedUser(existingAuthToken)

if (currentAuthenticatedUser === null) throw new Error('not-authenticated')

try {
// obtain access token
console.log('[bluesky] token')
const authorization = await fetch(
'https://bsky.social/xrpc/com.atproto.server.createSession',
{
method: 'POST',
body: JSON.stringify({
client_id: blueskyClient.id,
client_secret: blueskyClient.secret,
code: code,
redirect_uri: 'https://creatorsgarten.org/auth/callback',
grant_type: 'authorization_code',
}),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}
).then(o => {
if (o.ok) return o.json() as Promise<BlueskyTokenResponse>
else throw o
})

// get user information
console.log('[bluesky] user')
const api = new AtpApi({ service: 'https://bsky.social' })
api.setSession(authorization as AtpSessionData)
const user = await api.com.atproto.identity.resolveHandle({
handle: authorization.did,

Check failure on line 61 in src/packlets/backend/auth/authenticateBluesky.ts

View workflow job for this annotation

GitHub Actions / type-safety

Property 'did' does not exist on type 'BlueskyTokenResponse'.
}).then(o => {

Check failure on line 62 in src/packlets/backend/auth/authenticateBluesky.ts

View workflow job for this annotation

GitHub Actions / type-safety

Parameter 'o' implicitly has an 'any' type.
if (o.success) return o.data as BlueskyUserResponse
else throw o
})

// make sure that this account does not connected to another account
const existingUser = await collections.users.findOne({
_id: { $ne: new ObjectId(currentAuthenticatedUser.sub) },
'connections.bluesky.did': user.did,
})
if (existingUser !== null)
throw new Error('This connection has been connected to another account.')

// sync with mongo
await collections.users.updateOne(
{ uid: currentAuthenticatedUser.uid },
{
$set: {
connections: {
...currentAuthenticatedUser.connections,
bluesky: {

Check failure on line 82 in src/packlets/backend/auth/authenticateBluesky.ts

View workflow job for this annotation

GitHub Actions / type-safety

Type '{ connections: { bluesky: { did: any; handle: any; }; github: GitHubConnection; discord: DiscordConnection; }; }' is not assignable to type 'MatchKeysAndValues<User>'.

Check failure on line 82 in src/packlets/backend/auth/authenticateBluesky.ts

View workflow job for this annotation

GitHub Actions / type-safety

Object literal may only specify known properties, and 'bluesky' does not exist in type '{ github?: GitHubConnection | undefined; discord?: DiscordConnection | undefined; }'.
did: user.did,
handle: user.handle,
},
},
} satisfies Partial<User>,
}
)

return finalizeAuthentication(currentAuthenticatedUser.uid)
} catch (e) {
if (e instanceof Error) throw e
else throw new Error('Unable to verify authenticity of this connection.')
}
}
25 changes: 25 additions & 0 deletions src/pages/auth/connect/bluesky.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import CSRF from 'csrf'

import { csrfSecret } from '$constants/secrets/csrfSecret'
import { blueskyClient } from '$constants/secrets/blueskyClient'

import type { APIRoute } from 'astro'

export const GET: APIRoute = async ({ request, redirect }) => {
const csrfInstance = new CSRF()
const redirectHint =
new URL(request.url).hostname === 'localhost' ? 'localhost3000' : 'new'
const csrfToken = csrfInstance.create(csrfSecret ?? 'demodash')

const loginURI = `https://bsky.social/xrpc/com.atproto.server.createSession?${new URLSearchParams(
{
client_id: blueskyClient.id ?? '',
redirect_uri: 'https://creatorsgarten.org/auth/callback',
state: `${redirectHint}!/dashboard!bluesky-${csrfToken}`,
response_type: 'code',
scope: 'openid',
}
).toString()}`

return redirect(loginURI, 307)
}
Loading