diff --git a/packages/adapter-mysql/README.md b/packages/adapter-mysql/README.md new file mode 100644 index 0000000000..a07a2e6c0a --- /dev/null +++ b/packages/adapter-mysql/README.md @@ -0,0 +1,2 @@ +# mysql-adapter +sasadf \ No newline at end of file diff --git a/packages/adapter-mysql/package.json b/packages/adapter-mysql/package.json new file mode 100644 index 0000000000..cf36ac5196 --- /dev/null +++ b/packages/adapter-mysql/package.json @@ -0,0 +1,46 @@ +{ + "name": "mysql-adapter", + "version": "1.0.0", + "description": "", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "type": "module", + "types": "./dist/index.d.ts", + "scripts": { + "test": "./test/test.sh", + "build": "tsup" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/MAHB98/mysql-adapter.git" + }, + "keywords": [ + + "next-auth", + "@auth", + "Auth.js", + "next.js", + "oauth", + "mysql" + ], + "author": "MAHB98", + "license": "ISC", + "bugs": { + "url": "https://github.com/MAHB98/mysql-adapter/issues" + }, + "homepage": "https://github.com/MAHB98/mysql-adapter#readme", + "dependencies": { + "@auth/core": "^0.34.1", + "tsc": "^2.0.4", + "tslib": "^2.6.3" + }, + "devDependencies": { + + "@types/node": "^20.16.5", + "tsup": "^8.2.4", + "typescript": "^5.5.4" + }, + "peerDependencies": { + "mysql2": "3.11.2" + } +} diff --git a/packages/adapter-mysql/schema.sql b/packages/adapter-mysql/schema.sql new file mode 100644 index 0000000000..7e88e90edf --- /dev/null +++ b/packages/adapter-mysql/schema.sql @@ -0,0 +1,45 @@ +use mysql_database +CREATE TABLE sessions +( + id SERIAL, + userId INTEGER NOT NULL, + expires TIMESTAMP(3) NOT NULL, + sessionToken VARCHAR(255) NOT NULL, + + PRIMARY KEY (id) +); +CREATE TABLE users +( + id SERIAL, + name VARCHAR(255), + email VARCHAR(255), + emailVerified timestamp(3), + image TEXT, + + PRIMARY KEY (id) +); +CREATE TABLE accounts +( + id SERIAL, + userId INTEGER NOT NULL, + type VARCHAR(255) NOT NULL, + provider VARCHAR(255) NOT NULL, + providerAccountId VARCHAR(255) NOT NULL, + refresh_token TEXT, + access_token TEXT, + expires_at BIGINT, + id_token TEXT, + scope TEXT, + session_state TEXT, + token_type TEXT, + + PRIMARY KEY (id) +); +CREATE TABLE verification_token +( + identifier varchar(100) NOT NULL, + expires TIMESTAMP(3) NOT NULL, + token varchar(100) NOT NULL, + + PRIMARY KEY (identifier, token) +); diff --git a/packages/adapter-mysql/src/index.ts b/packages/adapter-mysql/src/index.ts new file mode 100644 index 0000000000..005e54c6da --- /dev/null +++ b/packages/adapter-mysql/src/index.ts @@ -0,0 +1,280 @@ +import type { + Adapter, + AdapterUser, + VerificationToken, + AdapterSession, +} from "@auth/core/adapters"; +import type { OkPacketParams, Pool } from "mysql2/promise"; +// type k = AdapterUser & { password?: string }; +export const mapExpiresAt = (account: any): any => { + const expires_at: number = parseInt(account.expires_at); + return { + ...account, + expires_at, + }; +}; + +const mysqlAdapter = (client: Pool): Adapter => { + return { + async createVerificationToken( + verificationToken: VerificationToken + ): Promise { + const { identifier, expires, token } = verificationToken; + const sql = ` + INSERT INTO verification_token ( identifier, expires, token ) + VALUES (?, ?, ?) + `; + await client.query(sql, [identifier, expires, token]); + return verificationToken; + }, + async useVerificationToken({ + identifier, + token, + }: { + identifier: string; + token: string; + }): Promise { + const sql = `select * from verification_token + where identifier = ? and token = ?`; + const sql1 = `delete from verification_token + where identifier = ? and token = ? + `; + + try { + const [result] = (await client.query(sql, [identifier, token])) as any; + if (result[0]) { + const [result1] = (await client.query(sql1, [ + identifier, + token, + ])) as OkPacketParams[]; + if (result1.affectedRows) { + return result[0]; + } + } + return null; + } catch { + return null; + } + }, + + async createUser(user: Omit) { + const { name, email, emailVerified, image } = user; + const sql = ` + INSERT INTO users (name, email, emailVerified, image) + VALUES (?, ?, ?, ?) + `; + const [result] = (await client.query(sql, [ + name, + email, + emailVerified, + image, + ])) as OkPacketParams[]; + const [result1] = await client.query("select * from users where id=?", [ + result.insertId, + ]); + const [reResult1] = result1 as AdapterUser[]; + return reResult1; + }, + async getUser(id) { + const sql = `select * from users where id = ?`; + try { + const [result] = await client.query(sql, [id]); + const reResult = result as AdapterUser[]; + return reResult[0] ? reResult[0] : null; + } catch (e) { + return null; + } + }, + async getUserByEmail(email) { + const sql = `select * from users where email = ?`; + const [result] = await client.query(sql, [email]); + const reResult = result as AdapterUser[]; + + return reResult[0] ? reResult[0] : null; + }, + async getUserByAccount({ + providerAccountId, + provider, + }): Promise { + const sql = ` + select * from users u join accounts a on u.id = a.userId + where + a.provider = ? + and + a.providerAccountId = ?`; + + const [result] = await client.query(sql, [provider, providerAccountId]); + const reResult = result as AdapterUser[]; + if (reResult[0]) { + const { email, emailVerified, id, image, name } = reResult[0]; + return { name, email, emailVerified, id, image }; + } + return null; + }, + async updateUser( + user: Partial & Pick + ): Promise { + const fetchSql = `select * from users where id = ?`; + const [query1] = await client.query(fetchSql, [user.id]); + const [oldUser] = query1 as AdapterUser[]; + + const newUser = { + ...oldUser, + ...user, + }; + + const { id, name, email, emailVerified, image } = newUser; + const updateSql = ` + UPDATE users set + name =?, email = ?, emailVerified = ?, image = ? + where id = ? + + `; + const [query2] = (await client.query(updateSql, [ + name, + email, + emailVerified, + image, + id, + ])) as OkPacketParams[]; + if (query2.affectedRows) return newUser; + else throw new Error("nothing find "); + }, + async linkAccount(account) { + const sql = ` + insert into accounts + ( + userId, + provider, + type, + providerAccountId, + access_token, + expires_at, + refresh_token, + id_token, + scope, + session_state, + token_type + ) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + const params = [ + account.userId, + account.provider, + account.type, + account.providerAccountId, + account.access_token, + account.expires_at, + account.refresh_token, + account.id_token, + account.scope, + account.session_state, + account.token_type, + ]; + + const [result] = (await client.query(sql, params)) as OkPacketParams[]; + return result.insertId ? mapExpiresAt(account) : null; + }, + async createSession({ sessionToken, userId, expires }) { + if (userId === undefined) { + throw Error(`userId is undef in createSession`); + } + const sql = `insert into sessions (userId, expires, sessionToken) + values (?, ?, ?) + `; + + const [result] = (await client.query(sql, [ + userId, + expires, + sessionToken, + ])) as OkPacketParams[]; + if (result.insertId) + return { id: result.insertId, sessionToken, userId, expires }; + else throw new Error("nothing created "); + }, + + async getSessionAndUser(sessionToken: string | undefined): Promise<{ + session: AdapterSession; + user: AdapterUser; + } | null> { + if (sessionToken === undefined) { + return null; + } + const [result1] = await client.query( + `select * from sessions where sessionToken = ?`, + [sessionToken] + ); + const [oldUser] = result1 as AdapterSession[]; + + if (!oldUser) { + return null; + } + + const [result2] = await client.query("select * from users where id = ?", [ + oldUser.userId, + ]); + const [newUser] = result2 as AdapterUser[]; + + if (!newUser) { + return null; + } + return { + session: oldUser, + user: newUser, + }; + }, + async updateSession( + session: Partial & Pick + ): Promise { + const { sessionToken } = session; + const [result1] = await client.query( + `select * from sessions where sessionToken = ?`, + [sessionToken] + ); + const [originalSession] = result1 as AdapterSession[]; + + if (!originalSession) { + return null; + } + + const newSession: AdapterSession = { + ...originalSession, + ...session, + }; + const sql = ` + UPDATE sessions set + expires = ? + where sessionToken = ? + `; + const [result] = (await client.query(sql, [ + newSession.expires, + newSession.sessionToken, + ])) as OkPacketParams[]; + return result.affectedRows ? newSession : null; + }, + async deleteSession(sessionToken) { + const sql = `delete from sessions where sessionToken = ?`; + await client.query(sql, [sessionToken]); + }, + async unlinkAccount(partialAccount) { + const { provider, providerAccountId } = partialAccount; + const sql = `delete from accounts where providerAccountId = ? and provider = ?`; + await client.query(sql, [providerAccountId, provider]); + }, + async deleteUser(userId: string) { + await client.beginTransaction(); + try { + await client.query(`delete from ${userTable} where id = ?`, [userId]); + await client.query(`delete from sessions where userId = ?`, [userId]); + await client.query(`delete from accounts where userId = ?`, [userId]); + await client.commit(); + } catch (error) { + await client.rollback(); + throw error; + } + return null; + }, + }; +}; +export default mysqlAdapter; diff --git a/packages/adapter-mysql/test/index.test.ts b/packages/adapter-mysql/test/index.test.ts new file mode 100644 index 0000000000..e2618ec825 --- /dev/null +++ b/packages/adapter-mysql/test/index.test.ts @@ -0,0 +1,50 @@ +import { runBasicTests } from "../utils/adapter"; +import mysqlAdapter, { mapExpiresAt } from "../src"; +import mysql from "mysql2/promise"; +import { AdapterUser } from "@auth/core/adapters"; +const client = mysql.createPool({ + host: "127.0.0.1", + database: "mysql_database", + user: "root", + password: "mysql", + port: 3333, +}); + +runBasicTests({ + adapter: mysqlAdapter(client), + db: { + disconnect: async () => { + await client.end(); + }, + user: async (id: string) => { + const sql = `select * from users where id = ?`; + const [result] = await client.query(sql, [id]); + + const [result1] = result as AdapterUser[]; + + return result1 ? result1 : null; + }, + account: async (account) => { + const sql = ` + select * from accounts where providerAccountId = ?`; + + const [result] = await client.query(sql, [account.providerAccountId]); + return result[0] ? mapExpiresAt(result[0]) : null; + }, + session: async (sessionToken) => { + const [result1] = await client.query( + `select * from sessions where sessionToken = ?`, + [sessionToken] + ); + return result1[0] ? result1[0] : null; + }, + async verificationToken(identifier_token) { + const { identifier, token } = identifier_token; + const sql = ` + select * from verification_token where identifier = ? and token = ?`; + + const [result] = await client.query(sql, [identifier, token]); + return result[0] ? result[0] : null; + }, + }, +}); diff --git a/packages/adapter-mysql/test/test.sh b/packages/adapter-mysql/test/test.sh new file mode 100755 index 0000000000..7c0b88e24a --- /dev/null +++ b/packages/adapter-mysql/test/test.sh @@ -0,0 +1,19 @@ +CONTAINER_NAME=authjs-mysql-test + +docker run -d --rm\ + --name ${CONTAINER_NAME} \ + -e MYSQL_ROOT_PASSWORD=mysql \ + -e MYSQL_DATABASE=mysql_database\ + -p 3333:3306\ + -v "$(pwd)"/schema.sql:/docker-entrypoint-initdb.d/schema.sql \ + mysql:latest +echo "waiting 10s for db to start..." +sleep 20 +# Always stop container, but exit with 1 when tests are failing +if vitest run -c ./utils/vitest.config.ts; +# https://docs.docker.com/ +then +docker stop ${CONTAINER_NAME} +else + docker stop ${CONTAINER_NAME} && exit 1 +fi \ No newline at end of file diff --git a/packages/adapter-mysql/tsconfig.json b/packages/adapter-mysql/tsconfig.json new file mode 100644 index 0000000000..a52c2d4046 --- /dev/null +++ b/packages/adapter-mysql/tsconfig.json @@ -0,0 +1,110 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ES2022", + /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "Node16" /* Specify what module code is generated. */, + "rootDir": "./src" /* Specify the root folder within your source files. */, + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true /* Create source map files for emitted JavaScript files. */, + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + //"outFile": "./" /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */, + "outDir": "./dist" /* Specify an output folder for all emitted files. */, + "removeComments": true /* Disable emitting comments. */, + "noEmit": true /* Disable emitting files from a compilation. */, + "importHelpers": true /* Allow importing helper functions from tslib once per project, instead of including them per-file. */, + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + "noEmitOnError": true /* Disable emitting files if any type checking errors are reported. */, + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/adapter-mysql/tsup.config.ts b/packages/adapter-mysql/tsup.config.ts new file mode 100644 index 0000000000..7c70f06dab --- /dev/null +++ b/packages/adapter-mysql/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; +export default defineConfig({ + format: ["cjs", "esm"], + entry: ["./src/index.ts"], + dts: true, + shims: true, + skipNodeModulesBundle: true, + clean: true, + target: "es2022", +});