feat: enhance audit logging, add MCP settings, and production docker setup
continuous-integration/drone/push Build is passing
continuous-integration/drone/push Build is passing
- Implemented comprehensive audit logging for Tasks, Users, Settings, Goals, Labels, AI Chat, and Rewards. - Added Admin UI for MCP Server settings and Audit Logs. - Created docker-compose-production.yml with Traefik configuration. - Fixed backend bugs (missing storage methods, route closure). - Added Audit Logging Guidelines.
This commit is contained in:
+305
-111
@@ -1,9 +1,11 @@
|
||||
import { type User, type InsertUser, type Label, type InsertLabel, type SharedLabel, type Task, type InsertTask, type XpEvent, type InsertXpEvent, type Goal, type InsertGoal, xpEvents, goals, type Reward, type InsertReward, type UserReward, type InsertUserReward, rewards, userRewards, systemSettings, type SystemSettings, type InsertSystemSettings, type SharedTask, type InsertSharedTask, type UserTaskAccess, type InsertUserTaskAccess, sharedTasks, userTaskAccess, type InsertPasswordResetToken, type PasswordResetToken } from "../shared/schema.js";
|
||||
import { type User, type InsertUser, type Label, type InsertLabel, type SharedLabel, type Task, type InsertTask, type XpEvent, type InsertXpEvent, type Goal, type InsertGoal, type Reward, type InsertReward, type UserReward, type InsertUserReward, type SystemSettings, type InsertSystemSettings, type SharedTask, type InsertSharedTask, type UserTaskAccess, type InsertUserTaskAccess, type InsertPasswordResetToken, type PasswordResetToken, type Conversation, type InsertConversation, type Message, type InsertMessage, type AuditLog, type InsertAuditLog } from "@shared/schema";
|
||||
import * as schema from "@shared/schema";
|
||||
import { getDatabase, pool } from "./db";
|
||||
import { eq, sql, and, desc, asc, gt, ne } from "drizzle-orm";
|
||||
import { randomUUID } from "crypto";
|
||||
import session from "express-session";
|
||||
import createMemoryStore from "memorystore";
|
||||
import connectPg from "connect-pg-simple";
|
||||
import { pool } from "./db.js";
|
||||
|
||||
const MemoryStore = createMemoryStore(session);
|
||||
const PostgresStore = connectPg(session);
|
||||
@@ -29,7 +31,6 @@ export interface IStorage {
|
||||
createSharedTask(sharedTask: InsertSharedTask): Promise<SharedTask>; // Alias for shareTask standard naming
|
||||
createUserTaskAccess(access: InsertUserTaskAccess): Promise<UserTaskAccess>; // Alias
|
||||
|
||||
getSharedTasks(userId: string): Promise<SharedTask[]>; // Tasks shared WITH user
|
||||
getSharedTasks(userId: string): Promise<SharedTask[]>; // Tasks shared WITH user
|
||||
getUserTaskAccess(viewerId: string): Promise<UserTaskAccess[]>; // Access Viewer has to Owners
|
||||
getTaskSharedUsers(taskId: string): Promise<User[]>; // Get users a task is shared WITH
|
||||
@@ -49,6 +50,7 @@ export interface IStorage {
|
||||
|
||||
// Labels
|
||||
getAllLabels(): Promise<Label[]>;
|
||||
getLabels(userId: string): Promise<Label[]>;
|
||||
getLabel(id: string): Promise<Label | undefined>;
|
||||
createLabel(label: InsertLabel): Promise<Label>;
|
||||
updateLabel(id: string, updates: Partial<InsertLabel>): Promise<Label | undefined>;
|
||||
@@ -60,6 +62,8 @@ export interface IStorage {
|
||||
createTask(task: InsertTask & { userId?: string }): Promise<Task>;
|
||||
updateTask(id: string, updates: Partial<InsertTask>): Promise<Task | undefined>;
|
||||
deleteTask(id: string): Promise<boolean>;
|
||||
searchTasks(query: string, userId: string): Promise<Task[]>;
|
||||
getSubtasks(parentTaskId: string): Promise<Task[]>;
|
||||
|
||||
// Gamification
|
||||
logXpEvent(event: InsertXpEvent): Promise<XpEvent>;
|
||||
@@ -71,9 +75,11 @@ export interface IStorage {
|
||||
getAllRewards(): Promise<Reward[]>;
|
||||
getUserRewards(userId: string): Promise<UserReward[]>;
|
||||
createReward(reward: InsertReward): Promise<Reward>;
|
||||
createReward(reward: InsertReward): Promise<Reward>;
|
||||
createUserReward(userReward: InsertUserReward): Promise<UserReward>;
|
||||
getReward(id: string): Promise<Reward | undefined>;
|
||||
purchaseReward(userId: string, rewardId: string, cost: number): Promise<{ success: boolean; user: User; userReward: UserReward }>;
|
||||
|
||||
// History
|
||||
// History
|
||||
getXpEvents(userId: string): Promise<XpEvent[]>;
|
||||
|
||||
@@ -81,6 +87,22 @@ export interface IStorage {
|
||||
createPasswordResetToken(token: InsertPasswordResetToken): Promise<PasswordResetToken>;
|
||||
getPasswordResetToken(token: string): Promise<PasswordResetToken | undefined>;
|
||||
markPasswordResetTokenUsed(id: string): Promise<void>;
|
||||
|
||||
// AI Chat
|
||||
createConversation(userId: string, title?: string): Promise<Conversation>;
|
||||
getConversations(userId: string): Promise<Conversation[]>;
|
||||
getConversation(id: string): Promise<Conversation | undefined>;
|
||||
updateConversation(id: string, title: string): Promise<Conversation | undefined>;
|
||||
deleteConversation(id: string): Promise<boolean>;
|
||||
addMessage(message: InsertMessage): Promise<Message>;
|
||||
getMessages(conversationId: string): Promise<Message[]>;
|
||||
getMessage(id: string): Promise<Message | undefined>;
|
||||
updateMessage(id: string, content: string): Promise<Message>;
|
||||
deleteMessagesAfter(conversationId: string, after: Date, excludeMessageId?: string): Promise<void>;
|
||||
|
||||
// Audit Logs
|
||||
createAuditLog(log: InsertAuditLog): Promise<AuditLog>;
|
||||
getAuditLogs(limit?: number): Promise<AuditLog[]>;
|
||||
}
|
||||
|
||||
export class MemStorage implements IStorage {
|
||||
@@ -98,6 +120,7 @@ export class MemStorage implements IStorage {
|
||||
private sharedLabels: Map<string, SharedLabel>;
|
||||
private userTaskAccess: Map<string, UserTaskAccess>;
|
||||
private passwordResetTokens: Map<string, PasswordResetToken>; // id -> Token
|
||||
private auditLogs: Map<string, AuditLog>;
|
||||
|
||||
sessionStore: session.Store;
|
||||
|
||||
@@ -110,11 +133,11 @@ export class MemStorage implements IStorage {
|
||||
this.goals = new Map();
|
||||
this.rewards = new Map();
|
||||
this.userRewards = new Map();
|
||||
this.userRewards = new Map();
|
||||
this.sharedTasks = new Map();
|
||||
this.sharedLabels = new Map();
|
||||
this.userTaskAccess = new Map();
|
||||
this.passwordResetTokens = new Map();
|
||||
this.auditLogs = new Map();
|
||||
this.sessionStore = new MemoryStore({
|
||||
checkPeriod: 86400000,
|
||||
});
|
||||
@@ -340,6 +363,12 @@ export class MemStorage implements IStorage {
|
||||
return Array.from(this.labels.values());
|
||||
}
|
||||
|
||||
async getLabels(userId: string): Promise<Label[]> {
|
||||
return Array.from(this.labels.values()).filter(
|
||||
l => l.creatorId === null || l.creatorId === userId
|
||||
);
|
||||
}
|
||||
|
||||
async getLabel(id: string): Promise<Label | undefined> {
|
||||
return this.labels.get(id);
|
||||
}
|
||||
@@ -401,10 +430,23 @@ export class MemStorage implements IStorage {
|
||||
// Merge and Dedupe
|
||||
const combined = [...myTasks, ...sharedToMe, ...globalSharedTasks, ...tasksFromSharedLabels];
|
||||
const unique = Array.from(new Map(combined.map(t => [t.id, t])).values());
|
||||
|
||||
return unique;
|
||||
}
|
||||
|
||||
async searchTasks(query: string, userId: string): Promise<Task[]> {
|
||||
const allTasks = await this.getTasksForUser(userId);
|
||||
if (!query) return allTasks;
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return allTasks.filter(t =>
|
||||
t.title.toLowerCase().includes(lowerQuery) ||
|
||||
(t.description && t.description.toLowerCase().includes(lowerQuery))
|
||||
);
|
||||
}
|
||||
|
||||
async getSubtasks(parentTaskId: string): Promise<Task[]> {
|
||||
return Array.from(this.tasks.values()).filter(t => t.parentTaskId === parentTaskId);
|
||||
}
|
||||
|
||||
async getTask(id: string): Promise<Task | undefined> {
|
||||
return this.tasks.get(id);
|
||||
}
|
||||
@@ -424,7 +466,10 @@ export class MemStorage implements IStorage {
|
||||
notes: insertTask.notes || null,
|
||||
labelId: insertTask.labelId || null,
|
||||
energyLevel: insertTask.energyLevel || "medium",
|
||||
|
||||
estimatedDuration: insertTask.estimatedDuration || null,
|
||||
parentTaskId: insertTask.parentTaskId || null,
|
||||
startDate: insertTask.startDate || null,
|
||||
dependencies: insertTask.dependencies || null,
|
||||
userId: insertTask.userId || null // Set ownership
|
||||
};
|
||||
@@ -546,6 +591,25 @@ export class MemStorage implements IStorage {
|
||||
return userReward;
|
||||
}
|
||||
|
||||
async getReward(id: string): Promise<Reward | undefined> {
|
||||
return this.rewards.get(id);
|
||||
}
|
||||
|
||||
async purchaseReward(userId: string, rewardId: string, cost: number): Promise<{ success: boolean; user: User; userReward: UserReward }> {
|
||||
const user = this.users.get(userId);
|
||||
if (!user) throw new Error("User not found");
|
||||
if (user.xp < cost) throw new Error("Insufficient XP");
|
||||
|
||||
// Deduct XP
|
||||
user.xp -= cost;
|
||||
this.users.set(userId, user);
|
||||
|
||||
// Create User Reward
|
||||
const userReward = await this.createUserReward({ userId, rewardId });
|
||||
|
||||
return { success: true, user, userReward };
|
||||
}
|
||||
|
||||
async getXpEvents(userId: string): Promise<XpEvent[]> {
|
||||
return Array.from(this.xpEvents.values())
|
||||
.filter(e => e.userId === userId)
|
||||
@@ -576,11 +640,63 @@ export class MemStorage implements IStorage {
|
||||
this.passwordResetTokens.set(id, token);
|
||||
}
|
||||
}
|
||||
|
||||
// AI Chat Stubs
|
||||
async createConversation(userId: string, title?: string): Promise<Conversation> {
|
||||
throw new Error("MemStorage: AI Chat not implemented.");
|
||||
}
|
||||
async getConversations(userId: string): Promise<Conversation[]> {
|
||||
return [];
|
||||
}
|
||||
async getConversation(id: string): Promise<Conversation | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
async deleteConversation(id: string): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
async addMessage(message: InsertMessage): Promise<Message> {
|
||||
throw new Error("MemStorage: AI Chat not implemented.");
|
||||
}
|
||||
async getMessages(conversationId: string): Promise<Message[]> {
|
||||
return [];
|
||||
}
|
||||
async getMessage(id: string): Promise<Message | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
async updateMessage(id: string, content: string): Promise<Message> {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
async deleteMessagesAfter(conversationId: string, after: Date): Promise<void> {
|
||||
// No-op
|
||||
}
|
||||
async updateConversation(id: string, title: string): Promise<Conversation | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Audit Logs (MemStorage)
|
||||
async createAuditLog(insertLog: InsertAuditLog): Promise<AuditLog> {
|
||||
const id = randomUUID();
|
||||
const log: AuditLog = {
|
||||
...insertLog,
|
||||
id,
|
||||
userId: insertLog.userId || null,
|
||||
entityId: insertLog.entityId || null,
|
||||
details: insertLog.details || null,
|
||||
source: insertLog.source || "USER",
|
||||
createdAt: new Date(),
|
||||
};
|
||||
this.auditLogs.set(id, log);
|
||||
return log;
|
||||
}
|
||||
|
||||
async getAuditLogs(limit = 100): Promise<AuditLog[]> {
|
||||
return Array.from(this.auditLogs.values())
|
||||
.sort((a, b) => (b.createdAt && a.createdAt ? b.createdAt.getTime() - a.createdAt.getTime() : 0))
|
||||
.slice(0, limit);
|
||||
}
|
||||
}
|
||||
|
||||
import { getDatabase } from './db.js';
|
||||
import { eq, sql, desc, and } from 'drizzle-orm';
|
||||
import * as schema from '../shared/schema.js';
|
||||
|
||||
|
||||
export class DbStorage implements IStorage {
|
||||
private db = getDatabase();
|
||||
@@ -671,11 +787,16 @@ export class DbStorage implements IStorage {
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// ... (rest of DbStorage labels, tasks, etc. implementation - unchanged mostly)
|
||||
async getAllLabels(): Promise<Label[]> {
|
||||
return await this.db.select().from(schema.labels);
|
||||
}
|
||||
|
||||
async getLabels(userId: string): Promise<Label[]> {
|
||||
return await this.db.select().from(schema.labels).where(
|
||||
sql`${schema.labels.creatorId} IS NULL OR ${schema.labels.creatorId} = ${userId}`
|
||||
);
|
||||
}
|
||||
|
||||
async getLabel(id: string): Promise<Label | undefined> {
|
||||
const result = await this.db.select().from(schema.labels).where(eq(schema.labels.id, id));
|
||||
return result[0];
|
||||
@@ -701,14 +822,6 @@ export class DbStorage implements IStorage {
|
||||
}
|
||||
|
||||
async getTasksForUser(userId: string): Promise<Task[]> {
|
||||
// Complex query:
|
||||
// (tasks.userId = current)
|
||||
// OR (id IN (select taskId from sharedTasks where sharedWith = current))
|
||||
// OR (userId IN (select ownerId from userTaskAccess where viewerId = current))
|
||||
|
||||
// For simplicity in this generated code, we can do parallel queries or use `or`.
|
||||
// Drizzle's `or` and `inArray` can be used.
|
||||
|
||||
// 1. My tasks
|
||||
const result = await this.db.select().from(schema.tasks).where(eq(schema.tasks.userId, userId));
|
||||
|
||||
@@ -736,12 +849,25 @@ export class DbStorage implements IStorage {
|
||||
tasksFromSharedLabels = await this.db.select().from(schema.tasks).where(sql`${schema.tasks.labelId} IN ${sharedLabelIds}`);
|
||||
}
|
||||
|
||||
// Dedupe
|
||||
const combined = [...result, ...sharedTasks, ...globalTasks, ...tasksFromSharedLabels];
|
||||
const unique = Array.from(new Map(combined.map(t => [t.id, t])).values());
|
||||
return unique;
|
||||
}
|
||||
|
||||
async searchTasks(query: string, userId: string): Promise<Task[]> {
|
||||
const allTasks = await this.getTasksForUser(userId);
|
||||
if (!query) return allTasks;
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return allTasks.filter(t =>
|
||||
t.title.toLowerCase().includes(lowerQuery) ||
|
||||
(t.description && t.description.toLowerCase().includes(lowerQuery))
|
||||
);
|
||||
}
|
||||
|
||||
async getSubtasks(parentTaskId: string): Promise<Task[]> {
|
||||
return await this.db.select().from(schema.tasks).where(eq(schema.tasks.parentTaskId, parentTaskId));
|
||||
}
|
||||
|
||||
async getTask(id: string): Promise<Task | undefined> {
|
||||
const result = await this.db.select().from(schema.tasks).where(eq(schema.tasks.id, id));
|
||||
return result[0];
|
||||
@@ -802,8 +928,6 @@ export class DbStorage implements IStorage {
|
||||
return result[0];
|
||||
}
|
||||
|
||||
|
||||
// Rewards
|
||||
async getAllRewards(): Promise<Reward[]> {
|
||||
return await this.db.select().from(schema.rewards);
|
||||
}
|
||||
@@ -822,32 +946,47 @@ export class DbStorage implements IStorage {
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getReward(id: string): Promise<Reward | undefined> {
|
||||
const result = await this.db.select().from(schema.rewards).where(eq(schema.rewards.id, id));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async purchaseReward(userId: string, rewardId: string, cost: number): Promise<{ success: boolean; user: User; userReward: UserReward }> {
|
||||
return await this.db.transaction(async (tx) => {
|
||||
// 1. Get User and verify XP
|
||||
const userRes = await tx.select().from(schema.users).where(eq(schema.users.id, userId));
|
||||
const user = userRes[0];
|
||||
if (!user) throw new Error("User not found");
|
||||
if (user.xp < cost) throw new Error("Insufficient XP");
|
||||
|
||||
// 2. Deduct XP
|
||||
const updatedUserRes = await tx.update(schema.users)
|
||||
.set({ xp: user.xp - cost })
|
||||
.where(eq(schema.users.id, userId))
|
||||
.returning();
|
||||
|
||||
// 3. Create User Reward
|
||||
const urRes = await tx.insert(schema.userRewards).values({
|
||||
userId,
|
||||
rewardId,
|
||||
purchasedAt: new Date()
|
||||
}).returning();
|
||||
|
||||
return { success: true, user: updatedUserRes[0], userReward: urRes[0] };
|
||||
});
|
||||
}
|
||||
|
||||
async getXpEvents(userId: string): Promise<XpEvent[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.xpEvents)
|
||||
.where(eq(schema.xpEvents.userId, userId))
|
||||
.orderBy(desc(schema.xpEvents.createdAt));
|
||||
return await this.db.select().from(schema.xpEvents).where(eq(schema.xpEvents.userId, userId));
|
||||
}
|
||||
|
||||
// Social Methods (DbStorage)
|
||||
async getLeaderboard(): Promise<User[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.users)
|
||||
.where(and(
|
||||
eq(schema.users.showOnLeaderboard, true),
|
||||
eq(schema.users.isActive, true)
|
||||
))
|
||||
.orderBy(desc(schema.users.xp));
|
||||
}
|
||||
|
||||
// Auth - Password Reset (DbStorage)
|
||||
async createPasswordResetToken(insertToken: InsertPasswordResetToken): Promise<PasswordResetToken> {
|
||||
const result = await this.db.insert(schema.passwordResetTokens).values(insertToken).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getPasswordResetToken(tokenString: string): Promise<PasswordResetToken | undefined> {
|
||||
const result = await this.db.select().from(schema.passwordResetTokens).where(eq(schema.passwordResetTokens.token, tokenString));
|
||||
async getPasswordResetToken(token: string): Promise<PasswordResetToken | undefined> {
|
||||
const result = await this.db.select().from(schema.passwordResetTokens).where(eq(schema.passwordResetTokens.token, token));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
@@ -857,10 +996,57 @@ export class DbStorage implements IStorage {
|
||||
.where(eq(schema.passwordResetTokens.id, id));
|
||||
}
|
||||
|
||||
// AI Chat Implementation
|
||||
async createConversation(userId: string, title?: string): Promise<Conversation> {
|
||||
const result = await this.db.insert(schema.conversations).values({
|
||||
userId,
|
||||
title: title || "New Chat",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date()
|
||||
}).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getConversations(userId: string): Promise<Conversation[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.conversations)
|
||||
.where(eq(schema.conversations.userId, userId))
|
||||
.orderBy(desc(schema.conversations.updatedAt));
|
||||
}
|
||||
|
||||
async getConversation(id: string): Promise<Conversation | undefined> {
|
||||
const result = await this.db.select().from(schema.conversations).where(eq(schema.conversations.id, id));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async deleteConversation(id: string): Promise<boolean> {
|
||||
await this.db.delete(schema.messages).where(eq(schema.messages.conversationId, id));
|
||||
const result = await this.db.delete(schema.conversations).where(eq(schema.conversations.id, id)).returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
async addMessage(message: InsertMessage): Promise<Message> {
|
||||
const result = await this.db.insert(schema.messages).values(message).returning();
|
||||
if (message.conversationId) {
|
||||
await this.db.update(schema.conversations)
|
||||
.set({ updatedAt: new Date() })
|
||||
.where(eq(schema.conversations.id, message.conversationId));
|
||||
}
|
||||
return result[0];
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Social & Leaderboard
|
||||
async getLeaderboard(): Promise<User[]> {
|
||||
return await this.db.select().from(schema.users)
|
||||
.where(and(eq(schema.users.isActive, true), eq(schema.users.showOnLeaderboard, true)))
|
||||
.orderBy(desc(schema.users.xp));
|
||||
}
|
||||
|
||||
async searchUsers(query: string): Promise<User[]> {
|
||||
if (!query || query.length < 2) return [];
|
||||
return await this.db.select()
|
||||
.from(schema.users)
|
||||
return await this.db.select().from(schema.users)
|
||||
.where(and(
|
||||
eq(schema.users.isSearchable, true),
|
||||
eq(schema.users.isActive, true),
|
||||
@@ -869,123 +1055,131 @@ export class DbStorage implements IStorage {
|
||||
}
|
||||
|
||||
async shareTask(sharedTask: InsertSharedTask): Promise<SharedTask> {
|
||||
return this.createSharedTask(sharedTask);
|
||||
}
|
||||
|
||||
async createSharedTask(sharedTask: InsertSharedTask): Promise<SharedTask> {
|
||||
const result = await this.db.insert(schema.sharedTasks).values(sharedTask).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async shareAllTasks(access: InsertUserTaskAccess): Promise<UserTaskAccess> {
|
||||
return this.createUserTaskAccess(access);
|
||||
}
|
||||
|
||||
async createUserTaskAccess(access: InsertUserTaskAccess): Promise<UserTaskAccess> {
|
||||
// Upsert or simple insert. Let's assume one Record per pair
|
||||
const result = await this.db.insert(schema.userTaskAccess).values(access).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async createSharedTask(sharedTask: InsertSharedTask): Promise<SharedTask> {
|
||||
return this.shareTask(sharedTask);
|
||||
}
|
||||
|
||||
async createUserTaskAccess(access: InsertUserTaskAccess): Promise<UserTaskAccess> {
|
||||
return this.shareAllTasks(access);
|
||||
}
|
||||
|
||||
async getSharedTasks(userId: string): Promise<SharedTask[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.sharedTasks)
|
||||
.where(eq(schema.sharedTasks.sharedWithUserId, userId));
|
||||
return await this.db.select().from(schema.sharedTasks).where(eq(schema.sharedTasks.sharedWithUserId, userId));
|
||||
}
|
||||
|
||||
async getUserTaskAccess(viewerId: string): Promise<UserTaskAccess[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.userTaskAccess)
|
||||
.where(eq(schema.userTaskAccess.viewerId, viewerId));
|
||||
return await this.db.select().from(schema.userTaskAccess).where(eq(schema.userTaskAccess.viewerId, viewerId));
|
||||
}
|
||||
|
||||
async getTaskSharedUsers(taskId: string): Promise<User[]> {
|
||||
const result = await this.db.select({
|
||||
id: schema.users.id,
|
||||
username: schema.users.username,
|
||||
email: schema.users.email,
|
||||
role: schema.users.role,
|
||||
isActive: schema.users.isActive,
|
||||
xp: schema.users.xp,
|
||||
level: schema.users.level,
|
||||
currentStreak: schema.users.currentStreak,
|
||||
lastTaskDate: schema.users.lastTaskDate,
|
||||
showOnLeaderboard: schema.users.showOnLeaderboard,
|
||||
isSearchable: schema.users.isSearchable,
|
||||
apiKey: schema.users.apiKey,
|
||||
aiEnabled: schema.users.aiEnabled,
|
||||
password: schema.users.password // Generally shouldn't return this, but following pattern
|
||||
})
|
||||
.from(schema.sharedTasks)
|
||||
.innerJoin(schema.users, eq(schema.sharedTasks.sharedWithUserId, schema.users.id))
|
||||
.where(eq(schema.sharedTasks.taskId, taskId));
|
||||
return result;
|
||||
const shares = await this.db.select().from(schema.sharedTasks).where(eq(schema.sharedTasks.taskId, taskId));
|
||||
if (shares.length === 0) return [];
|
||||
|
||||
return await this.db.select().from(schema.users)
|
||||
.where(sql`${schema.users.id} IN ${shares.map(s => s.sharedWithUserId)}`);
|
||||
}
|
||||
|
||||
async unshareTask(taskId: string, userId: string): Promise<boolean> {
|
||||
const result = await this.db.delete(schema.sharedTasks)
|
||||
.where(and(
|
||||
eq(schema.sharedTasks.taskId, taskId),
|
||||
eq(schema.sharedTasks.sharedWithUserId, userId)
|
||||
))
|
||||
.where(and(eq(schema.sharedTasks.taskId, taskId), eq(schema.sharedTasks.sharedWithUserId, userId)))
|
||||
.returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
// Shared Labels (DbStorage)
|
||||
async shareLabel(labelId: string, sharedWithUserId: string, sharedByUserId: string, permission: string = 'read'): Promise<SharedLabel> {
|
||||
const result = await this.db.insert(schema.sharedLabels).values({
|
||||
labelId,
|
||||
sharedWithUserId,
|
||||
sharedByUserId,
|
||||
permission
|
||||
permission,
|
||||
createdAt: new Date()
|
||||
}).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getSharedLabels(userId: string): Promise<SharedLabel[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.sharedLabels)
|
||||
.where(eq(schema.sharedLabels.sharedWithUserId, userId));
|
||||
return await this.db.select().from(schema.sharedLabels).where(eq(schema.sharedLabels.sharedWithUserId, userId));
|
||||
}
|
||||
|
||||
async getLabelSharedUsers(labelId: string): Promise<User[]> {
|
||||
const result = await this.db.select({
|
||||
id: schema.users.id,
|
||||
username: schema.users.username,
|
||||
email: schema.users.email,
|
||||
role: schema.users.role,
|
||||
isActive: schema.users.isActive,
|
||||
xp: schema.users.xp,
|
||||
level: schema.users.level,
|
||||
currentStreak: schema.users.currentStreak,
|
||||
lastTaskDate: schema.users.lastTaskDate,
|
||||
showOnLeaderboard: schema.users.showOnLeaderboard,
|
||||
isSearchable: schema.users.isSearchable,
|
||||
apiKey: schema.users.apiKey,
|
||||
aiEnabled: schema.users.aiEnabled,
|
||||
password: schema.users.password
|
||||
})
|
||||
.from(schema.sharedLabels)
|
||||
.innerJoin(schema.users, eq(schema.sharedLabels.sharedWithUserId, schema.users.id))
|
||||
.where(eq(schema.sharedLabels.labelId, labelId));
|
||||
return result;
|
||||
const shares = await this.db.select().from(schema.sharedLabels).where(eq(schema.sharedLabels.labelId, labelId));
|
||||
if (shares.length === 0) return [];
|
||||
return await this.db.select().from(schema.users)
|
||||
.where(sql`${schema.users.id} IN ${shares.map(s => s.sharedWithUserId)}`);
|
||||
}
|
||||
|
||||
async unshareLabel(labelId: string, userId: string): Promise<boolean> {
|
||||
const result = await this.db.delete(schema.sharedLabels)
|
||||
.where(and(
|
||||
eq(schema.sharedLabels.labelId, labelId),
|
||||
eq(schema.sharedLabels.sharedWithUserId, userId)
|
||||
))
|
||||
.where(and(eq(schema.sharedLabels.labelId, labelId), eq(schema.sharedLabels.sharedWithUserId, userId)))
|
||||
.returning();
|
||||
return result.length > 0;
|
||||
}
|
||||
|
||||
async getLabelShares(labelId: string): Promise<SharedLabel[]> {
|
||||
return await this.db.select().from(schema.sharedLabels).where(eq(schema.sharedLabels.labelId, labelId));
|
||||
}
|
||||
|
||||
async updateConversation(id: string, title: string): Promise<Conversation | undefined> {
|
||||
const result = await this.db.update(schema.conversations)
|
||||
.set({ title, updatedAt: new Date() })
|
||||
.where(eq(schema.conversations.id, id))
|
||||
.returning();
|
||||
return result[0];
|
||||
}
|
||||
async getMessages(conversationId: string): Promise<Message[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.sharedLabels)
|
||||
.where(eq(schema.sharedLabels.labelId, labelId));
|
||||
.from(schema.messages)
|
||||
.where(eq(schema.messages.conversationId, conversationId))
|
||||
.orderBy(asc(schema.messages.createdAt));
|
||||
}
|
||||
|
||||
async getMessage(id: string): Promise<Message | undefined> {
|
||||
const result = await this.db.select().from(schema.messages).where(eq(schema.messages.id, id));
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async updateMessage(id: string, content: string): Promise<Message> {
|
||||
const result = await this.db.update(schema.messages)
|
||||
.set({ content })
|
||||
.where(eq(schema.messages.id, id))
|
||||
.returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async deleteMessagesAfter(conversationId: string, after: Date, excludeMessageId?: string): Promise<void> {
|
||||
const filters = [
|
||||
eq(schema.messages.conversationId, conversationId),
|
||||
gt(schema.messages.createdAt, after)
|
||||
];
|
||||
|
||||
if (excludeMessageId) {
|
||||
filters.push(ne(schema.messages.id, excludeMessageId));
|
||||
}
|
||||
|
||||
await this.db.delete(schema.messages)
|
||||
.where(and(...filters));
|
||||
}
|
||||
|
||||
// Audit Logs (DbStorage)
|
||||
async createAuditLog(insertLog: InsertAuditLog): Promise<AuditLog> {
|
||||
const result = await this.db.insert(schema.auditLogs).values(insertLog).returning();
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async getAuditLogs(limit = 100): Promise<AuditLog[]> {
|
||||
return await this.db.select()
|
||||
.from(schema.auditLogs)
|
||||
.orderBy(desc(schema.auditLogs.createdAt))
|
||||
.limit(limit);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user