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",