diff --git a/AccessVsRefresh.txt b/AccessVsRefresh.txt new file mode 100644 index 0000000..382d649 --- /dev/null +++ b/AccessVsRefresh.txt @@ -0,0 +1,12 @@ +Access Token + +* JWT +* Contains all of the info someone needs to be logged +* Says this user has Access +* Only available in current session + +Refresh Token +* JWT +* Only contains session id +* If valid, then it is used to generate new access Token +* Used to refresh the access token \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 83e68ba..8145a28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "bcryptjs": "^2.4.3", "dotenv": "^8.2.0", "fastify": "^3.12.0", + "fastify-cookie": "^5.1.0", "fastify-static": "^4.0.1", + "jsonwebtoken": "^8.5.1", "mongodb": "^3.6.4" } }, @@ -134,6 +136,11 @@ "node": ">=0.6.19" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -158,6 +165,14 @@ "node": ">= 0.6" } }, + "node_modules/cookie-signature": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz", + "integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -216,6 +231,14 @@ "node": ">=8" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -309,6 +332,16 @@ "node": ">=10.16.0" } }, + "node_modules/fastify-cookie": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-cookie/-/fastify-cookie-5.1.0.tgz", + "integrity": "sha512-AN5C/p7YVSgnW1D9fcUL10yRIN+9lcOtyps3h4/5ZsxwrHVgdNH5T77CbnIrzfAx6qz7K/8NYQCTE8cxZIJcJg==", + "dependencies": { + "cookie": "^0.4.0", + "cookie-signature": "^1.1.0", + "fastify-plugin": "^3.0.0" + } + }, "node_modules/fastify-error": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.0.tgz", @@ -441,6 +474,54 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "node_modules/jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=4", + "npm": ">=1.4.28" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/light-my-request": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.4.1.tgz", @@ -453,6 +534,41 @@ "set-cookie-parser": "^2.4.1" } }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -996,6 +1112,11 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.5.tgz", "integrity": "sha512-kDuEzldR21lHciPQAIulLs1LZlCXdLziXI6Mb/TDkwXhb//UORJNPXgcRs2CuO4H0DcMkpfT3/ySsP3unoZjBg==" }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -1014,6 +1135,11 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" }, + "cookie-signature": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.1.0.tgz", + "integrity": "sha512-Alvs19Vgq07eunykd3Xy2jF0/qSNv2u7KDbAek9H5liV1UMijbqFs5cycZvv5dVsvseT/U4H8/7/w8Koh35C4A==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1052,6 +1178,14 @@ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -1130,6 +1264,16 @@ "tiny-lru": "^7.0.0" } }, + "fastify-cookie": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-cookie/-/fastify-cookie-5.1.0.tgz", + "integrity": "sha512-AN5C/p7YVSgnW1D9fcUL10yRIN+9lcOtyps3h4/5ZsxwrHVgdNH5T77CbnIrzfAx6qz7K/8NYQCTE8cxZIJcJg==", + "requires": { + "cookie": "^0.4.0", + "cookie-signature": "^1.1.0", + "fastify-plugin": "^3.0.0" + } + }, "fastify-error": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/fastify-error/-/fastify-error-0.3.0.tgz", @@ -1244,6 +1388,49 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "light-my-request": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-4.4.1.tgz", @@ -1256,6 +1443,41 @@ "set-cookie-parser": "^2.4.1" } }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", diff --git a/package.json b/package.json index d5a5701..0a6cbfe 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "bcryptjs": "^2.4.3", "dotenv": "^8.2.0", "fastify": "^3.12.0", + "fastify-cookie": "^5.1.0", "fastify-static": "^4.0.1", + "jsonwebtoken": "^8.5.1", "mongodb": "^3.6.4" } } diff --git a/src/accounts/authorize.js b/src/accounts/authorize.js index 464412a..73e2219 100644 --- a/src/accounts/authorize.js +++ b/src/accounts/authorize.js @@ -12,7 +12,6 @@ export async function authorizeUser(email, password) { const savedPassword = userData.password // Compare password with one in database const isAuthorized = await compare(password, savedPassword) - console.log("isAuthorized", isAuthorized) // Return boolean of if password is correct - return isAuthorized + return { isAuthorized, userId: userData._id } } \ No newline at end of file diff --git a/src/accounts/logUserIn.js b/src/accounts/logUserIn.js new file mode 100644 index 0000000..e8f2dcd --- /dev/null +++ b/src/accounts/logUserIn.js @@ -0,0 +1,27 @@ +import { createSession } from './session.js' +import { createTokens } from './tokens.js' + +export async function logUserIn(userId, request, reply) { + const connectionInformation = { + ip: request.ip, + userAgent: request.headers['user-agent'], + } + // Create Session + const sessionToken = await createSession(userId, connectionInformation) + // Create JWT + const { accessToken, refreshToken } = await createTokens(sessionToken, userId) + // Set Cookie + const now = new Date() + // Get date, 30 days in the future + const refreshExpires = now.setDate(now.getDate() + 30) + reply.setCookie('refreshToken', refreshToken, { + path: "/", + domain: "localhost", + httpOnly: true, + expires: refreshExpires, + }).setCookie('accessToken', accessToken, { + path: "/", + domain: "localhost", + httpOnly: true, + }) +} \ No newline at end of file diff --git a/src/accounts/session.js b/src/accounts/session.js new file mode 100644 index 0000000..c9f9c25 --- /dev/null +++ b/src/accounts/session.js @@ -0,0 +1,25 @@ +import { randomBytes } from 'crypto' + +export async function createSession(userId, connection) { + try { + // Generate a session token + const sessionToken = randomBytes(43).toString("hex") + // Retrieve connection information + const { ip, userAgent } = connection + // database insert for ession + const { session } = await import('../session/session.js') + await session.insertOne({ + sessionToken, + userId, + valid: true, + userAgent, + ip, + updatedAt: new Date(), + createdAt: new Date(), + }) + // Return session token + return sessionToken + } catch (e) { + throw new Error('Session Creation Failed') + } +} \ No newline at end of file diff --git a/src/accounts/tokens.js b/src/accounts/tokens.js new file mode 100644 index 0000000..283af95 --- /dev/null +++ b/src/accounts/tokens.js @@ -0,0 +1,29 @@ +import jwt from 'jsonwebtoken' + +const JWTSignature = process.env.JWT_SIGNATURE + +export async function createTokens(sessionToken, userId) { + try { + // Create Refresh Token + // Session Id + const refreshToken = jwt.sign( + { + sessionToken + }, + JWTSignature + ) + // Create Access Token + // Session Id, User Id + const accessToken = jwt.sign( + { + sessionToken, + userId, + }, + JWTSignature + ) + // Return Refresh Token & Access Token + return { accessToken, refreshToken } + } catch (e) { + console.error('e', e) + } +} \ No newline at end of file diff --git a/src/accounts/user.js b/src/accounts/user.js new file mode 100644 index 0000000..cffe381 --- /dev/null +++ b/src/accounts/user.js @@ -0,0 +1,73 @@ +import mongo from 'mongodb' +import jwt from 'jsonwebtoken' +import { createTokens } from './tokens.js' + +const { ObjectId } = mongo + +const JWTSignature = process.env.JWT_SIGNATURE + +export async function getUserFromCookies(request, reply) { + try { + const { user } = await import("../user/user.js") + const { session } = await import("../session/session.js") + // Check to make sure access token exists + if (request?.cookies?.accessToken) { + // If access token + const { accessToken } = request.cookies + // Decode access token + const decodedAccessToken = jwt.verify(accessToken, JWTSignature) + // Return user from record + return user.findOne({ + _id: ObjectId(decodedAccessToken?.userId), + }) + } + + if (request?.cookies?.refreshToken) { + const { refreshToken } = request.cookies + // Decode refresh token + const { sessionToken } = jwt.verify(refreshToken, JWTSignature) + // Look up session + const currentSession = await session.findOne({ sessionToken }) + // Confirm session is valid + if (currentSession.valid) { + // Look up current user + const currentUser = await user.findOne({ + _id: ObjectId(currentSession.userId) + }) + console.log('currentUser', currentUser); + // Refresh tokens + await refreshTokens(sessionToken, currentUser._id, reply) + // Retrun current user + return currentUser + } else { + // bad session + } + } + } catch (e) { + console.error(e) + } +} + +export async function refreshTokens(sessionToken, userId, reply) { + try { + // Create JWT + const { accessToken, refreshToken } = await createTokens(sessionToken, userId) + // Set Cookie + const now = new Date() + // Get date, 30 days in the future + const refreshExpires = now.setDate(now.getDate() + 30) + reply + .setCookie('refreshToken', refreshToken, { + path: "/", + domain: "localhost", + httpOnly: true, + expires: refreshExpires, + }).setCookie('accessToken', accessToken, { + path: "/", + domain: "localhost", + httpOnly: true, + }) + } catch (e) { + console.error(e) + } +} \ No newline at end of file diff --git a/src/index.js b/src/index.js index afcfd74..b811861 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,14 @@ import './env.js' import { fastify } from 'fastify' import fastifyStatic from 'fastify-static' +import fastifyCookie from 'fastify-cookie'; import path from 'path' import { fileURLToPath } from 'url' import { connectDb } from './db.js' import { registerUser } from './accounts/register.js' import { authorizeUser } from './accounts/authorize.js' +import { logUserIn } from './accounts/logUserIn.js' +import { getUserFromCookies } from './accounts/user.js' // ESM specific "features" const __filename = fileURLToPath(import.meta.url) @@ -15,6 +18,10 @@ const app = fastify() async function startApp() { try { + app.register(fastifyCookie, { + secret: process.env.COOKIE_SIGNATURE, + }) + app.register(fastifyStatic, { root: path.join(__dirname, "public"), }) @@ -34,19 +41,42 @@ async function startApp() { app.post('/api/authorize', {}, async (request, reply) => { try { console.log(request.body.email, request.body.password) - const userId = await authorizeUser( + const { isAuthorized, userId } = await authorizeUser( request.body.email, request.body.password ) + if (isAuthorized) { + await logUserIn(userId, request, reply) + reply.send({ + data: "User Logged In", + }) + } + reply.send({ + data: "Auth Failed", + }) } catch (e) { console.error('e', e); } }) - // app.get("/", {}, (request, reply) => { - // reply.send({ - // data: "hello world", - // }) - // }) + + app.get("/test", {}, async (request, reply) => { + try { + // Verify user login + const user = await getUserFromCookies(request, reply) + // Return user email, if it exists, otherwise return unauthorized + if (user?._id) { + reply.send({ + data: user, + }) + } else { + reply.send({ + data: "User Loopup Failed" + }) + } + } catch (e) { + throw new Error(e) + } + }) await app.listen(3000); console.log('🚀 Server Listening at port: 3000'); diff --git a/src/session/session.js b/src/session/session.js new file mode 100644 index 0000000..80ff5ac --- /dev/null +++ b/src/session/session.js @@ -0,0 +1,4 @@ +import { client } from '../db.js' + +export const session = client.db("test").collection("session") +