/* DeepStudy Pro MAX — Luxe UI * Same powerhouse features, premium UI polish for DeepSite. * Env: OPENAI_API_KEY, OPENAI_MODEL, JWT_SECRET, SMTP_*, APP_BASE_URL */ require('dotenv').config(); const path=require('path'),fs=require('fs'),os=require('os'); const express=require('express'),cors=require('cors'),bodyParser=require('body-parser'); const multer=require('multer'),pdfParse=require('pdf-parse'),mammoth=require('mammoth'); const {v4:uuidv4}=require('uuid'); const helmet=require('helmet'); const rateLimit=require('express-rate-limit'); const cookieParser=require('cookie-parser'); const nodemailer=require('nodemailer'); const bcrypt=require('bcryptjs'); const jwt=require('jsonwebtoken'); const OpenAI=require('openai'); const app=express(); app.use(helmet({contentSecurityPolicy:false})); app.use(cors({origin:true,credentials:true})); app.use(cookieParser()); app.use(bodyParser.json({limit:'16mb'})); app.use(rateLimit({windowMs:60_000,max:180})); const PORT=process.env.PORT||3000; const APP_BASE_URL=process.env.APP_BASE_URL||`http://localhost:${PORT}`; const JWT_SECRET=process.env.JWT_SECRET||'dev_change_me'; const DATA_DIR=path.join(process.cwd(),'data'); const USERS_PATH=path.join(DATA_DIR,'users.json'); const PACKS_PATH=path.join(DATA_DIR,'packs.json'); if(!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR); if(!fs.existsSync(USERS_PATH)) fs.writeFileSync(USERS_PATH,JSON.stringify([])); if(!fs.existsSync(PACKS_PATH)) fs.writeFileSync(PACKS_PATH,JSON.stringify([])); const readJSON=p=>{try{return JSON.parse(fs.readFileSync(p,'utf8'));}catch{return[];}}; const writeJSON=(p,v)=>fs.writeFileSync(p,JSON.stringify(v,null,2)); const readUsers=()=>readJSON(USERS_PATH), writeUsers=a=>writeJSON(USERS_PATH,a); const readPacks=()=>readJSON(PACKS_PATH), writePacks=a=>writeJSON(PACKS_PATH,a); let transporter=null; if(process.env.SMTP_HOST){ transporter=nodemailer.createTransport({host:process.env.SMTP_HOST,port:Number(process.env.SMTP_PORT||587),secure:false,auth:{user:process.env.SMTP_USER,pass:process.env.SMTP_PASS}}); } const openaiConfigured=!!process.env.OPENAI_API_KEY; const openaiClient=openaiConfigured? new OpenAI.OpenAI({apiKey:process.env.OPENAI_API_KEY}):null; const MODEL=process.env.OPENAI_MODEL||'gpt-4o-mini'; const sanitize=s=>(s||'').toString().replace(/\u0000/g,'').trim(); const safeParseJSON=s=>{try{return JSON.parse(s);}catch{return null;}}; const signToken=(payload,exp='7d')=>jwt.sign(payload,JWT_SECRET,{expiresIn:exp}); const verifyToken=t=>{try{return jwt.verify(t,JWT_SECRET);}catch{return null;}}; function authRequired(req,res,next){const t=req.cookies?.dsp_auth; if(!t) return res.status(401).json({ok:false,error:'נדרשת התחברות'}); const dec=verifyToken(t); if(!dec) return res.status(401).json({ok:false,error:'אסימון לא תקף'}); req.user=dec; next();} const upload=multer({dest:path.join(os.tmpdir(),'deepstudy_uploads'),limits:{fileSize:32*1024*1024}}); async function extractText(filePath,originalName){ const ext=(path.extname(originalName||'').toLowerCase()||'').slice(1); if(ext==='pdf'){const d=await pdfParse(fs.readFileSync(filePath)); return sanitize(d.text);} if(ext==='docx'){const {value}=await mammoth.extractRawText({path:filePath}); return sanitize(value);} if(ext==='txt'||ext===''){return sanitize(fs.readFileSync(filePath,'utf8'));} throw new Error(`פורמט לא נתמך: ${ext} (pdf/docx/txt)`); } function chunkText(str,targetTokens=1200,overlapTokens=120){ const txt=sanitize(str); const charsPerTok=3; const chunkChars=targetTokens*charsPerTok; const overlapChars=overlapTokens*charsPerTok; const out=[]; let i=0,idx=1; while(i
Browse files=txt.length) break;} return out; } async function llm(messages,system){ if(!openaiConfigured){const last=messages[messages.length-1]?.content||''; return `🔁 מצב דמו:\n${last.slice(0,600)}\n\n[הדגמה]`;} const r=await openaiClient.chat.completions.create({model:MODEL,temperature:0.35,messages:[{role:'system',content:system||'You are a kind Hebrew school tutor.'},...messages]}); return r.choices?.[0]?.message?.content?.trim()||''; } const mapPrompt=(goal,sid,text)=>[{role:'user',content: `מקטע "${sid}". מטרה: ${sanitize(goal)||'הבנה + הכנה לבוחן'}. """ ${text} """ צור תקציר (5–8 משפטים), נקודות מפתח, ומושגים. Markdown: ## ${sid} ### תקציר - ... ### נקודות מפתח - ... ### מושגים - מושג | הסבר קצר`}]; function reducePrompt(goal,mapped,merged){return[{role:'user',content: `אחד את סיכומי המקטעים ל"חבילת לימוד" בעברית עם [Si]. מטרה: ${sanitize(goal)||'הבנה + תרגול'}. סיכומים: """ ${mapped} """ (להקשר בלבד): """ ${merged.slice(0,20000)} """ הפק Markdown: # סיכום (8–12 משפטים, עם [Si]) # נקודות מפתח (בולטים + [Si]) # מילון מושגים (טבלה: מושג | הסבר קצר | מקטעים) # כרטיסיות (8–14): שאלה → תשובה קצרה # מבחן לדוגמה (10–14) בסוף הוסף JSON תקין: {"answerKey":[{"qid":"Q1","correct":"B","explanation":"..."}, ...]}`}];} async function buildStudyPack(goal,merged){ const chunks=chunkText(merged,1200,120); if(!chunks.length){return{studyPackMarkdown:'# סיכום\nדמו קצר.',answerKey:{answerKey:[]},sections:[]};} const mapped=[]; for(const ch of chunks) mapped.push(await llm(mapPrompt(goal,ch.id,ch.text))); const mappedMarkdown=mapped.join('\n\n'); const reduced=await llm(reducePrompt(goal,mappedMarkdown,merged)); let answerKey={answerKey:[]}; const m=reduced.match(/\{[\s\S]*\}/g)||[]; for(let i=m.length-1;i>=0;i--){const c=safeParseJSON(m[i]); if(c?.answerKey){answerKey=c;break;}} return{studyPackMarkdown:reduced,answerKey,sections:chunks.map(c=>c.id)}; } // ---------- AUTH ---------- app.post('/api/auth/signup',async(req,res)=>{ try{ const email=sanitize(req.body.email).toLowerCase(), pw=sanitize(req.body.password); if(!email||!pw||pw.length<6) return res.status(400).json({ok:false,error:'אימייל/סיסמה לא תקינים'}); const users=readUsers(); if(users.find(u=>u.email===email)) return res.status(409).json({ok:false,error:'אימייל קיים'}); const hash=await bcrypt.hash(pw,10); const user={id:uuidv4(),email,passwordHash:hash,createdAt:Date.now(),emailVerified:false}; users.push(user); writeUsers(users); if(transporter){ const t=signToken({action:'verify',uid:user.id},'2d'); const link=`${APP_BASE_URL}/verify?token=${t}`; await transporter.sendMail({from:process.env.SMTP_FROM||'DeepStudy Pro ',to:email,subject:'ברוכה הבאה! אשרי את המייל 🎉', html:`
ברוכה הבאה
לאימות החשבון:
לחצי כאן
`}); } const token=signToken({uid:user.id,email:user.email}); res.cookie('dsp_auth',token,{httpOnly:true,sameSite:'lax',maxAge:7*24*3600*1000}); res.json({ok:true,user:{id:user.id,email:user.email,emailVerified:user.emailVerified},info:transporter?'נשלח מייל אימות':'דמו: אין SMTP'}); }catch(e){console.error(e);res.status(500).json({ok:false,error:'שגיאה בהרשמה'});} }); app.post('/api/auth/login',async(req,res)=>{ try{ const email=sanitize(req.body.email).toLowerCase(), pw=sanitize(req.body.password); const users=readUsers(); const u=users.find(x=>x.email===email); if(!u) return res.status(401).json({ok:false,error:'שגוי'}); const ok=await bcrypt.compare(pw,u.passwordHash); if(!ok) return res.status(401).json({ok:false,error:'שגוי'}); const token=signToken({uid:u.id,email:u.email}); res.cookie('dsp_auth',token,{httpOnly:true,sameSite:'lax',maxAge:7*24*3600*1000}); res.json({ok:true,user:{id:u.id,email:u.email,emailVerified:u.emailVerified}}); }catch(e){console.error(e);res.status(500).json({ok:false,error:'שגיאה בהתחברות'});} }); app.post('/api/auth/logout',(req,res)=>{res.clearCookie('dsp_auth');res.json({ok:true});}); app.get('/api/auth/me',(req,res)=>{const t=req.cookies?.dsp_auth; const dec=t&&verifyToken(t); if(!dec) return res.json({ok:false}); const u=readUsers().find(x=>x.id===dec.uid); if(!u) return res.json({ok:false}); res.json({ok:true,user:{id:u.id,email:u.email,emailVerified:u.emailVerified}}); }); app.get('/api/auth/verify',(req,res)=>{const t=sanitize(req.query.token); const d=verifyToken(t); if(!d||d.action!=='verify') return res.status(400).json({ok:false,error:'טוקן לא תקף'}); const users=readUsers(); const u=users.find(x=>x.id===d.uid); if(!u) return res.status(400).json({ok:false,error:'לא קיים'}); if(!u.emailVerified){u.emailVerified=true; writeUsers(users);} res.json({ok:true,verified:true}); }); app.post('/api/auth/request-reset',async(req,res)=>{const email=sanitize(req.body.email).toLowerCase(); const u=readUsers().find(x=>x.email===email); if(u&&transporter){const t=signToken({action:'reset',uid:u.id},'2h'); const link=`${APP_BASE_URL}/reset?token=${t}`; await transporter.sendMail({from:process.env.SMTP_FROM||'DeepStudy Pro ',to:email,subject:'איפוס סיסמה',html:`
איפוס
קישור לאיפוס
`});} res.json({ok:true}); }); app.post('/api/auth/reset',async(req,res)=>{const {token,password}=req.body||{}; const d=verifyToken(sanitize(token)); if(!d||d.action!=='reset') return res.status(400).json({ok:false,error:'טוקן לא תקף'}); if(!password||String(password).length<6) return res.status(400).json({ok:false,error:'סיסמה קצרה'}); const users=readUsers(); const u=users.find(x=>x.id===d.uid); if(!u) return res.status(400).json({ok:false,error:'לא נמצא'}); u.passwordHash=await bcrypt.hash(String(password),10); writeUsers(users); res.json({ok:true}); }); // ---------- CORE ---------- app.get('/api/health',(_req,res)=>res.json({ok:true,model:openaiConfigured?MODEL:'DEMO',smtp:!!transporter})); app.post('/api/study-pack',authRequired,upload.array('files',6),async(req,res)=>{ const cleanup=()=> (req.files||[]).forEach(f=>fs.existsSync(f.path)&&fs.unlinkSync(f.path)); try{ const goal=sanitize(req.body.goal).slice(0,600); let merged=''; if(req.files?.length){for(const f of req.files){merged+=`\n\n===== ${f.originalname} =====\n${await extractText(f.path,f.originalname)}\n`;}} const raw=sanitize(req.body.text||''); if(raw) merged+=`\n\n===== Pasted Text =
- routes/auth.js +69 -0
- utils/helpers.js +36 -0
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const express = require('express');
|
| 2 |
+
const router = express.Router();
|
| 3 |
+
const { readUsers, writeUsers, signToken, verifyToken } = require('../utils/helpers');
|
| 4 |
+
const bcrypt = require('bcryptjs');
|
| 5 |
+
const { v4: uuidv4 } = require('uuid');
|
| 6 |
+
|
| 7 |
+
// Signup route
|
| 8 |
+
router.post('/signup', async (req, res) => {
|
| 9 |
+
try {
|
| 10 |
+
const email = sanitize(req.body.email).toLowerCase();
|
| 11 |
+
const pw = sanitize(req.body.password);
|
| 12 |
+
|
| 13 |
+
if (!email || !pw || pw.length < 6) {
|
| 14 |
+
return res.status(400).json({ ok: false, error: 'אימייל/סיסמה לא תקינים' });
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
const users = readUsers();
|
| 18 |
+
if (users.find(u => u.email === email)) {
|
| 19 |
+
return res.status(409).json({ ok: false, error: 'אימייל קיים' });
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const hash = await bcrypt.hash(pw, 10);
|
| 23 |
+
const user = {
|
| 24 |
+
id: uuidv4(),
|
| 25 |
+
email,
|
| 26 |
+
passwordHash: hash,
|
| 27 |
+
createdAt: Date.now(),
|
| 28 |
+
emailVerified: false
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
users.push(user);
|
| 32 |
+
writeUsers(users);
|
| 33 |
+
|
| 34 |
+
if (transporter) {
|
| 35 |
+
const t = signToken({ action: 'verify', uid: user.id }, '2d');
|
| 36 |
+
const link = `${APP_BASE_URL}/verify?token=${t}`;
|
| 37 |
+
await transporter.sendMail({
|
| 38 |
+
from: process.env.SMTP_FROM || 'DeepStudy Pro <noreply@example.com>',
|
| 39 |
+
to: email,
|
| 40 |
+
subject: 'ברוכה הבאה! אשרי את המייל 🎉',
|
| 41 |
+
html: `...email template...`
|
| 42 |
+
});
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
const token = signToken({ uid: user.id, email: user.email });
|
| 46 |
+
res.cookie('dsp_auth', token, {
|
| 47 |
+
httpOnly: true,
|
| 48 |
+
sameSite: 'lax',
|
| 49 |
+
maxAge: 7 * 24 * 3600 * 1000
|
| 50 |
+
});
|
| 51 |
+
|
| 52 |
+
res.json({
|
| 53 |
+
ok: true,
|
| 54 |
+
user: {
|
| 55 |
+
id: user.id,
|
| 56 |
+
email: user.email,
|
| 57 |
+
emailVerified: user.emailVerified
|
| 58 |
+
},
|
| 59 |
+
info: transporter ? 'נשלח מייל אימות' : 'דמו: אין SMTP'
|
| 60 |
+
});
|
| 61 |
+
} catch (e) {
|
| 62 |
+
console.error(e);
|
| 63 |
+
res.status(500).json({ ok: false, error: 'שגיאה בהרשמה' });
|
| 64 |
+
}
|
| 65 |
+
});
|
| 66 |
+
|
| 67 |
+
// ... other auth routes ...
|
| 68 |
+
|
| 69 |
+
module.exports = router;
|
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const fs = require('fs');
|
| 2 |
+
const path = require('path');
|
| 3 |
+
|
| 4 |
+
// Data storage helper functions
|
| 5 |
+
const readJSON = (p) => {
|
| 6 |
+
try {
|
| 7 |
+
return JSON.parse(fs.readFileSync(p, 'utf8'));
|
| 8 |
+
} catch {
|
| 9 |
+
return [];
|
| 10 |
+
}
|
| 11 |
+
};
|
| 12 |
+
|
| 13 |
+
const writeJSON = (p, v) => fs.writeFileSync(p, JSON.stringify(v, null, 2));
|
| 14 |
+
|
| 15 |
+
// Text sanitization
|
| 16 |
+
const sanitize = (s) => (s || '').toString().replace(/\u0000/g, '').trim();
|
| 17 |
+
|
| 18 |
+
// JSON parsing with safety
|
| 19 |
+
const safeParseJSON = (s) => {
|
| 20 |
+
try {
|
| 21 |
+
return JSON.parse(s);
|
| 22 |
+
} catch {
|
| 23 |
+
return null;
|
| 24 |
+
}
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
module.exports = {
|
| 28 |
+
readJSON,
|
| 29 |
+
writeJSON,
|
| 30 |
+
sanitize,
|
| 31 |
+
safeParseJSON,
|
| 32 |
+
readUsers: () => readJSON(path.join(process.cwd(), 'data', 'users.json')),
|
| 33 |
+
writeUsers: (a) => writeJSON(path.join(process.cwd(), 'data', 'users.json'), a),
|
| 34 |
+
readPacks: () => readJSON(path.join(process.cwd(), 'data', 'packs.json')),
|
| 35 |
+
writePacks: (a) => writeJSON(path.join(process.cwd(), 'data', 'packs.json'), a)
|
| 36 |
+
};
|