Last active
January 26, 2023 06:39
-
-
Save hgoona/ad5d6fcfa446a7a56b29a977861747eb to your computer and use it in GitHub Desktop.
AuthJS (beta) + Sveltekit 1.0 with Email + Surreal Database adapter (in PR)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// .ENV file | |
#// NEVER EXPOSE - SURREAL DB ADMIN KEY | |
VITE_SURREAL_DB_ADMIN_USER = "" | |
VITE_SURREAL_DB_ADMIN_PASS = "" | |
# AUTHJS PROVIDERS | |
AUTH_SECRET = "" # AuthJS - What goes in here? Random number?? https://generate-secret.vercel.app/32 | |
VITE_GITHUB_ID = "" | |
VITE_GITHUB_SECRET = "" | |
VITE_DISCORD_CLIENT_ID = "" | |
VITE_DISCORD_CLIENT_SECRET = "" | |
# email configuration | |
VITE_EMAIL_SERVER = "" | |
VITE_EMAIL_FROM = "" | |
# VITE_EMAIL_SERVER= "smtp://username:[email protected]:587" | |
# VITE_EMAIL_FROM= "[email protected]" | |
VITE_EMAIL_SERVER_HOST = smtp.gmail.com | |
VITE_EMAIL_SERVER_PORT = 587 | |
# VITE_EMAIL_SERVER_USER = <my google email>@gmail.com | |
# VITE_EMAIL_SERVER_PASSWORD = <my google smtp password> | |
VITE_EMAIL_SERVER_USER = "" | |
VITE_EMAIL_SERVER_PASSWORD = "" | |
# fly.io setup | |
VITE_SURREAL_HOST ="<fly app name>.fly.dev" | |
# VITE_SURREAL_PORT = "80" #❌ | |
VITE_SURREAL_PORT = "443" #✅ | |
VITE_SURREAL_USER = "root" | |
VITE_SURREAL_PASS = "" | |
VITE_SURREAL_NS = "test" | |
VITE_SURREAL_DB = "test" | |
# VITE_SURREAL_PROTOCOL = "TCP" # ❌ | |
VITE_SURREAL_PROTOCOL = "HTTPS" # ✅ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/ | |
import { redirect, type Handle } from "@sveltejs/kit"; | |
import { sequence } from "@sveltejs/kit/hooks"; | |
import { SvelteKitAuth } from "@auth/sveltekit" | |
import GitHub from "@auth/core/providers/github" | |
import Discord from "@auth/core/providers/discord" | |
import { Email } from "@auth/core/providers/email"; | |
import { SurrealDBAdapter } from "$lib/adapters/surrealdb" | |
import { clientPromise } from "$lib/surrealdbConfig"; | |
import { | |
VITE_EMAIL_SERVER, | |
VITE_EMAIL_FROM, | |
VITE_GITHUB_ID, | |
VITE_GITHUB_SECRET, | |
VITE_DISCORD_CLIENT_ID, | |
VITE_DISCORD_CLIENT_SECRET | |
} from "$env/static/private" | |
// async function authorization({ event, resolve }) { | |
const authorization: Handle = async ({ event, resolve }) => { | |
// Protect any routes under /protected | |
// ROUTES | |
// PROTECTED | |
if (event.url.pathname.startsWith('/protected')) { | |
const session = await event.locals.getSession(); | |
if (!session) { | |
throw redirect(303, '/auth'); | |
} | |
} | |
// ADMIN | |
// if (event.url.pathname.startsWith('/protected/admin')) { | |
// const session = await event.locals.getSession(); | |
// if (!session?.user.) { // TODO check Role: Admin | |
// throw redirect(303, '/auth'); | |
// } | |
// } | |
// If the request is still here, just proceed as normally | |
const result = await resolve(event, { | |
transformPageChunk: ({ html }) => html | |
}); | |
return result; | |
} | |
// First handle authentication, then authorization | |
// Each function acts as a middleware, receiving the request handle | |
// And returning a handle which gets passed to the next function | |
export const handle: Handle = sequence( | |
SvelteKitAuth({ | |
adapter: SurrealDBAdapter(clientPromise), | |
providers: [ | |
// @ts-expect-error issue https://github.com/nextauthjs/next-auth/issues/6174 | |
GitHub({ | |
clientId: VITE_GITHUB_ID, | |
clientSecret: VITE_GITHUB_SECRET | |
}), | |
// @ts-expect-error issue https://github.com/nextauthjs/next-auth/issues/6174 | |
Discord({ | |
clientId: VITE_DISCORD_CLIENT_ID, | |
clientSecret: VITE_DISCORD_CLIENT_SECRET | |
}), | |
// @ts-expect-error issue https://github.com/nextauthjs/next-auth/issues/6174 | |
Email({ | |
server: VITE_EMAIL_SERVER, | |
from: VITE_EMAIL_FROM | |
}), | |
], | |
// pages: { | |
// signIn: '/auth/signin', | |
// signOut: '/auth/signout', | |
// error: '/auth/error', // Error code passed in query string as ?error= | |
// verifyRequest: '/auth/verify-request', // (used for check email message) | |
// newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest) | |
// }, | |
}), | |
authorization | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/lib/adapters/ | |
import type { | |
SurrealREST as Surreal, | |
SurrealRESTResponse as Result, | |
} from "surrealdb-rest-ts" | |
import type { | |
Adapter, | |
AdapterUser, | |
AdapterAccount, | |
AdapterSession, | |
VerificationToken, | |
// } from "next-auth/adapters" //❌ original | |
} from "@auth/core/adapters" | |
// import type { ProviderType } from "next-auth/providers" //❌ original | |
import type { ProviderType } from "@auth/core/providers" | |
type Document = Record<string, string | null | undefined> & { id: string } | |
export type UserDoc = Document & { email: string } | |
export type AccountDoc<T = string> = { | |
id: string | |
userId: T | |
refresh_token?: string | |
access_token?: string | |
type: ProviderType | |
provider: string | |
providerAccountId: string | |
expires_at?: number | |
} | |
export type SessionDoc<T = string> = Document & { userId: T } | |
const extractId = (surrealId: string) => surrealId.split(":")[1] ?? surrealId | |
// Convert DB object to AdapterUser | |
export const docToUser = (doc: UserDoc): AdapterUser => ({ | |
...doc, | |
id: extractId(doc.id), | |
emailVerified: doc.emailVerified ? new Date(doc.emailVerified) : null, | |
}) | |
// Convert DB object to AdapterAccount | |
export const docToAccount = (doc: AccountDoc): AdapterAccount => { | |
const account = { | |
...doc, | |
id: extractId(doc.id), | |
userId: doc.userId ? extractId(doc.userId) : "", | |
} | |
return account | |
} | |
// Convert DB object to AdapterSession | |
export const docToSession = ( | |
doc: SessionDoc<string | UserDoc> | |
): AdapterSession => ({ | |
userId: extractId( | |
typeof doc.userId === "string" ? doc.userId : doc.userId.id | |
), | |
expires: new Date(doc.expires ?? ""), | |
sessionToken: doc.sessionToken ?? "", | |
}) | |
// Convert AdapterUser to DB object | |
const userToDoc = ( | |
user: Omit<AdapterUser, "id"> | Partial<AdapterUser> | |
): Omit<UserDoc, "id"> => { | |
const doc = { | |
...user, | |
emailVerified: user.emailVerified?.toISOString(), | |
} | |
return doc | |
} | |
// Convert AdapterAccount to DB object | |
const accountToDoc = (account: AdapterAccount): Omit<AccountDoc, "id"> => { | |
const doc = { | |
...account, | |
userId: `user:${account.userId}`, | |
} | |
return doc | |
} | |
// Convert AdapterSession to DB object | |
export const sessionToDoc = ( | |
session: AdapterSession | |
): Omit<SessionDoc, "id"> => { | |
const doc = { | |
...session, | |
expires: session.expires.toISOString(), | |
} | |
return doc | |
} | |
export function SurrealDBAdapter( | |
client: Promise<Surreal> | |
// options = {} | |
): Adapter { | |
return { | |
async createUser(data) { | |
const surreal = await client | |
const doc = userToDoc(data) | |
const user = (await surreal.create("user", doc)) as UserDoc | |
return docToUser(user) | |
}, | |
async getUser(id: string) { | |
const surreal = await client | |
try { | |
const users = (await surreal.select(`user:${id}`)) as UserDoc[] | |
return docToUser(users[0]) | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
return null | |
}, | |
async getUserByEmail(email: string) { | |
const surreal = await client | |
try { | |
const users = await surreal.query<Result<UserDoc[]>[]>( | |
`SELECT * FROM user WHERE email = $email`, | |
{ email } | |
) | |
const user = users[0].result?.[0] | |
if (user) return docToUser(user) | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
return null | |
}, | |
async getUserByAccount({ providerAccountId, provider }) { | |
const surreal = await client | |
try { | |
const users = await surreal.query<Result<AccountDoc<UserDoc>[]>[]>( | |
`SELECT userId | |
FROM account | |
WHERE providerAccountId = $providerAccountId | |
AND provider = $provider | |
FETCH userId`, | |
{ providerAccountId, provider } | |
) | |
const user = users[0].result?.[0]?.userId | |
if (user) return docToUser(user) | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
return null | |
}, | |
async updateUser(user) { | |
const surreal = await client | |
const doc = userToDoc(user) | |
let updatedUser = await surreal.change<Omit<UserDoc, "id">>( | |
`user:${user.id}`, | |
doc | |
) | |
if (Array.isArray(updatedUser)) { | |
updatedUser = updatedUser[0] | |
} | |
return docToUser(updatedUser as UserDoc) | |
}, | |
async deleteUser(userId) { | |
const surreal = await client | |
// delete account | |
try { | |
const accounts = await surreal.query<Result<AccountDoc[]>[]>( | |
`SELECT * | |
FROM account | |
WHERE userId = $userId | |
LIMIT 1`, | |
{ userId: `user:${userId}` } | |
) | |
const account = accounts[0].result?.[0] | |
if (account) { | |
const accountId = extractId(account.id) | |
await surreal.delete(`account:${accountId}`) | |
} | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
// delete session | |
try { | |
const sessions = await surreal.query<Result<SessionDoc[]>[]>( | |
`SELECT * | |
FROM session | |
WHERE userId = $userId | |
LIMIT 1`, | |
{ userId: `user:${userId}` } | |
) | |
const session = sessions[0].result?.[0] | |
if (session) { | |
const sessionId = extractId(session.id) | |
await surreal.delete(`session:${sessionId}`) | |
} | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
// delete user | |
await surreal.delete(`user:${userId}`) | |
// TODO: put all 3 deletes inside a Promise all | |
}, | |
async linkAccount(account) { | |
const surreal = await client | |
const doc = (await surreal.create( | |
"account", | |
accountToDoc(account) | |
)) as AccountDoc | |
return docToAccount(doc) | |
}, | |
async unlinkAccount({ providerAccountId, provider }) { | |
const surreal = await client | |
try { | |
const accounts = await surreal.query<Result<AccountDoc[]>[]>( | |
`SELECT * | |
FROM account | |
WHERE providerAccountId = $providerAccountId | |
AND provider = $provider | |
LIMIT 1`, | |
{ providerAccountId, provider } | |
) | |
const account = accounts[0].result?.[0] | |
if (account) { | |
const accountId = extractId(account.id) | |
await surreal.delete(`account:${accountId}`) | |
} | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
}, | |
async createSession({ sessionToken, userId, expires }) { | |
const surreal = await client | |
const doc = { | |
sessionToken, | |
userId: `user:${userId}`, | |
expires, | |
} | |
await surreal.create("session", doc) | |
return doc | |
}, | |
async getSessionAndUser(sessionToken) { | |
const surreal = await client | |
try { | |
// Can't use limit 1 because it prevent userId to be fetched. | |
// Works setting limit to 2 | |
const sessions = await surreal.query<Result<SessionDoc<UserDoc>[]>[]>( | |
`SELECT * | |
FROM session | |
WHERE sessionToken = $sessionToken | |
FETCH userId`, | |
{ sessionToken } | |
) | |
const session = sessions[0].result?.[0] | |
if (session) { | |
const userDoc = session.userId | |
if (!userDoc) return null | |
return { | |
user: docToUser(userDoc), | |
session: docToSession({ | |
...session, | |
userId: userDoc.id, | |
}), | |
} | |
} | |
return null | |
} catch (e) { | |
return null | |
} | |
}, | |
async updateSession(sessionData) { | |
const surreal = await client | |
try { | |
const sessions = await surreal.query<Result<SessionDoc[]>[]>( | |
`SELECT * | |
FROM session | |
WHERE sessionToken = $sessionToken | |
LIMIT 1`, | |
{ sessionToken: sessionData.sessionToken } | |
) | |
const session = sessions[0].result?.[0] | |
if (session && sessionData.expires) { | |
const sessionId = extractId(session.id) | |
let updatedSession = await surreal.change<Omit<SessionDoc, "id">>( | |
`session:${sessionId}`, | |
sessionToDoc({ | |
...session, | |
...sessionData, | |
userId: session.userId, | |
expires: sessionData.expires, | |
}) | |
) | |
if (Array.isArray(updatedSession)) { | |
updatedSession = updatedSession[0] | |
} | |
return docToSession(updatedSession as SessionDoc) | |
} | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
return null | |
}, | |
async deleteSession(sessionToken: string) { | |
const surreal = await client | |
try { | |
const sessions = await surreal.query<Result<SessionDoc[]>[]>( | |
`SELECT * | |
FROM session | |
WHERE sessionToken = $sessionToken | |
LIMIT 1`, | |
{ sessionToken } | |
) | |
const session = sessions[0].result?.[0] | |
if (session) { | |
const sessionId = extractId(session.id) | |
await surreal.delete(`session:${sessionId}`) | |
} | |
} catch (e) { | |
console.log(e)// TODO | |
} | |
}, | |
async createVerificationToken({ identifier, expires, token }) { | |
const surreal = await client | |
const doc = { | |
identifier, | |
expires, | |
token, | |
} | |
await surreal.create("verification_token", doc) | |
return doc | |
}, | |
async useVerificationToken({ identifier, token }) { | |
const surreal = await client | |
try { | |
const tokens = await surreal.query< | |
Result<(VerificationToken & { id: string })[]>[] | |
>( | |
`SELECT * | |
FROM verification_token | |
WHERE identifier = $identifier | |
AND token = $token | |
LIMIT 1`, | |
{ identifier, token } | |
) | |
const vt = tokens[0].result?.[0] | |
if (vt) { | |
await surreal.delete(vt.id) | |
return { | |
identifier: vt.identifier, | |
expires: new Date(vt.expires), | |
token: vt.token, | |
} | |
} | |
return null | |
} catch (e) { | |
return null | |
} | |
}, | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// src/lib/ | |
import { SurrealREST } from "surrealdb-rest-ts"; | |
import { | |
VITE_SURREAL_HOST, | |
VITE_SURREAL_PORT, | |
VITE_SURREAL_USER, | |
VITE_SURREAL_PASS, | |
VITE_SURREAL_NS, | |
VITE_SURREAL_DB, | |
VITE_SURREAL_PROTOCOL | |
} from "$env/static/private" | |
const host = VITE_SURREAL_HOST | |
const port = VITE_SURREAL_PORT | |
const user = VITE_SURREAL_USER | |
const pass = VITE_SURREAL_PASS | |
const ns = VITE_SURREAL_NS | |
const db = VITE_SURREAL_DB | |
const protocol = VITE_SURREAL_PROTOCOL | |
export const clientPromise = new Promise<SurrealREST>((resolve) => { | |
resolve( | |
new SurrealREST(`${protocol}://${host}:${port}`, { | |
ns, | |
db, | |
user, | |
password: pass, | |
}) | |
); | |
}); |
Halleluuuuuuuuuuu! 🎉
The basic setup is now working for:
Sveltekit 1.0 + AuthJS Beta + SurrealDB Beta using @martinschaer's surrealdb-rest-ts
See the corrected files above for:
- hooks server,
- surreal config file,
- the surrealdb adapter - a tweaked Auth/core compatible version of @martinschaer's PR at time of writing (nextauthjs/next-auth#6251)
- .env example
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Are these dependencies an issue?