fix
This commit is contained in:
110
backend/prisma/migrations/20260216150447_init/migration.sql
Normal file
110
backend/prisma/migrations/20260216150447_init/migration.sql
Normal file
@@ -0,0 +1,110 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" VARCHAR(255) NOT NULL,
|
||||
"username" VARCHAR(255) NOT NULL,
|
||||
"email" VARCHAR(255) NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "clients" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"description" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"user_id" VARCHAR(255) NOT NULL,
|
||||
|
||||
CONSTRAINT "clients_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "projects" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"description" TEXT,
|
||||
"color" VARCHAR(7),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"user_id" VARCHAR(255) NOT NULL,
|
||||
"client_id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "projects_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "time_entries" (
|
||||
"id" TEXT NOT NULL,
|
||||
"start_time" TIMESTAMPTZ NOT NULL,
|
||||
"end_time" TIMESTAMPTZ NOT NULL,
|
||||
"description" TEXT,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"user_id" VARCHAR(255) NOT NULL,
|
||||
"project_id" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "time_entries_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "ongoing_timers" (
|
||||
"id" TEXT NOT NULL,
|
||||
"start_time" TIMESTAMPTZ NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
"user_id" VARCHAR(255) NOT NULL,
|
||||
"project_id" TEXT,
|
||||
|
||||
CONSTRAINT "ongoing_timers_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "clients_user_id_idx" ON "clients"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_user_id_idx" ON "projects"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "projects_client_id_idx" ON "projects"("client_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "time_entries_user_id_idx" ON "time_entries"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "time_entries_user_id_start_time_idx" ON "time_entries"("user_id", "start_time");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "time_entries_project_id_idx" ON "time_entries"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ongoing_timers_user_id_key" ON "ongoing_timers"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "ongoing_timers_project_id_key" ON "ongoing_timers"("project_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "ongoing_timers_user_id_idx" ON "ongoing_timers"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "clients" ADD CONSTRAINT "clients_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "projects" ADD CONSTRAINT "projects_client_id_fkey" FOREIGN KEY ("client_id") REFERENCES "clients"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "time_entries" ADD CONSTRAINT "time_entries_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "time_entries" ADD CONSTRAINT "time_entries_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ongoing_timers" ADD CONSTRAINT "ongoing_timers_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "ongoing_timers" ADD CONSTRAINT "ongoing_timers_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
provider = "postgresql"
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import dotenv from "dotenv";
|
||||
import path from "path";
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
||||
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
|
||||
|
||||
export const config = {
|
||||
port: parseInt(process.env.PORT || '3001', 10),
|
||||
nodeEnv: process.env.NODE_ENV || 'development',
|
||||
|
||||
port: parseInt(process.env.PORT || "3001", 10),
|
||||
nodeEnv: process.env.NODE_ENV || "development",
|
||||
|
||||
database: {
|
||||
url: process.env.DATABASE_URL || '',
|
||||
url: process.env.DATABASE_URL || "",
|
||||
},
|
||||
|
||||
|
||||
oidc: {
|
||||
issuerUrl: process.env.OIDC_ISSUER_URL || '',
|
||||
clientId: process.env.OIDC_CLIENT_ID || '',
|
||||
redirectUri: process.env.OIDC_REDIRECT_URI || 'http://localhost:3001/auth/callback',
|
||||
issuerUrl: process.env.OIDC_ISSUER_URL || "",
|
||||
clientId: process.env.OIDC_CLIENT_ID || "",
|
||||
redirectUri:
|
||||
process.env.OIDC_REDIRECT_URI ||
|
||||
"http://localhost:3001/api/auth/callback",
|
||||
},
|
||||
|
||||
|
||||
session: {
|
||||
secret: process.env.SESSION_SECRET || 'default-secret-change-in-production',
|
||||
secret: process.env.SESSION_SECRET || "default-secret-change-in-production",
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
},
|
||||
|
||||
|
||||
cors: {
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
||||
origin: process.env.FRONTEND_URL || "http://localhost:5173",
|
||||
credentials: true,
|
||||
},
|
||||
};
|
||||
|
||||
export function validateConfig(): void {
|
||||
const required = [
|
||||
'DATABASE_URL',
|
||||
'OIDC_ISSUER_URL',
|
||||
'OIDC_CLIENT_ID',
|
||||
];
|
||||
|
||||
const required = ["DATABASE_URL", "OIDC_ISSUER_URL", "OIDC_CLIENT_ID"];
|
||||
|
||||
for (const key of required) {
|
||||
if (!process.env[key]) {
|
||||
throw new Error(`Missing required environment variable: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (config.session.secret.length < 32) {
|
||||
console.warn('Warning: SESSION_SECRET should be at least 32 characters for security');
|
||||
console.warn(
|
||||
"Warning: SESSION_SECRET should be at least 32 characters for security",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import session from 'express-session';
|
||||
import { config, validateConfig } from './config';
|
||||
import { connectDatabase } from './prisma/client';
|
||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
||||
import express from "express";
|
||||
import cors from "cors";
|
||||
import session from "express-session";
|
||||
import { config, validateConfig } from "./config";
|
||||
import { connectDatabase } from "./prisma/client";
|
||||
import { errorHandler, notFoundHandler } from "./middleware/errorHandler";
|
||||
|
||||
// Import routes
|
||||
import authRoutes from './routes/auth.routes';
|
||||
import clientRoutes from './routes/client.routes';
|
||||
import projectRoutes from './routes/project.routes';
|
||||
import timeEntryRoutes from './routes/timeEntry.routes';
|
||||
import timerRoutes from './routes/timer.routes';
|
||||
import authRoutes from "./routes/auth.routes";
|
||||
import clientRoutes from "./routes/client.routes";
|
||||
import projectRoutes from "./routes/project.routes";
|
||||
import timeEntryRoutes from "./routes/timeEntry.routes";
|
||||
import timerRoutes from "./routes/timer.routes";
|
||||
|
||||
async function main() {
|
||||
// Validate configuration
|
||||
@@ -22,40 +22,44 @@ async function main() {
|
||||
const app = express();
|
||||
|
||||
// CORS
|
||||
app.use(cors({
|
||||
origin: config.cors.origin,
|
||||
credentials: true,
|
||||
}));
|
||||
app.use(
|
||||
cors({
|
||||
origin: config.cors.origin,
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Session
|
||||
app.use(session({
|
||||
secret: config.session.secret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
name: 'sessionId',
|
||||
cookie: {
|
||||
secure: config.nodeEnv === 'production',
|
||||
httpOnly: true,
|
||||
maxAge: config.session.maxAge,
|
||||
sameSite: config.nodeEnv === 'production' ? 'strict' : 'lax',
|
||||
},
|
||||
}));
|
||||
app.use(
|
||||
session({
|
||||
secret: config.session.secret,
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
name: "sessionId",
|
||||
cookie: {
|
||||
secure: false,
|
||||
httpOnly: false,
|
||||
maxAge: config.session.maxAge,
|
||||
sameSite: "lax",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Health check
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/auth', authRoutes);
|
||||
app.use('/api/clients', clientRoutes);
|
||||
app.use('/api/projects', projectRoutes);
|
||||
app.use('/api/time-entries', timeEntryRoutes);
|
||||
app.use('/api/timer', timerRoutes);
|
||||
app.use("/api/auth", authRoutes);
|
||||
app.use("/api/clients", clientRoutes);
|
||||
app.use("/api/projects", projectRoutes);
|
||||
app.use("/api/time-entries", timeEntryRoutes);
|
||||
app.use("/api/timer", timerRoutes);
|
||||
|
||||
// Error handling
|
||||
app.use(notFoundHandler);
|
||||
@@ -69,6 +73,6 @@ async function main() {
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Failed to start server:', error);
|
||||
console.error("Failed to start server:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { initializeOIDC, createAuthSession, getAuthorizationUrl, handleCallback, getUserInfo } from '../auth/oidc';
|
||||
import { syncUser } from '../middleware/auth';
|
||||
import type { AuthenticatedRequest } from '../types';
|
||||
import { Router } from "express";
|
||||
import {
|
||||
initializeOIDC,
|
||||
createAuthSession,
|
||||
getAuthorizationUrl,
|
||||
handleCallback,
|
||||
getUserInfo,
|
||||
} from "../auth/oidc";
|
||||
import { syncUser } from "../middleware/auth";
|
||||
import type { AuthenticatedRequest } from "../types";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -16,71 +22,74 @@ async function ensureOIDC() {
|
||||
}
|
||||
|
||||
// GET /auth/login - Initiate OIDC login flow
|
||||
router.get('/login', async (req, res) => {
|
||||
router.get("/login", async (req, res) => {
|
||||
try {
|
||||
await ensureOIDC();
|
||||
|
||||
|
||||
const session = createAuthSession();
|
||||
req.session.oidc = session;
|
||||
|
||||
|
||||
const authorizationUrl = getAuthorizationUrl(session);
|
||||
res.redirect(authorizationUrl);
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ error: 'Failed to initiate login' });
|
||||
console.error("Login error:", error);
|
||||
res.status(500).json({ error: "Failed to initiate login" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /auth/callback - OIDC callback handler
|
||||
router.get('/callback', async (req, res) => {
|
||||
router.get("/callback", async (req, res) => {
|
||||
try {
|
||||
await ensureOIDC();
|
||||
|
||||
|
||||
const oidcSession = req.session.oidc;
|
||||
if (!oidcSession) {
|
||||
res.status(400).json({ error: 'Invalid session' });
|
||||
res.status(400).json({ error: "Invalid session" });
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenSet = await handleCallback(req.query as Record<string, string>, oidcSession);
|
||||
const tokenSet = await handleCallback(
|
||||
req.query as Record<string, string>,
|
||||
oidcSession,
|
||||
);
|
||||
const user = await getUserInfo(tokenSet);
|
||||
|
||||
|
||||
// Sync user with database
|
||||
await syncUser(user);
|
||||
|
||||
|
||||
// Store user in session
|
||||
req.session.user = user;
|
||||
delete req.session.oidc;
|
||||
|
||||
|
||||
// Redirect to frontend
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||
res.redirect(`${frontendUrl}/auth/callback?success=true`);
|
||||
} catch (error) {
|
||||
console.error('Callback error:', error);
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||
console.error("Callback error:", error);
|
||||
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||
res.redirect(`${frontendUrl}/auth/callback?error=authentication_failed`);
|
||||
}
|
||||
});
|
||||
|
||||
// POST /auth/logout - End session
|
||||
router.post('/logout', (req: AuthenticatedRequest, res) => {
|
||||
router.post("/logout", (req: AuthenticatedRequest, res) => {
|
||||
req.session.destroy((err) => {
|
||||
if (err) {
|
||||
res.status(500).json({ error: 'Failed to logout' });
|
||||
res.status(500).json({ error: "Failed to logout" });
|
||||
return;
|
||||
}
|
||||
res.clearCookie('connect.sid');
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
res.clearCookie("connect.sid");
|
||||
res.json({ message: "Logged out successfully" });
|
||||
});
|
||||
});
|
||||
|
||||
// GET /auth/me - Get current user
|
||||
router.get('/me', (req: AuthenticatedRequest, res) => {
|
||||
router.get("/me", (req: AuthenticatedRequest, res) => {
|
||||
if (!req.session?.user) {
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
res.status(401).json({ error: "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
res.json(req.session.user);
|
||||
});
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user