Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions apps/client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<p>Coming Soon!</p>
</>
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
gap: '20px'
}}>
<h1>MemoBrain</h1>
<button
onClick={handleGoogleLogin}
style={{
padding: '12px 24px',
fontSize: '16px',
fontWeight: '600',
backgroundColor: '#4285f4',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '10px',
transition: 'background-color 0.2s'
}}
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#357ae8'}
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#4285f4'}
>
<svg width="18" height="18" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/>
<path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/>
<path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/>
<path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/>
<path fill="none" d="M0 0h48v48H0z"/>
</svg>
Login with Google
</button>
</div>
)
}

Expand Down
17 changes: 5 additions & 12 deletions apps/server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
PORT=
23 changes: 20 additions & 3 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
}
}
87 changes: 87 additions & 0 deletions apps/server/src/config/index.ts
Original file line number Diff line number Diff line change
@@ -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;
29 changes: 26 additions & 3 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
@@ -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}`);
});
32 changes: 32 additions & 0 deletions apps/server/src/middlewares/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -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",
});
}
};
99 changes: 99 additions & 0 deletions apps/server/src/middlewares/logger.middleware.ts
Original file line number Diff line number Diff line change
@@ -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 "";
}
Loading
Loading