Skip to content

Instantly share code, notes, and snippets.

@taavetmaask
Last active September 13, 2023 10:13
Show Gist options
  • Save taavetmaask/e333921f918d325348e9c9f13ceb6a30 to your computer and use it in GitHub Desktop.
Save taavetmaask/e333921f918d325348e9c9f13ceb6a30 to your computer and use it in GitHub Desktop.
Simple SurrealDB Auth.js Adapter

Simple SurrealDB Auth.js Adapter

Simple Auth.js Adapter I made for SurrealDB. It uses REST API. Surrealdb-js package doesn't have to be installed. It's also important to change the generateSessionToken() function in SolidAuthConfig as shown below (#6074).

Adapter code:

import {
  Adapter,
  AdapterUser,
  AdapterAccount,
  AdapterSession,
} from "@auth/core/adapters";

export interface SurrealAdapterOptions {
  user: string;
  password: string;
  namespace: string;
  database: string;
  url: string;
}

async function querySql<T = any>(
  options: SurrealAdapterOptions,
  sql: string
): Promise<Response<T>> {
  const buffer = Buffer.from(options.user + ":" + options.password);
  const auth = `Basic ${buffer.toString("base64")}`;

  const headers = {
    Accept: "application/json",
    Authorization: auth,
    NS: options.namespace,
    DB: options.database,
  };

  const response = await fetch(options.url, {
    method: "POST",
    headers,
    body: sql,
  });

  return await response.json();
}

type Response<T> = { time: string; status: string; result: T[] }[];

const id = (id: string) => id.split(":")[1] ?? id;

const userFromResponse = (response: any): AdapterUser => {
  return {
    ...response,
    id: id(response.id),
    emailVerified: response.emailVerified
      ? new Date(response.emailVerified)
      : null,
  };
};

const dataFromUser = (user: Partial<AdapterUser>): any => {
  return {
    ...user,
    id: undefined,
    emailVerified: user.emailVerified ? user.emailVerified.toISOString() : null,
  };
};

const accountFromResponse = (response: any): AdapterAccount => {
  return {
    ...response,
    id: id(response.id),
    userId: response.userId ? id(response.userId) : null,
  };
};

const dataFromAccount = (account: AdapterAccount): any => {
  return {
    ...account,
    id: undefined,
    userId: `user:${account.userId}`,
  };
};

const sessionFromResponse = (response: any): AdapterSession => {
  const userId = response.userId;
  return {
    userId: id(typeof userId === "string" ? userId : userId.id),
    expires: new Date(response.expires ?? ""),
    sessionToken: response.sessionToken ?? "",
  };
};

const dataFromSession = (session: AdapterSession): any => {
  return {
    ...session,
    userId: `user:${session.userId}`,
    expires: session.expires.toISOString(),
  };
};

const SurrealAdapter = (options: SurrealAdapterOptions): Adapter => {
  return {
    async createUser(user) {
      const data = dataFromUser(user);
      const query = `INSERT INTO user ${JSON.stringify(data)};`;
      const response = await querySql(options, query);
      return userFromResponse(response[0].result[0]);
    },

    async getUser(id) {
      const query = `SELECT * FROM user:${id} LIMIT 1`;
      const response = await querySql(options, query);

      if (response[0].result.length == 1) {
        return userFromResponse(response[0].result[0]);
      } else {
        return null;
      }
    },

    async getUserByEmail(email) {
      const query = `SELECT * FROM user WHERE email="${email}" LIMIT 1;`;
      const response = await querySql(options, query);

      if (response[0].result.length == 1) {
        return userFromResponse(response[0].result[0]);
      } else {
        return null;
      }
    },

    async getUserByAccount({ providerAccountId, provider }) {
      const query = `SELECT userId FROM account WHERE providerAccountId="${providerAccountId}" AND provider="${provider}" FETCH userId;`;
      const response = await querySql(options, query);

      if (response[0].result.length == 1 && response[0].result[0].userId) {
        return userFromResponse(response[0].result[0].userId);
      } else {
        return null;
      }
    },

    async updateUser(user) {
      const data = dataFromUser(user);
      const query = `INSERT INTO user:${user.id} ${JSON.stringify(data)};`;
      const response = await querySql(options, query);
      return userFromResponse(response[0].result[0]);
    },

    async deleteUser(userId) {
      const query = `
        BEGIN TRANSACTION;
        DELETE account WHERE userId=user:${userId};
        DELETE session WHERE userId=user:${userId};
        DELETE user:${userId};
        COMMIT TRANSACTION;
      `;
      await querySql(options, query);
    },

    async linkAccount(account) {
      const data = dataFromAccount(account);
      const query = `INSERT INTO account ${JSON.stringify(data)};`;
      const response = await querySql(options, query);
      return accountFromResponse(response[0].result[0]);
    },

    async unlinkAccount({ providerAccountId, provider }) {
      const query = `DELETE account WHERE providerAccountId="${providerAccountId}" AND provider="${provider}";`;
      await querySql(options, query);
    },

    async createSession(session) {
      const data = dataFromSession(session);
      const query = `INSERT INTO session ${JSON.stringify(data)};`;
      const response = await querySql(options, query);
      return sessionFromResponse(response[0].result[0]);
    },

    async getSessionAndUser(sessionToken) {
      const query = `SELECT * FROM session WHERE sessionToken="${sessionToken}" FETCH userId;`;
      const response = await querySql(options, query);

      if (response[0].result.length == 1 && response[0].result[0].userId) {
        const session = sessionFromResponse(response[0].result[0]);
        const user = userFromResponse(response[0].result[0].userId);
        return { session, user };
      } else {
        return null;
      }
    },

    async updateSession(session) {
      const expires = session.expires?.toISOString();
      const query = `UPDATE session SET expires="${expires}" WHERE sessionToken="${session.sessionToken}";`;
      const response = await querySql(options, query);
      if (response[0].result.length == 1) {
        return sessionFromResponse(response[0].result[0]);
      } else {
        return null;
      }
    },

    async deleteSession(sessionToken) {
      const query = `DELETE session WHERE sessionToken="${sessionToken}";`;
      await querySql(options, query);
    },

    async createVerificationToken({ identifier, expires, token }) {
      const data = { identifier, expires, token };
      const query = `INSERT INTO vertification_token ${JSON.stringify(data)};`;
      await querySql(options, query);
      return data;
    },

    async useVerificationToken({ identifier, token }) {
      const query = `DELETE verification_token WHERE identifier="${identifier}" AND token="${token}" LIMIT 1";`;
      const response = await querySql(options, query);

      if (response[0].result.length == 1) {
        const result = response[0].result[0];
        return {
          ...result,
          expires: new Date(result.expires),
        };
      } else {
        return null;
      }
    },
  };
};

export default SurrealAdapter;

How to use:

export const authOptions: SolidAuthConfig = {
  adapter: SurrealAdapter({
    user: process.env.SURREAL_USER as string,
    password: process.env.SURREAL_PASSWORD as string,
    url: process.env.SURREAL_SQL_URL as string, // ../sql has to be included
    namespace: "test",
    database: "test",
  }),
  session: {
    strategy: "database",
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60, // 24 hours
    generateSessionToken: () => crypto.randomUUID(),
  },
  providers: [
    // @ts-expect-error Types are wrong
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID as string,
      clientSecret: process.env.AUTH_GITHUB_SECRET as string,
    }),
  ],
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment