diff --git a/apps/client/src/App.tsx b/apps/client/src/App.tsx index 64b4927..a9a23f2 100644 --- a/apps/client/src/App.tsx +++ b/apps/client/src/App.tsx @@ -1,11 +1,50 @@ import './App.css' function App() { + const handleGoogleLogin = () => { + // Redirect to your server's Google OAuth endpoint + window.location.href = 'http://localhost:3001/auth/google'; + }; return ( - <> -

Coming Soon!

- +
+

MemoBrain

+ +
) } diff --git a/apps/server/.env.example b/apps/server/.env.example index 8043f37..e99ef52 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -2,21 +2,14 @@ GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= GOOGLE_REDIRECT_URI= -DATABASE_USER= -DATABASE_PASSWORD= -DATABASE_HOST= -DATABASE_PORT= -DATABASE_NAME= - ACCESS_JWT_SECRET= REFRESH_JWT_SECRET= -ACCESS_TOKEN_COOKIE_NAME= -REFRESH_TOKEN_COOKIE_NAME= -ACCESS_TOKEN_EXPIRE= -REFRESH_TOKEN_EXPIRE= +ACCESS_TOKEN_COOKIE_NAME=access_token +REFRESH_TOKEN_COOKIE_NAME=refresh_token +ACCESS_TOKEN_EXPIRE=15m +REFRESH_TOKEN_EXPIRE=7d CLIENT_APP_URL= -HASH_ALGORITHM= -HASH_SALT_ROUNDS= \ No newline at end of file +PORT= \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index ff0410b..823239a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -3,7 +3,13 @@ "module": "src/index.ts", "type": "module", "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/cookie-parser": "^1.4.10", + "@types/cors": "^2.8.19", + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^25.0.10", + "@types/passport": "^1.0.17", + "@types/passport-google-oauth2": "^0.1.10" }, "peerDependencies": { "typescript": "^5.0.0" @@ -16,7 +22,18 @@ "check-types": "tsc --noEmit" }, "dependencies": { - "@types/express": "^5.0.3", - "express": "^5.1.0" + "@repo/db": "workspace:*", + "@repo/types": "workspace:*", + "@types/express": "^5.0.6", + "@types/ms": "^2.1.0", + "argon2": "^0.44.0", + "cookie-parser": "^1.4.7", + "cors": "^2.8.6", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "ms": "^2.1.3", + "passport": "^0.7.0", + "passport-google-oauth2": "^0.2.0", + "passport-jwt": "^4.0.1" } } \ No newline at end of file diff --git a/apps/server/src/config/index.ts b/apps/server/src/config/index.ts new file mode 100644 index 0000000..2751141 --- /dev/null +++ b/apps/server/src/config/index.ts @@ -0,0 +1,87 @@ +import type { StringValue } from 'ms'; + +interface Config { + // Google OAuth + google: { + clientId: string; + clientSecret: string; + redirectUri: string; + }; + + // JWT + jwt: { + accessSecret: string; + refreshSecret: string; + }; + + // Cookies + cookies: { + accessTokenName: string; + refreshTokenName: string; + accessTokenExpire: StringValue; + refreshTokenExpire: StringValue; + }; + + // Client + client: { + appUrl: string; + }; + + // Server + server: { + port: number; + }; +} + +const getEnvVar = (key: string, defaultValue?: string): string => { + const value = process.env[key] || defaultValue; + if (!value) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; +}; + +const getEnvVarAsNumber = (key: string, defaultValue?: number): number => { + const value = process.env[key]; + if (!value) { + if (defaultValue !== undefined) { + return defaultValue; + } + throw new Error(`Missing required environment variable: ${key}`); + } + const parsed = parseInt(value, 10); + if (isNaN(parsed)) { + throw new Error(`Environment variable ${key} must be a valid number`); + } + return parsed; +}; + +export const config: Config = { + google: { + clientId: getEnvVar("GOOGLE_CLIENT_ID"), + clientSecret: getEnvVar("GOOGLE_CLIENT_SECRET"), + redirectUri: getEnvVar("GOOGLE_REDIRECT_URI"), + }, + + jwt: { + accessSecret: getEnvVar("ACCESS_JWT_SECRET"), + refreshSecret: getEnvVar("REFRESH_JWT_SECRET"), + }, + + cookies: { + accessTokenName: getEnvVar("ACCESS_TOKEN_COOKIE_NAME", "access_token"), + refreshTokenName: getEnvVar("REFRESH_TOKEN_COOKIE_NAME", "refresh_token"), + accessTokenExpire: getEnvVar("ACCESS_TOKEN_EXPIRE", "15m") as StringValue, + refreshTokenExpire: getEnvVar("REFRESH_TOKEN_EXPIRE", "7d") as StringValue, + }, + + client: { + appUrl: getEnvVar("CLIENT_APP_URL", "http://localhost:5173"), + }, + + server: { + port: getEnvVarAsNumber("PORT", 3001), + }, +}; + +export default config; diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 5f98491..e386036 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -1,12 +1,35 @@ import express from "express"; import type { Request, Response } from "express"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import "dotenv/config"; +import passport from "./utils/passport"; + +import { authRouter } from "./routes/auth.routes"; +import { userRouter } from "./routes/user.routes"; +import config from "./config"; +import { loggerMiddleware } from "./middlewares/logger.middleware"; const app = express(); +app.use(express.json()); +app.use(loggerMiddleware); +app.use(cookieParser()); +app.use( + cors({ + credentials: true, + origin: config.client.appUrl, + }), +); +app.use(passport.initialize()); + +app.use("/auth", authRouter); +app.use("/user", userRouter); + app.get("/", (req: Request, res: Response) => { - res.send("Hello World"); + res.send("Hello World"); }); -app.listen(3000, () => { - console.log("Server is running on port 3000"); +app.listen(config.server.port, () => { + console.log(`Server is running on port ${config.server.port}`); }); diff --git a/apps/server/src/middlewares/auth.middleware.ts b/apps/server/src/middlewares/auth.middleware.ts new file mode 100644 index 0000000..0ef8c8f --- /dev/null +++ b/apps/server/src/middlewares/auth.middleware.ts @@ -0,0 +1,32 @@ +import type { Request, Response, NextFunction } from "express"; +import { verifyToken } from "../utils/auth.helper"; +import config from "../config"; + +export const authMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + try { + // Get token from cookie + const token = req.cookies[config.cookies.accessTokenName]; + + if (!token) { + return res.status(401).json({ + error: "Unauthorized - No token provided", + }); + } + + // Verify token + const decoded = verifyToken(token, config.jwt.accessSecret); + + // Attach user to request + (req as any).user = decoded; + + next(); + } catch (error) { + return res.status(401).json({ + error: "Unauthorized - Invalid token", + }); + } +}; \ No newline at end of file diff --git a/apps/server/src/middlewares/logger.middleware.ts b/apps/server/src/middlewares/logger.middleware.ts new file mode 100644 index 0000000..7b5302f --- /dev/null +++ b/apps/server/src/middlewares/logger.middleware.ts @@ -0,0 +1,99 @@ +import type { Request, Response, NextFunction } from "express"; + +/** + * Logger middleware to log all incoming requests and their responses + */ +export const loggerMiddleware = ( + req: Request, + res: Response, + next: NextFunction +) => { + const startTime = Date.now(); + const timestamp = new Date().toISOString(); + + // Log incoming request + console.log("\nšŸ”µ Incoming Request:"); + console.log(` Timestamp: ${timestamp}`); + console.log(` Method: ${req.method}`); + console.log(` Path: ${req.path}`); + console.log(` URL: ${req.originalUrl}`); + console.log(` IP: ${req.ip}`); + console.log(` User-Agent: ${req.get("user-agent") || "N/A"}`); + + // Log request body if present + if (req.body && Object.keys(req.body).length > 0) { + console.log(` Body:`, JSON.stringify(req.body, null, 2)); + } + + // Log query parameters if present + if (req.query && Object.keys(req.query).length > 0) { + console.log(` Query:`, JSON.stringify(req.query, null, 2)); + } + + // Capture the original res.json and res.send methods + const originalJson = res.json.bind(res); + const originalSend = res.send.bind(res); + + // Override res.json to log response + res.json = function (body: any) { + logResponse(body); + return originalJson(body); + }; + + // Override res.send to log response + res.send = function (body: any) { + logResponse(body); + return originalSend(body); + }; + + // Function to log response details + const logResponse = (body: any) => { + const duration = Date.now() - startTime; + const statusCode = res.statusCode; + + console.log("\n🟢 Outgoing Response:"); + console.log(` Method: ${req.method}`); + console.log(` Path: ${req.path}`); + console.log(` Status: ${statusCode} ${getStatusText(statusCode)}`); + console.log(` Duration: ${duration}ms`); + + // Log response body (truncate if too large) + if (body) { + const bodyStr = typeof body === "string" ? body : JSON.stringify(body); + const truncatedBody = + bodyStr.length > 500 ? bodyStr.substring(0, 500) + "..." : bodyStr; + console.log(` Response:`, truncatedBody); + } + + console.log("─".repeat(80)); + }; + + // Handle response finish event for cases where json/send aren't called + res.on("finish", () => { + const duration = Date.now() - startTime; + const statusCode = res.statusCode; + + // Only log if we haven't already logged (json/send weren't called) + if (!res.headersSent || res.writableEnded) { + console.log("\n🟔 Response Finished:"); + console.log(` Method: ${req.method}`); + console.log(` Path: ${req.path}`); + console.log(` Status: ${statusCode} ${getStatusText(statusCode)}`); + console.log(` Duration: ${duration}ms`); + console.log("─".repeat(80)); + } + }); + + next(); +}; + +/** + * Helper function to get human-readable status text + */ +function getStatusText(statusCode: number): string { + if (statusCode >= 200 && statusCode < 300) return "āœ“ Success"; + if (statusCode >= 300 && statusCode < 400) return "↻ Redirect"; + if (statusCode >= 400 && statusCode < 500) return "⚠ Client Error"; + if (statusCode >= 500) return "āœ— Server Error"; + return ""; +} diff --git a/apps/server/src/routes/auth.routes.ts b/apps/server/src/routes/auth.routes.ts new file mode 100644 index 0000000..171c1f0 --- /dev/null +++ b/apps/server/src/routes/auth.routes.ts @@ -0,0 +1,21 @@ +import { Router } from "express"; +import { + handleRefresh, + handleGoogleCallback, + handleGoogleSignIn, + handleSignIn, + handleSignOut, + handleSignUp, +} from "../services/auth.services"; +import { authMiddleware } from "../middlewares/auth.middleware"; + +export const authRouter = Router(); + +authRouter.post("/signup", handleSignUp); +authRouter.post("/signin", handleSignIn); +authRouter.post("/signout", handleSignOut); + +authRouter.get("/google", handleGoogleSignIn); +authRouter.get("/google/callback", handleGoogleCallback); + +authRouter.post("/refresh", handleRefresh); diff --git a/apps/server/src/routes/user.routes.ts b/apps/server/src/routes/user.routes.ts new file mode 100644 index 0000000..9c04a63 --- /dev/null +++ b/apps/server/src/routes/user.routes.ts @@ -0,0 +1,7 @@ +import { Router } from "express"; +import { handleGetMe } from "../services/user.services"; +import { authMiddleware } from "../middlewares/auth.middleware"; + +export const userRouter = Router(); + +userRouter.get("/me", authMiddleware, handleGetMe); diff --git a/apps/server/src/services/auth.services.ts b/apps/server/src/services/auth.services.ts new file mode 100644 index 0000000..8ca98ed --- /dev/null +++ b/apps/server/src/services/auth.services.ts @@ -0,0 +1,360 @@ +import type { Request, Response, NextFunction } from "express"; +import type { + SignUpRequest, + SignInRequest, + SignOutRequest, + RefreshTokenRequest, + AuthResponse, +} from "../types/auth.types"; +import passport from "../utils/passport"; +import { createToken, hashString, verifyString, verifyToken } from "../utils/auth.helper"; +import config from "../config"; +import type { User } from "@repo/types"; +import { db, users, refreshTokens } from "@repo/db"; +import { eq, and } from "drizzle-orm"; +import crypto from "crypto"; + +export const handleSignUp = async ( + req: Request<{}, AuthResponse, SignUpRequest>, + res: Response +) => { + try { + const { name, email, password } = req.body; + + // Check if user exists + const existingUser = await db.query.users.findFirst({ + where: eq(users.email, email), + }); + + if (existingUser) { + return res.status(400).json({ + error: "User already exists", + } as any); + } + + // Hash password + const passwordHash = await hashString(password); + + // Create user + const [newUser] = await db + .insert(users) + .values({ + name, + email, + passwordHash, + googleId: null, + }) + .returning(); + + if (!newUser) { + return res.status(500).json({ + error: "Failed to create user", + } as any); + } + + // Generate tokens + const accessToken = createToken(newUser, config.jwt.accessSecret); + const refreshToken = createToken(newUser, config.jwt.refreshSecret); + + // Store refresh token + const tokenHash = crypto.createHash("sha256").update(refreshToken).digest("hex"); + await db.insert(refreshTokens).values({ + userId: newUser.id, + tokenHash, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7 days + }); + + // Set cookies + res.cookie(config.cookies.accessTokenName, accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 15 * 60 * 1000, + }); + + res.cookie(config.cookies.refreshTokenName, refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.status(201).json({ + user: { + id: newUser.id, + name: newUser.name, + email: newUser.email, + }, + accessToken, + refreshToken, + }); + } catch (error) { + console.error("Sign up error:", error); + res.status(500).json({ error: "Sign up failed" } as any); + } +}; + +export const handleSignIn = async ( + req: Request<{}, AuthResponse, SignInRequest>, + res: Response +) => { + try { + const { email, password } = req.body; + + // Find user + const user = await db.query.users.findFirst({ + where: eq(users.email, email), + }); + + if (!user || !user.passwordHash) { + return res.status(401).json({ + error: "Invalid credentials", + } as any); + } + + // Verify password + const isValid = await verifyString(password, user.passwordHash); + if (!isValid) { + return res.status(401).json({ + error: "Invalid credentials", + } as any); + } + + // Generate tokens + const accessToken = createToken(user, config.jwt.accessSecret); + const refreshToken = createToken(user, config.jwt.refreshSecret); + + // Store refresh token + const tokenHash = crypto.createHash("sha256").update(refreshToken).digest("hex"); + await db.insert(refreshTokens).values({ + userId: user.id, + tokenHash, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + + // Set cookies + res.cookie(config.cookies.accessTokenName, accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 15 * 60 * 1000, + }); + + res.cookie(config.cookies.refreshTokenName, refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.json({ + user: { + id: user.id, + name: user.name, + email: user.email, + }, + accessToken, + refreshToken, + }); + } catch (error) { + res.status(500).json({ error: "Sign in failed" } as any); + } +}; + +export const handleSignOut = async ( + req: Request<{}, {}, SignOutRequest>, + res: Response<{ message: string }> +) => { + try { + // Get refresh token from cookie (preferred) or body + const refreshToken = req.cookies[config.cookies.refreshTokenName] || req.body?.refreshToken; + + if (refreshToken) { + const tokenHash = crypto.createHash("sha256").update(refreshToken).digest("hex"); + + // Revoke token + await db + .update(refreshTokens) + .set({ revokedAt: new Date() }) + .where(eq(refreshTokens.tokenHash, tokenHash)); + } + + // Clear cookies + res.clearCookie(config.cookies.accessTokenName); + res.clearCookie(config.cookies.refreshTokenName); + + res.json({ message: "Signed out successfully" }); + } catch (error) { + console.error("Sign out error:", error); + res.status(500).json({ error: "Sign out failed" } as any); + } +}; + +export const handleGoogleSignIn = ( + req: Request, + res: Response, + next: NextFunction +) => { + passport.authenticate("google", { + scope: ["profile", "email"], + session: false, + })(req, res, next); +}; + +export const handleGoogleCallback = ( + req: Request, + res: Response, + next: NextFunction +) => { + passport.authenticate( + "google", + { session: false }, + async (err: Error | null, user: User | false, info: any) => { + try { + if (err) { + return res.status(500).json({ + error: "Authentication failed", + message: err.message, + } as any); + } + + if (!user) { + return res.status(401).json({ + error: "Authentication failed", + message: "No user found", + } as any); + } + + // Generate tokens + const accessToken = createToken(user, config.jwt.accessSecret); + const refreshToken = createToken(user, config.jwt.refreshSecret); + + // Store refresh token + const tokenHash = crypto.createHash("sha256").update(refreshToken).digest("hex"); + await db.insert(refreshTokens).values({ + userId: user.id, + tokenHash, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + + // Set cookies + res.cookie(config.cookies.accessTokenName, accessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 15 * 60 * 1000, + }); + + res.cookie(config.cookies.refreshTokenName, refreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.json({ + user: { + id: user.id, + name: user.name, + email: user.email, + }, + accessToken, + refreshToken, + }); + } catch (error) { + next(error); + } + } + )(req, res, next); +}; + +export const handleRefresh = async ( + req: Request<{}, AuthResponse, RefreshTokenRequest>, + res: Response +) => { + try { + // Get refresh token from cookie (preferred) or body + const refreshToken = req.cookies[config.cookies.refreshTokenName] || req.body?.refreshToken; + + if (!refreshToken) { + return res.status(401).json({ + error: "Refresh token required", + } as any); + } + + // Verify token + const decoded = verifyToken(refreshToken, config.jwt.refreshSecret) as any; + const tokenHash = crypto.createHash("sha256").update(refreshToken).digest("hex"); + + // Check if token exists and is valid + const storedToken = await db.query.refreshTokens.findFirst({ + where: and( + eq(refreshTokens.tokenHash, tokenHash), + eq(refreshTokens.userId, decoded.id) + ), + }); + + if (!storedToken || storedToken.revokedAt || storedToken.expiresAt < new Date()) { + return res.status(401).json({ + error: "Invalid refresh token", + } as any); + } + + // Get user + const user = await db.query.users.findFirst({ + where: eq(users.id, decoded.id), + }); + + if (!user) { + return res.status(401).json({ + error: "User not found", + } as any); + } + + // Revoke old token + await db + .update(refreshTokens) + .set({ revokedAt: new Date() }) + .where(eq(refreshTokens.id, storedToken.id)); + + // Generate new tokens + const newAccessToken = createToken(user, config.jwt.accessSecret); + const newRefreshToken = createToken(user, config.jwt.refreshSecret); + + // Store new refresh token + const newTokenHash = crypto.createHash("sha256").update(newRefreshToken).digest("hex"); + await db.insert(refreshTokens).values({ + userId: user.id, + tokenHash: newTokenHash, + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), + }); + + // Set cookies + res.cookie(config.cookies.accessTokenName, newAccessToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 15 * 60 * 1000, + }); + + res.cookie(config.cookies.refreshTokenName, newRefreshToken, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + res.json({ + user: { + id: user.id, + name: user.name, + email: user.email, + }, + accessToken: newAccessToken, + refreshToken: newRefreshToken, + }); + } catch (error) { + console.error("Token refresh error:", error); + res.status(401).json({ error: "Token refresh failed" } as any); + } +}; \ No newline at end of file diff --git a/apps/server/src/services/user.services.ts b/apps/server/src/services/user.services.ts new file mode 100644 index 0000000..5f7a83a --- /dev/null +++ b/apps/server/src/services/user.services.ts @@ -0,0 +1,41 @@ +import type { Request, Response } from "express"; +import type { UserProfileResponse } from "../types/user.types"; +import { db, users } from "@repo/db"; +import { eq } from "drizzle-orm"; + +export const handleGetMe = async ( + req: Request, + res: Response +) => { + try { + // User ID should be set by auth middleware + const userId = (req as any).user?.id; + + if (!userId) { + return res.status(401).json({ + error: "Unauthorized", + } as any); + } + + const user = await db.query.users.findFirst({ + where: eq(users.id, userId), + }); + + if (!user) { + return res.status(404).json({ + error: "User not found", + } as any); + } + + res.json({ + id: user.id, + name: user.name, + email: user.email, + createdAt: user.createdAt, + }); + } catch (error) { + res.status(500).json({ + error: "Failed to get user", + } as any); + } +}; \ No newline at end of file diff --git a/apps/server/src/types/auth.types.ts b/apps/server/src/types/auth.types.ts new file mode 100644 index 0000000..9133c5f --- /dev/null +++ b/apps/server/src/types/auth.types.ts @@ -0,0 +1,41 @@ +// Auth Service Types + +// Sign Up Request Body +export interface SignUpRequest { + name: string; + email: string; + password: string; +} + +// Sign In Request Body (Email/Password) +export interface SignInRequest { + email: string; + password: string; +} + +// Sign Out Request (optional - can use auth middleware to get user) +export interface SignOutRequest { + refreshToken?: string; +} + +// Refresh Token Request +export interface RefreshTokenRequest { + refreshToken: string; +} + +// Auth Response (tokens + user info) +export interface AuthResponse { + user: { + id: string; + name: string; + email: string; + }; + accessToken: string; + refreshToken: string; +} + +// Google OAuth Callback Query Params +export interface GoogleCallbackQuery { + code: string; + state?: string; +} diff --git a/apps/server/src/types/user.types.ts b/apps/server/src/types/user.types.ts new file mode 100644 index 0000000..6311a4e --- /dev/null +++ b/apps/server/src/types/user.types.ts @@ -0,0 +1,9 @@ +// User Service Types + +// User Profile Response (public user data) +export interface UserProfileResponse { + id: string; + name: string; + email: string; + createdAt: Date; +} \ No newline at end of file diff --git a/apps/server/src/utils/auth.helper.ts b/apps/server/src/utils/auth.helper.ts new file mode 100644 index 0000000..597a19c --- /dev/null +++ b/apps/server/src/utils/auth.helper.ts @@ -0,0 +1,33 @@ +import jwt, { type SignOptions } from "jsonwebtoken"; +import argon2 from "argon2"; +import type { User } from "@repo/types"; +import config from "../config"; + +// Create Access JWT token +export const createToken = (user: User, secret: string) => { + const options: SignOptions = { + expiresIn: config.cookies.accessTokenExpire, + }; + + return jwt.sign({ + id: user.id, + name: user.name, + email: user.email, + }, secret, options); +}; + +// Verify Access JWT token +export const verifyToken = (token: string, secret: string) => { + return jwt.verify(token, secret); +}; + +export const hashString = async (str: string) => { + return await argon2.hash(str, { + type: argon2.argon2id, + timeCost: 2, + }); +}; + +export const verifyString = async (str: string, hash: string) => { + return await argon2.verify(hash, str); +}; diff --git a/apps/server/src/utils/passport.ts b/apps/server/src/utils/passport.ts new file mode 100644 index 0000000..168045a --- /dev/null +++ b/apps/server/src/utils/passport.ts @@ -0,0 +1,87 @@ +import { Strategy as GoogleStrategy } from "passport-google-oauth2"; +import type { VerifyCallback } from "passport-google-oauth2"; +import passport from "passport"; +import { db, users } from "@repo/db"; +import { eq } from "drizzle-orm"; +import config from "../config"; + +// Configure Google OAuth2 Strategy +passport.use( + new GoogleStrategy( + { + clientID: config.google.clientId, + clientSecret: config.google.clientSecret, + callbackURL: config.google.redirectUri, + passReqToCallback: true, + }, + async ( + request: any, + accessToken: string, + refreshToken: string, + profile: any, + done: VerifyCallback + ) => { + try { + // Check if user exists with this Google ID + const existingUser = await db.query.users.findFirst({ + where: eq(users.googleId, profile.id), + }); + + if (existingUser) { + // User exists, return it + return done(null, existingUser); + } + + // Check if user exists with this email + const userByEmail = await db.query.users.findFirst({ + where: eq(users.email, profile.email!), + }); + + if (userByEmail) { + // Link Google account to existing user + const [updatedUser] = await db + .update(users) + .set({ googleId: profile.id }) + .where(eq(users.id, userByEmail.id)) + .returning(); + + return done(null, updatedUser); + } + + // Create new user + const [newUser] = await db + .insert(users) + .values({ + name: profile.displayName, + email: profile.email!, + googleId: profile.id, + passwordHash: null, // Google OAuth users don't have passwords + }) + .returning(); + + return done(null, newUser); + } catch (error) { + return done(error as Error, undefined); + } + } + ) +); + +// Serialize user for session (if using sessions) +passport.serializeUser((user: any, done) => { + done(null, user.id); +}); + +// Deserialize user from session (if using sessions) +passport.deserializeUser(async (id: string, done) => { + try { + const user = await db.query.users.findFirst({ + where: eq(users.id, id), + }); + done(null, user); + } catch (error) { + done(error, null); + } +}); + +export default passport; \ No newline at end of file diff --git a/apps/server/src/utils/user.helper.ts b/apps/server/src/utils/user.helper.ts new file mode 100644 index 0000000..e69de29 diff --git a/bun.lockb b/bun.lockb index 3d5dc4e..ada53b3 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 472ecd8..8d3f69f 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "MemoBrain", + "name": "cobrain", "private": true, "scripts": { "build": "turbo run build",