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,39 +1,37 @@
|
|||||||
import dotenv from 'dotenv';
|
import dotenv from "dotenv";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
|
|
||||||
dotenv.config({ path: path.resolve(__dirname, '../../.env') });
|
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
port: parseInt(process.env.PORT || '3001', 10),
|
port: parseInt(process.env.PORT || "3001", 10),
|
||||||
nodeEnv: process.env.NODE_ENV || 'development',
|
nodeEnv: process.env.NODE_ENV || "development",
|
||||||
|
|
||||||
database: {
|
database: {
|
||||||
url: process.env.DATABASE_URL || '',
|
url: process.env.DATABASE_URL || "",
|
||||||
},
|
},
|
||||||
|
|
||||||
oidc: {
|
oidc: {
|
||||||
issuerUrl: process.env.OIDC_ISSUER_URL || '',
|
issuerUrl: process.env.OIDC_ISSUER_URL || "",
|
||||||
clientId: process.env.OIDC_CLIENT_ID || '',
|
clientId: process.env.OIDC_CLIENT_ID || "",
|
||||||
redirectUri: process.env.OIDC_REDIRECT_URI || 'http://localhost:3001/auth/callback',
|
redirectUri:
|
||||||
|
process.env.OIDC_REDIRECT_URI ||
|
||||||
|
"http://localhost:3001/api/auth/callback",
|
||||||
},
|
},
|
||||||
|
|
||||||
session: {
|
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
|
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||||
},
|
},
|
||||||
|
|
||||||
cors: {
|
cors: {
|
||||||
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
|
origin: process.env.FRONTEND_URL || "http://localhost:5173",
|
||||||
credentials: true,
|
credentials: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function validateConfig(): void {
|
export function validateConfig(): void {
|
||||||
const required = [
|
const required = ["DATABASE_URL", "OIDC_ISSUER_URL", "OIDC_CLIENT_ID"];
|
||||||
'DATABASE_URL',
|
|
||||||
'OIDC_ISSUER_URL',
|
|
||||||
'OIDC_CLIENT_ID',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const key of required) {
|
for (const key of required) {
|
||||||
if (!process.env[key]) {
|
if (!process.env[key]) {
|
||||||
@@ -42,6 +40,8 @@ export function validateConfig(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (config.session.secret.length < 32) {
|
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 express from "express";
|
||||||
import cors from 'cors';
|
import cors from "cors";
|
||||||
import session from 'express-session';
|
import session from "express-session";
|
||||||
import { config, validateConfig } from './config';
|
import { config, validateConfig } from "./config";
|
||||||
import { connectDatabase } from './prisma/client';
|
import { connectDatabase } from "./prisma/client";
|
||||||
import { errorHandler, notFoundHandler } from './middleware/errorHandler';
|
import { errorHandler, notFoundHandler } from "./middleware/errorHandler";
|
||||||
|
|
||||||
// Import routes
|
// Import routes
|
||||||
import authRoutes from './routes/auth.routes';
|
import authRoutes from "./routes/auth.routes";
|
||||||
import clientRoutes from './routes/client.routes';
|
import clientRoutes from "./routes/client.routes";
|
||||||
import projectRoutes from './routes/project.routes';
|
import projectRoutes from "./routes/project.routes";
|
||||||
import timeEntryRoutes from './routes/timeEntry.routes';
|
import timeEntryRoutes from "./routes/timeEntry.routes";
|
||||||
import timerRoutes from './routes/timer.routes';
|
import timerRoutes from "./routes/timer.routes";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
// Validate configuration
|
// Validate configuration
|
||||||
@@ -22,40 +22,44 @@ async function main() {
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
// CORS
|
// CORS
|
||||||
app.use(cors({
|
app.use(
|
||||||
|
cors({
|
||||||
origin: config.cors.origin,
|
origin: config.cors.origin,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Body parsing
|
// Body parsing
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
// Session
|
// Session
|
||||||
app.use(session({
|
app.use(
|
||||||
|
session({
|
||||||
secret: config.session.secret,
|
secret: config.session.secret,
|
||||||
resave: false,
|
resave: false,
|
||||||
saveUninitialized: false,
|
saveUninitialized: false,
|
||||||
name: 'sessionId',
|
name: "sessionId",
|
||||||
cookie: {
|
cookie: {
|
||||||
secure: config.nodeEnv === 'production',
|
secure: false,
|
||||||
httpOnly: true,
|
httpOnly: false,
|
||||||
maxAge: config.session.maxAge,
|
maxAge: config.session.maxAge,
|
||||||
sameSite: config.nodeEnv === 'production' ? 'strict' : 'lax',
|
sameSite: "lax",
|
||||||
},
|
},
|
||||||
}));
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
app.get('/health', (_req, res) => {
|
app.get("/health", (_req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/auth', authRoutes);
|
app.use("/api/auth", authRoutes);
|
||||||
app.use('/api/clients', clientRoutes);
|
app.use("/api/clients", clientRoutes);
|
||||||
app.use('/api/projects', projectRoutes);
|
app.use("/api/projects", projectRoutes);
|
||||||
app.use('/api/time-entries', timeEntryRoutes);
|
app.use("/api/time-entries", timeEntryRoutes);
|
||||||
app.use('/api/timer', timerRoutes);
|
app.use("/api/timer", timerRoutes);
|
||||||
|
|
||||||
// Error handling
|
// Error handling
|
||||||
app.use(notFoundHandler);
|
app.use(notFoundHandler);
|
||||||
@@ -69,6 +73,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
console.error('Failed to start server:', error);
|
console.error("Failed to start server:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from "express";
|
||||||
import { initializeOIDC, createAuthSession, getAuthorizationUrl, handleCallback, getUserInfo } from '../auth/oidc';
|
import {
|
||||||
import { syncUser } from '../middleware/auth';
|
initializeOIDC,
|
||||||
import type { AuthenticatedRequest } from '../types';
|
createAuthSession,
|
||||||
|
getAuthorizationUrl,
|
||||||
|
handleCallback,
|
||||||
|
getUserInfo,
|
||||||
|
} from "../auth/oidc";
|
||||||
|
import { syncUser } from "../middleware/auth";
|
||||||
|
import type { AuthenticatedRequest } from "../types";
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -16,7 +22,7 @@ async function ensureOIDC() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET /auth/login - Initiate OIDC login flow
|
// GET /auth/login - Initiate OIDC login flow
|
||||||
router.get('/login', async (req, res) => {
|
router.get("/login", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await ensureOIDC();
|
await ensureOIDC();
|
||||||
|
|
||||||
@@ -26,23 +32,26 @@ router.get('/login', async (req, res) => {
|
|||||||
const authorizationUrl = getAuthorizationUrl(session);
|
const authorizationUrl = getAuthorizationUrl(session);
|
||||||
res.redirect(authorizationUrl);
|
res.redirect(authorizationUrl);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
console.error("Login error:", error);
|
||||||
res.status(500).json({ error: 'Failed to initiate login' });
|
res.status(500).json({ error: "Failed to initiate login" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /auth/callback - OIDC callback handler
|
// GET /auth/callback - OIDC callback handler
|
||||||
router.get('/callback', async (req, res) => {
|
router.get("/callback", async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await ensureOIDC();
|
await ensureOIDC();
|
||||||
|
|
||||||
const oidcSession = req.session.oidc;
|
const oidcSession = req.session.oidc;
|
||||||
if (!oidcSession) {
|
if (!oidcSession) {
|
||||||
res.status(400).json({ error: 'Invalid session' });
|
res.status(400).json({ error: "Invalid session" });
|
||||||
return;
|
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);
|
const user = await getUserInfo(tokenSet);
|
||||||
|
|
||||||
// Sync user with database
|
// Sync user with database
|
||||||
@@ -53,31 +62,31 @@ router.get('/callback', async (req, res) => {
|
|||||||
delete req.session.oidc;
|
delete req.session.oidc;
|
||||||
|
|
||||||
// Redirect to frontend
|
// 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`);
|
res.redirect(`${frontendUrl}/auth/callback?success=true`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Callback error:', error);
|
console.error("Callback error:", error);
|
||||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5173';
|
const frontendUrl = process.env.FRONTEND_URL || "http://localhost:5173";
|
||||||
res.redirect(`${frontendUrl}/auth/callback?error=authentication_failed`);
|
res.redirect(`${frontendUrl}/auth/callback?error=authentication_failed`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /auth/logout - End session
|
// POST /auth/logout - End session
|
||||||
router.post('/logout', (req: AuthenticatedRequest, res) => {
|
router.post("/logout", (req: AuthenticatedRequest, res) => {
|
||||||
req.session.destroy((err) => {
|
req.session.destroy((err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500).json({ error: 'Failed to logout' });
|
res.status(500).json({ error: "Failed to logout" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.clearCookie('connect.sid');
|
res.clearCookie("connect.sid");
|
||||||
res.json({ message: 'Logged out successfully' });
|
res.json({ message: "Logged out successfully" });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// GET /auth/me - Get current user
|
// GET /auth/me - Get current user
|
||||||
router.get('/me', (req: AuthenticatedRequest, res) => {
|
router.get("/me", (req: AuthenticatedRequest, res) => {
|
||||||
if (!req.session?.user) {
|
if (!req.session?.user) {
|
||||||
res.status(401).json({ error: 'Not authenticated' });
|
res.status(401).json({ error: "Not authenticated" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.json(req.session.user);
|
res.json(req.session.user);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
DATABASE_URL: "postgresql://timetracker:timetracker_password@db:5432/timetracker"
|
DATABASE_URL: "postgresql://timetracker:timetracker_password@db:5432/timetracker"
|
||||||
OIDC_ISSUER_URL: ${OIDC_ISSUER_URL}
|
OIDC_ISSUER_URL: ${OIDC_ISSUER_URL}
|
||||||
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
|
OIDC_CLIENT_ID: ${OIDC_CLIENT_ID}
|
||||||
OIDC_REDIRECT_URI: "http://localhost:3001/auth/callback"
|
OIDC_REDIRECT_URI: "http://localhost:3001/api/auth/callback"
|
||||||
SESSION_SECRET: ${SESSION_SECRET}
|
SESSION_SECRET: ${SESSION_SECRET}
|
||||||
PORT: 3001
|
PORT: 3001
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import type { User } from '@/types';
|
import type { User } from "@/types";
|
||||||
|
|
||||||
const AUTH_BASE = '/auth';
|
const AUTH_BASE = "/api/auth";
|
||||||
|
|
||||||
export const authApi = {
|
export const authApi = {
|
||||||
login: (): void => {
|
login: (): void => {
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react';
|
import react from "@vitejs/plugin-react";
|
||||||
import path from 'path';
|
import path from "path";
|
||||||
|
|
||||||
|
const backend = "http://127.0.0.1:3001";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5173,
|
port: 5173,
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
"/api": {
|
||||||
target: 'http://localhost:3001',
|
target: backend,
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
'/auth': {
|
|
||||||
target: 'http://localhost:3001',
|
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user