feat: Implement AI Chat Agent, Email Notifications, and UI enhancements
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-12 08:35:48 +01:00
parent ccfb674318
commit 5d8976b1cd
58 changed files with 7867 additions and 844 deletions
+25
View File
@@ -0,0 +1,25 @@
import { storage } from "../server/storage";
async function run() {
try {
const user = await storage.getUserByUsername("admin");
if (user) {
console.log("User found:", {
id: user.id,
username: user.username,
role: user.role,
aiEnabled: user.aiEnabled,
// Check if property exists
hasAiEnabled: "aiEnabled" in user
});
} else {
console.log("User 'admin' not found");
}
process.exit(0);
} catch (e) {
console.error(e);
process.exit(1);
}
}
run();
+69
View File
@@ -0,0 +1,69 @@
import { storage } from "../server/storage";
import { scrypt, randomBytes } from "crypto";
import { promisify } from "util";
const scryptAsync = promisify(scrypt);
async function hashPassword(password: string) {
const salt = randomBytes(16).toString("hex");
const buf = (await scryptAsync(password, salt, 64)) as Buffer;
return `${buf.toString("hex")}.${salt}`;
}
async function run() {
try {
console.log("🔍 Checking for 'admin' user...");
let user = await storage.getUserByUsername("admin");
const password = "admin";
console.log(`🔐 Hashing password '${password}'...`);
const hashedPassword = await hashPassword(password);
if (user) {
console.log(`✅ User 'admin' found (ID: ${user.id}). Updating credentials...`);
await storage.updateUser(user.id, {
password: hashedPassword,
role: 'admin',
isActive: true
});
console.log("✅ Admin user updated successfully.");
} else {
console.log("⚠️ User 'admin' not found. Checking by email 'admin@example.com'...");
const emailUser = await storage.getUserByEmail("admin@example.com");
if (emailUser) {
console.log(`✅ User found by email (ID: ${emailUser.id}). Updating to username 'admin'...`);
await storage.updateUser(emailUser.id, {
username: "admin",
password: hashedPassword,
role: "admin",
isActive: true
});
console.log("✅ Admin user updated/renamed successfully.");
} else {
console.log("🆕 Creating new 'admin' user...");
await storage.createUser({
username: "admin",
email: "admin@example.com",
password: hashedPassword,
role: "admin",
isActive: true,
showOnLeaderboard: false,
isSearchable: false,
// aiEnabled defaults to true in schema/storage if omitted in Insert,
// but depending on implementation might need it.
// Storage implementation handles defaults if not provided?
// Let's pass defaults we know are safe.
} as any); // cast as any to avoid strict InsertUser type mismatches if interfaces drift
console.log("✅ Admin user created successfully.");
}
}
process.exit(0);
} catch (e) {
console.error("❌ Failed to create/update admin user:", e);
process.exit(1);
}
}
run();
+151
View File
@@ -0,0 +1,151 @@
const BASE_URL = "http://localhost:5001";
const MAILHOG_API = "http://localhost:8025/api/v2";
let cookie = "";
async function request(method: string, path: string, body?: any) {
const headers: any = { "Content-Type": "application/json" };
if (cookie) headers["Cookie"] = cookie;
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const setCookie = res.headers.get("set-cookie");
if (setCookie) {
cookie = setCookie.split(";")[0];
}
const text = await res.text();
try {
const data = JSON.parse(text);
return { status: res.status, data };
} catch {
return { status: res.status, data: text };
}
}
async function getLatestEmail(toEmail: string) {
try {
const res = await fetch(`${MAILHOG_API}/messages`);
const data = await res.json();
// MailHog returns { total: number, count: number, start: number, items: [...] }
// items are sorted newest first usually in MailHog UI, but API might vary.
// Let's filter by 'To' and take the first one.
const messages = data.items;
for (const msg of messages) {
// Headers is an object like { "To": ["<email>"], ... }
// Content.Headers.To
const toHeader = msg.Content.Headers.To?.[0];
if (toHeader && toHeader.includes(toEmail)) {
return msg;
}
}
return null;
} catch (e) {
console.error("Failed to fetch from MailHog:", e);
return null;
}
}
async function run() {
console.log("📧 Testing Email Flow...");
const timestamp = Date.now();
const username = `email_user_${timestamp}`;
const email = `test_${timestamp}@example.com`;
const password = "password123";
const newPassword = "newpassword456";
// 1. Register User
console.log(`\n1. Registering user: ${username} (${email})`);
let res = await request("POST", "/api/register", { username, password, email });
if (res.status === 201 || res.status === 200) {
console.log("✅ Registration successful");
} else {
console.error("❌ Registration failed:", res.data);
process.exit(1);
}
// 2. Request Password Reset
console.log("\n2. Requesting Password Reset...");
// Logout first just in case
await request("POST", "/api/logout");
cookie = ""; // Clear cookie
res = await request("POST", "/api/auth/forgot-password", { email });
if (res.status === 200) {
console.log("✅ Reset request sent:", res.data.message);
} else {
console.error("❌ Reset request failed:", res.data);
process.exit(1);
}
// 3. Check MailHog
console.log("\n3. Checking MailHog for email...");
// Wait a bit for email to arrive
await new Promise(r => setTimeout(r, 2000));
const emailMsg = await getLatestEmail(email);
if (emailMsg) {
console.log("✅ Email found!", emailMsg.Content.Headers.Subject[0]);
} else {
console.error("❌ Email NOT found in MailHog!");
process.exit(1);
}
// 4. Extract Token
// We expect a link like: http://localhost:5001/reset-password?token=...
// In text body: msg.Content.Body
let body = emailMsg.Content.Body;
// Simple QP decoding for test
body = body.replace(/=\r\n/g, '').replace(/=\n/g, '').replace(/=3D/g, '=');
console.log("DEBUG BODY DECODED:", body);
const match = body.match(/token=([a-zA-Z0-9-]+)/);
if (!match) {
console.error("❌ Token not found in email body!");
console.log("Body:", body);
process.exit(1);
}
const token = match[1];
console.log("✅ Token extracted:", token);
// 5. Reset Password
console.log("\n5. Resetting Password...");
res = await request("POST", "/api/auth/reset-password", { token, newPassword });
if (res.status === 200) {
console.log("✅ Password reset successful");
} else {
console.error("❌ Password reset failed:", res.data);
process.exit(1);
}
// 6. Login with New Password
console.log("\n6. Logging in with NEW password...");
res = await request("POST", "/api/login", { username, password: newPassword });
if (res.status === 200) {
console.log("✅ Login successful with new password!");
} else {
console.error("❌ Login failed:", res.data);
process.exit(1);
}
// 7. Login with OLD Password (should fail)
console.log("\n7. Verifying OLD password fails...");
res = await request("POST", "/api/login", { username, password });
if (res.status === 401) {
console.log("✅ Old password rejected correctly.");
} else {
console.error("❌ Old password SHOULD fail but got:", res.status);
}
console.log("\n🎉 Full Email Flow Test Passed!");
}
run();
+221
View File
@@ -0,0 +1,221 @@
import { scrypt, randomBytes } from "crypto";
import { promisify } from "util";
const BASE_URL = "http://localhost:5001";
let cookie = "";
async function request(method: string, path: string, body?: any) {
const headers: any = { "Content-Type": "application/json" };
if (cookie) headers["Cookie"] = cookie;
const res = await fetch(`${BASE_URL}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
// Capture cookie
const setCookie = res.headers.get("set-cookie");
if (setCookie) {
cookie = setCookie.split(";")[0];
}
const text = await res.text();
// console.log("RAW RESPONSE:", text); // Uncomment if needed
try {
const data = JSON.parse(text);
if (res.status >= 400 && data.error === "Failed to update task") {
console.log("DEBUG ERROR RESPONSE:", text);
}
return { status: res.status, data };
} catch {
return { status: res.status, data: text };
}
}
async function requestWithRetry(method: string, path: string, body?: any, retries = 5, delay = 2000) {
for (let i = 0; i < retries; i++) {
try {
return await request(method, path, body);
} catch (err: any) {
if (i === retries - 1) throw err;
if (err.cause && (err.cause.code === 'ECONNRESET' || err.cause.code === 'ECONNREFUSED')) {
console.log(`Connection failed (${err.cause.code}). Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
} else {
throw err;
}
}
}
throw new Error("Request failed after retries");
}
async function run() {
console.log("🚀 Starting Gamification E2E Test");
// 1. Enable Registration (Just in case)
// We can't easily do this via API without admin.
// Let's assume registration is open or we use the Setup flow if needed.
// Actually, let's try to register. If 403, we try to login as admin?
const username = `test_gamer_${Date.now()}`;
const password = "password123";
const email = `${username}@example.com`;
console.log(`\n👤 Registering User: ${username}`);
let res = await requestWithRetry("POST", "/api/register", {
username,
password,
email,
showOnLeaderboard: true
});
if (res.status === 403) {
console.log("Registration disabled. Trying Setup...");
// Try setup? Or maybe just login as admin?
// Let's assume we can create a user via raw storage import if API fails,
// but mixing contexts is messy.
console.error("❌ Registration disabled and no admin handling in script. Aborting.");
process.exit(1);
}
if (res.status !== 200 && res.status !== 201) {
// Maybe already logged in or error
console.error("❌ Registration failed:", res.data);
// Try login
res = await requestWithRetry("POST", "/api/login", { username, password });
if (res.status !== 200) {
console.error("❌ Login failed:", res.data);
process.exit(1);
}
}
const userId = res.data.id;
console.log("✅ User Logged In. ID:", userId);
// 2. Create a Task & Complete it for XP
console.log("\n📝 Creating Task...");
res = await request("POST", "/api/tasks", { title: "XP Grind Task", dueDate: null });
if (res.status !== 201) {
console.error("❌ Task Creation Failed:", res.status, JSON.stringify(res.data, null, 2));
}
const taskId = res.data?.id;
console.log("Task Created:", taskId);
if (taskId) {
console.log("✅ Completing Task...");
res = await request("PATCH", `/api/tasks/${taskId}`, { status: "done" });
if (res.status !== 200) {
console.error("❌ Task Completion Failed:", res.status, JSON.stringify(res.data, null, 2));
}
console.log("Task Update Result:", res.data.status);
}
// 3. Verify XP (via Leaderboard or Profile?)
// We don't have a direct 'get me' endpoint that shows XP easily,
// but /api/user/history should show the event!
console.log("\n📜 Checking History for XP Gain...");
res = await request("GET", "/api/user/history");
if (res.status !== 200) {
console.error("❌ History Fetch Failed:", res.status, JSON.stringify(res.data, null, 2));
}
const history = Array.isArray(res.data) ? res.data : [];
console.log("History Events:", history.length);
const taskEvent = history.find((e: any) => e.source === 'task_completion');
if (taskEvent) {
console.log(`✅ Found Task Completion Event: +${taskEvent.amount} XP`);
} else {
console.error("❌ No Task Completion Event found!");
}
const clearEvent = history.find((e: any) => e.source === 'daily_clear_bonus');
if (clearEvent) {
console.log(`✅ Found Daily Clear Bonus: +${clearEvent.amount} XP`);
} else {
console.log("️ No Daily Clear Bonus (Normal if other tasks exist)");
}
// 4. Create a Reward (System allows user creation for now?)
console.log("\n🎁 Creating Custom Reward...");
res = await request("POST", "/api/rewards", {
title: "Test Reward",
description: "E2E Test Reward",
cost: 10,
type: "virtual",
icon: "gift"
});
const rewardId = res.data.id;
console.log("Reward Created:", rewardId);
// 5. Verify Leaderboard
console.log("\n🏆 Verifying Leaderboard...");
res = await request("GET", "/api/leaderboard");
const leaderboard = Array.isArray(res.data) ? res.data : [];
const me = leaderboard.find((u: any) => u.username === username);
if (me) {
if (me.xp > 0) {
console.log(`✅ Leaderboard verified: User ${username} has ${me.xp} XP`);
} else {
console.error(`❌ Leaderboard Error: User has ${me.xp} XP (expected > 0)`);
}
} else {
console.error("❌ Leaderboard Error: User not found in leaderboard");
// Check privacy settings? Default is showOnLeaderboard=false
// Oops, default is false in schema? Let's check schema/storage.
}
// 6. Purchase Reward
console.log("\n💰 Purchasing Reward...");
res = await request("POST", "/api/rewards/purchase", { rewardId: rewardId });
if (res.status === 200) {
console.log("✅ Purchase Successful");
} else {
console.error("❌ Purchase Failed:", res.data);
}
// 6. Verify Inventory
console.log("\n🎒 Checking Inventory...");
res = await request("GET", "/api/user/inventory");
const inventory = Array.isArray(res.data) ? res.data : [];
const item = inventory.find((i: any) => i.rewardId === rewardId);
if (item) {
console.log("✅ Reward found in Inventory!");
console.log("Inventory Item:", item);
} else {
console.error("❌ Reward NOT found in Inventory.");
console.log("Full Inventory Response:", JSON.stringify(inventory, null, 2));
}
// 7. Verify Privacy: User 2 should NOT see User 1's custom reward
console.log("\n🕵️ Checking Privacy...");
const user2 = `test_gamer_2_${Date.now()}`;
await request("POST", "/api/logout"); // Logout User 1
console.log(`Registering User 2: ${user2}`);
res = await requestWithRetry("POST", "/api/register", { username: user2, password: "password123", email: `${user2}@example.com` });
if (res.status === 201) {
console.log("User 2 Logged In");
res = await request("GET", "/api/rewards");
const rewards = Array.isArray(res.data) ? res.data : [];
const privateReward = rewards.find((r: any) => r.id === rewardId);
if (privateReward) {
console.error("❌ PRIVACY FAIL: User 2 can see User 1's custom reward!");
console.error("Reward:", privateReward);
} else {
console.log("✅ PRIVACY SUCCESS: User 2 cannot see private reward.");
}
} else {
console.warn("⚠️ Could not register User 2, skipping privacy check.");
}
console.log("\n✅ Test Complete.");
}
run().catch(console.error);
+46
View File
@@ -0,0 +1,46 @@
import { storage } from "../server/storage";
import { insertUserSchema } from "../shared/schema";
async function runTest() {
console.log("Starting Gamification Logic Test...");
// 1. Setup Test User
const timestamp = Date.now();
const username = `gamer_${timestamp}`;
const password = "password123";
const email = `gamer_${timestamp}@example.com`;
console.log(`Creating user: ${username}`);
const user = await storage.createUser({
username,
email,
password, // Note: In real app this wants hashed, but we are bypassing auth middleware for direct storage tests?
// API tests need real auth.
// Let's rely on Direct Storage + Logic Verification since pure API test is complex with auth cookies in a simple script.
role: 'user',
isActive: true
});
// We need to simulate the API logic because the logic resides in the Route handler (routes.ts), not just storage.
// This is tricky without a full HTTP client.
// ALTERNATIVE: We can define a helper to mock Request/Response and call the route handler?
// Too complex.
// Let's use fetch against the running server.
const baseUrl = "http://localhost:5001";
// Login to get cookie
console.log("Logging in via API...");
const loginRes = await fetch(`${baseUrl}/api/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
// Wait, storage.createUser doesn't hash password! API login will fail if I stored plain text?
// Yes. Routes.ts: setupAuth uses comparePassword(password, user.password).
// So I must hash the password if I insert via storage.
// OR I can register via API.
}