Skip to content

Instantly share code, notes, and snippets.

@hgoona
Last active January 26, 2023 06:39
Show Gist options
  • Save hgoona/ad5d6fcfa446a7a56b29a977861747eb to your computer and use it in GitHub Desktop.
Save hgoona/ad5d6fcfa446a7a56b29a977861747eb to your computer and use it in GitHub Desktop.
AuthJS (beta) + Sveltekit 1.0 with Email + Surreal Database adapter (in PR)
// .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" # ✅
// 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
)
// 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
}
},
}
}
// 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,
})
);
});
@hgoona
Copy link
Author

hgoona commented Jan 26, 2023

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:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment