from pathlib import Path
import zipfile, textwrap, os, json
root=Path("/mnt/data/qazaq-cargo")
(root/"public").mkdir(parents=True,exist_ok=True)
(root/"src").mkdir(exist_ok=True)
(root/"package.json").write_text("""{
"name":"qazaq-cargo",
"version":"1.0.0",
"private":true,
"scripts":{"start":"node server.js","dev":"node --watch server.js"},
"dependencies":{
"dotenv":"^16.4.7",
"express":"^5.1.0",
"firebase-admin":"^13.5.0",
"express-rate-limit":"^8.0.1"
}
}""",encoding="utf-8")
(root/".env.example").write_text("""# SERVER
PORT=3000
NODE_ENV=development
# WhatsApp Cloud API
WHATSAPP_ACCESS_TOKEN=PASTE_META_ACCESS_TOKEN_HERE
WHATSAPP_PHONE_NUMBER_ID=PASTE_PHONE_NUMBER_ID_HERE
WHATSAPP_API_VERSION=v23.0
WHATSAPP_SENDER_NUMBER=+77783727777
# OTP
OTP_TTL_SECONDS=300
OTP_COOLDOWN_SECONDS=60
# Firebase Admin
# Recommended: put the JSON service account file outside public/
GOOGLE_APPLICATION_CREDENTIALS=./serviceAccountKey.json
FIREBASE_PROJECT_ID=PASTE_FIREBASE_PROJECT_ID_HERE
# First admin phone, digits only, e.g. 77783727777
ADMIN_PHONE=77783727777
""",encoding="utf-8")
(root/"serviceAccountKey.json.example").write_text("""{
"type": "service_account",
"project_id": "YOUR_PROJECT_ID",
"private_key_id": "YOUR_PRIVATE_KEY_ID",
"private_key": "-----BEGIN PRIVATE KEY-----\\\\nPASTE_PRIVATE_KEY\\\\n-----END PRIVATE KEY-----\\\\n",
"client_email": "firebase-adminsdk-xxxxx@YOUR_PROJECT_ID.iam.gserviceaccount.com",
"client_id": "YOUR_CLIENT_ID",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "PASTE_CLIENT_CERT_URL"
}""",encoding="utf-8")
(root/"server.js").write_text(r"""require("dotenv").config();
const path=require("path");
const crypto=require("crypto");
const express=require("express");
const rateLimit=require("express-rate-limit");
const {initializeApp,cert,getApps}=require("firebase-admin/app");
const {getFirestore,FieldValue}=require("firebase-admin/firestore");
const app=express();
const PORT=process.env.PORT||3000;
app.use(express.json({limit:"100kb"}));
app.use(express.static(path.join(__dirname,"public")));
function initFirebase(){
if(getApps().length)return getFirestore();
const opts={projectId:process.env.FIREBASE_PROJECT_ID};
if(process.env.GOOGLE_APPLICATION_CREDENTIALS){
const fs=require("fs");
const f=path.resolve(process.env.GOOGLE_APPLICATION_CREDENTIALS);
if(fs.existsSync(f)) opts.credential=cert(require(f));
}
initializeApp(opts);
return getFirestore();
}
let db;
try{db=initFirebase()}catch(e){console.error("Firebase init error:",e.message)}
const otpStore=new Map();
const sessions=new Map();
const sendLimiter=rateLimit({windowMs:15*60*1000,max:5,standardHeaders:true,legacyHeaders:false});
const verifyLimiter=rateLimit({windowMs:15*60*1000,max:15,standardHeaders:true,legacyHeaders:false});
function digits(phone){return String(phone||"").replace(/\D/g,"").replace(/^8/,"7")}
function validPhone(p){return /^7\d{10}$/.test(p)}
function token(){return crypto.randomBytes(32).toString("hex")}
function requireDb(res){if(!db){res.status(503).json({error:"Firebase not configured"});return false}return true}
function auth(req,res,next){
const t=(req.headers.authorization||"").replace(/^Bearer\s+/,"");
const s=sessions.get(t);
if(!s)return res.status(401).json({error:"Unauthorized"});
req.user=s;req.sessionToken=t;next();
}
function admin(req,res,next){if(req.user.role!=="admin"&&req.user.role!=="moderator")return res.status(403).json({error:"Admin only"});next()}
async function sendWhatsAppText(to,text){
const url=`https://graph.facebook.com/${process.env.WHATSAPP_API_VERSION||"v23.0"}/${process.env.WHATSAPP_PHONE_NUMBER_ID}/messages`;
const r=await fetch(url,{method:"POST",headers:{
"Authorization":`Bearer ${process.env.WHATSAPP_ACCESS_TOKEN}`,
"Content-Type":"application/json"
},body:JSON.stringify({messaging_product:"whatsapp",to,type:"text",text:{preview_url:false,body:text}})});
const data=await r.json();
if(!r.ok)throw new Error(data?.error?.message||"WhatsApp API error");
return data;
}
app.get("/api/health",(req,res)=>res.json({
ok:true,
firebase:!!db,
whatsappConfigured:!!(process.env.WHATSAPP_ACCESS_TOKEN&&process.env.WHATSAPP_PHONE_NUMBER_ID),
sender:process.env.WHATSAPP_SENDER_NUMBER||null
}));
app.post("/api/auth/send-otp",sendLimiter,async(req,res)=>{
const phone=digits(req.body.phone);
if(!validPhone(phone))return res.status(400).json({error:"Введите корректный номер Казахстана, например +7 778 372 77 77"});
const old=otpStore.get(phone);
if(old&&Date.now()-old.created
{
if(!requireDb(res))return;
const phone=digits(req.body.phone),code=String(req.body.code||"").trim(),x=otpStore.get(phone);
if(!validPhone(phone)||!x)return res.status(400).json({error:"Код не найден или истёк"});
if(Date.now()-x.created>Number(process.env.OTP_TTL_SECONDS||300)*1000)return res.status(400).json({error:"Код истёк"});
if(x.attempts>=5)return res.status(429).json({error:"Слишком много попыток"});
x.attempts++;
const hash=crypto.createHash("sha256").update(code).digest("hex");
if(hash!==x.hash)return res.status(400).json({error:"Неверный код"});
otpStore.delete(phone);
const ref=db.collection("users").doc(phone);
const snap=await ref.get();
let user=snap.exists?snap.data():{
id:phone,phone,name:"",company:"",iin:"",
role:phone===digits(process.env.ADMIN_PHONE)?"admin":"user",
createdAt:FieldValue.serverTimestamp()
};
await ref.set({...user,lastLoginAt:FieldValue.serverTimestamp()},{merge:true});
const t=token();sessions.set(t,{id:phone,phone,role:user.role,name:user.name,company:user.company||""});
res.json({ok:true,token:t,user:{id:phone,phone,role:user.role,name:user.name,company:user.company||""},needsProfile:!user.name});
});
app.put("/api/me",auth,async(req,res)=>{
if(!requireDb(res))return;
const data={name:String(req.body.name||"").trim(),company:String(req.body.company||"").trim(),iin:String(req.body.iin||"").trim()};
if(!data.name)return res.status(400).json({error:"Имя обязательно"});
await db.collection("users").doc(req.user.id).set(data,{merge:true});
Object.assign(req.user,data);
res.json({ok:true,user:req.user});
});
app.post("/api/transports",auth,async(req,res)=>{
if(!requireDb(res))return;
const b=req.body;
if(!b.from||!b.to||!b.date||!b.body||!b.weight)return res.status(400).json({error:"Заполните обязательные поля"});
const ref=await db.collection("transports").add({
from:String(b.from),to:String(b.to),date:String(b.date),body:String(b.body),
weight:Number(b.weight),volume:Number(b.volume||0),number:String(b.number||""),
price:Number(b.price||0),currency:String(b.currency||"тенге"),
phone:String(b.phone||req.user.phone),gps:String(b.gps||"Жоқ"),
description:String(b.description||""),userId:req.user.id,userName:req.user.name||"",
status:"pending",createdAt:FieldValue.serverTimestamp()
});
res.json({ok:true,id:ref.id});
});
app.get("/api/transports",async(req,res)=>{
if(!requireDb(res))return;
let q=db.collection("transports");
if(req.query.status)q=q.where("status","==",String(req.query.status));
const snap=await q.limit(200).get();
const arr=snap.docs.map(d=>({id:d.id,...d.data()}));
res.json({ok:true,items:arr});
});
app.get("/api/my-transports",auth,async(req,res)=>{
if(!requireDb(res))return;
const snap=await db.collection("transports").where("userId","==",req.user.id).limit(100).get();
res.json({ok:true,items:snap.docs.map(d=>({id:d.id,...d.data()}))});
});
app.patch("/api/admin/transports/:id",auth,admin,async(req,res)=>{
if(!requireDb(res))return;
const status=String(req.body.status||"");
if(!["approved","rejected","pending"].includes(status))return res.status(400).json({error:"Invalid status"});
await db.collection("transports").doc(req.params.id).update({status,moderatedBy:req.user.id,moderatedAt:FieldValue.serverTimestamp()});
res.json({ok:true});
});
app.get("/api/admin/users",auth,admin,async(req,res)=>{
if(!requireDb(res))return;
const snap=await db.collection("users").limit(500).get();
res.json({ok:true,items:snap.docs.map(d=>({id:d.id,...d.data()}))});
});
app.post("/api/logout",auth,(req,res)=>{sessions.delete(req.sessionToken);res.json({ok:true})});
app.get("*",(req,res)=>res.sendFile(path.join(__dirname,"public","index.html")));
app.listen(PORT,()=>console.log(`QAZAQ CARGO http://localhost:${PORT}`));
""",encoding="utf-8")
# Simple frontend
(root/"public/index.html").write_text(r"""
QAZAQ CARGO
QAZAQ CARGO
Қонақ
🚛 QAZAQ CARGO
Жүк және транспорт тасымалдаушыларына арналған платформа.
💬 WhatsApp арқылы тіркелу
ЭЦҚ/NCALayer жоқ. OTP коды тікелей WhatsApp-қа жіберіледі.
📱 WhatsApp OTP
➕ Транспорт қосу
Алдымен WhatsApp арқылы кіріңіз.
| Маршрут | Дата | Кузов | Автор | Статус | Әрекет |
|---|
""",encoding="utf-8")
(root/"README.md").write_text("""# QAZAQ CARGO — WhatsApp OTP + Firebase/Firestore
## 1. Орнату
```bash
npm install
copy .env.example .env