diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..cd42198 Binary files /dev/null and b/backend/app/__pycache__/main.cpython-313.pyc differ diff --git a/backend/app/api/__pycache__/auth.cpython-313.pyc b/backend/app/api/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000..df4a7b4 Binary files /dev/null and b/backend/app/api/__pycache__/auth.cpython-313.pyc differ diff --git a/backend/app/api/__pycache__/scanner.cpython-313.pyc b/backend/app/api/__pycache__/scanner.cpython-313.pyc new file mode 100644 index 0000000..75629f8 Binary files /dev/null and b/backend/app/api/__pycache__/scanner.cpython-313.pyc differ diff --git a/backend/app/api/auth.py b/backend/app/api/auth.py new file mode 100644 index 0000000..b94b240 --- /dev/null +++ b/backend/app/api/auth.py @@ -0,0 +1,106 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.orm import Session +from pydantic import BaseModel +from jose import JWTError, jwt + +from app.db.database import get_db +from app.db.models import User +from app.core.security import ( + public_key, decrypt_password, verify_password, + get_password_hash, create_access_token +) +from app.core.config import SECRET_KEY, ALGORITHM + +router = APIRouter() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/auth/login") + +class AuthRequest(BaseModel): + username: str + password: str # Base64 encoded RSA-encrypted password + +class ProfileUpdateRequest(BaseModel): + display_name: str = None + email_id: str = None + mobile_no: str = None + gender: str = None + password: str = None # Base64 encoded RSA-encrypted new password + +@router.get("/public-key") +def get_public_key(): + return {"public_key": public_key.decode("utf-8")} + +@router.post("/signup") +def signup(req: AuthRequest, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.username == req.username).first() + if db_user: + raise HTTPException(status_code=400, detail="Username already registered") + + # Decrypt password + decrypted_password = decrypt_password(req.password) + hashed_password = get_password_hash(decrypted_password) + + new_user = User(username=req.username, hashed_password=hashed_password) + db.add(new_user) + db.commit() + return {"message": "User created successfully"} + +@router.post("/login") +def login(req: AuthRequest, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.username == req.username).first() + if not db_user: + raise HTTPException(status_code=400, detail="Invalid credentials") + + decrypted_password = decrypt_password(req.password) + if not verify_password(decrypted_password, db_user.hashed_password): + raise HTTPException(status_code=400, detail="Invalid credentials") + + access_token = create_access_token(data={"sub": db_user.username}) + return {"access_token": access_token, "token_type": "bearer"} + +def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + username: str = payload.get("sub") + if username is None: + raise credentials_exception + except JWTError: + raise credentials_exception + + user = db.query(User).filter(User.username == username).first() + if user is None: + raise credentials_exception + return user + +@router.get("/me") +def get_me(current_user: User = Depends(get_current_user)): + return { + "username": current_user.username, + "display_name": current_user.display_name, + "email_id": current_user.email_id, + "mobile_no": current_user.mobile_no, + "gender": current_user.gender + } + +@router.put("/update-profile") +def update_profile(req: ProfileUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + if req.display_name is not None: + current_user.display_name = req.display_name + if req.email_id is not None: + current_user.email_id = req.email_id + if req.mobile_no is not None: + current_user.mobile_no = req.mobile_no + if req.gender is not None: + current_user.gender = req.gender + if req.password: + decrypted_password = decrypt_password(req.password) + hashed_password = get_password_hash(decrypted_password) + current_user.hashed_password = hashed_password + + db.commit() + return {"message": "Profile updated successfully"} diff --git a/backend/app/api/scanner.py b/backend/app/api/scanner.py new file mode 100644 index 0000000..6283430 --- /dev/null +++ b/backend/app/api/scanner.py @@ -0,0 +1,27 @@ +from fastapi import APIRouter, Depends +from app.api.auth import get_current_user +from app.services.stock_engine import ( + get_scan, get_ribbon, get_smartmoney, get_trend, get_marketintel +) + +router = APIRouter() + +@router.get("/scan") +def scan(current_user = Depends(get_current_user)): + return get_scan() + +@router.get("/ribbon") +def ribbon(current_user = Depends(get_current_user)): + return get_ribbon() + +@router.get("/smartmoney") +def smartmoney(current_user = Depends(get_current_user)): + return get_smartmoney() + +@router.get("/trend") +def trend(current_user = Depends(get_current_user)): + return get_trend() + +@router.get("/marketintel") +def marketintel(current_user = Depends(get_current_user)): + return get_marketintel() diff --git a/backend/app/core/__pycache__/config.cpython-313.pyc b/backend/app/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000..cab9925 Binary files /dev/null and b/backend/app/core/__pycache__/config.cpython-313.pyc differ diff --git a/backend/app/core/__pycache__/security.cpython-313.pyc b/backend/app/core/__pycache__/security.cpython-313.pyc new file mode 100644 index 0000000..12987d2 Binary files /dev/null and b/backend/app/core/__pycache__/security.cpython-313.pyc differ diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..3aa511d --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,6 @@ +import os + +DATABASE_URL = "postgresql://postgres:M%40triXPostgr3s%406202@103.125.129.116:5333/stock_scanner" +SECRET_KEY = "stock-scanner-super-secret-key-12345" +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 60 diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..1bfceb6 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,61 @@ +import os +import base64 +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +import bcrypt +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_OAEP +from fastapi import HTTPException + +from app.core.config import SECRET_KEY, ALGORITHM + +RSA_PRIVATE_KEY_PATH = "private.pem" +RSA_PUBLIC_KEY_PATH = "public.pem" + +if not os.path.exists(RSA_PRIVATE_KEY_PATH) or not os.path.exists(RSA_PUBLIC_KEY_PATH): + key = RSA.generate(2048) + private_key = key.export_key() + with open(RSA_PRIVATE_KEY_PATH, "wb") as file_out: + file_out.write(private_key) + + public_key = key.publickey().export_key() + with open(RSA_PUBLIC_KEY_PATH, "wb") as file_out: + file_out.write(public_key) +else: + with open(RSA_PRIVATE_KEY_PATH, "rb") as f: + private_key = f.read() + with open(RSA_PUBLIC_KEY_PATH, "rb") as f: + public_key = f.read() + +rsa_private_key = RSA.import_key(private_key) +cipher_rsa = PKCS1_OAEP.new(rsa_private_key) + +def decrypt_password(encrypted_b64_password: str) -> str: + try: + encrypted_bytes = base64.b64decode(encrypted_b64_password) + decrypted = cipher_rsa.decrypt(encrypted_bytes) + return decrypted.decode("utf-8") + except Exception as e: + raise HTTPException(status_code=400, detail="Invalid RSA payload or decryption failed") + +def verify_password(plain_password, hashed_password): + if isinstance(plain_password, str): + plain_password = plain_password.encode('utf-8') + if isinstance(hashed_password, str): + hashed_password = hashed_password.encode('utf-8') + return bcrypt.checkpw(plain_password, hashed_password) + +def get_password_hash(password): + if isinstance(password, str): + password = password.encode('utf-8') + return bcrypt.hashpw(password, bcrypt.gensalt()).decode('utf-8') + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=15) + to_encode.update({"exp": expire}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) diff --git a/backend/app/db/__pycache__/database.cpython-313.pyc b/backend/app/db/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..6679c7b Binary files /dev/null and b/backend/app/db/__pycache__/database.cpython-313.pyc differ diff --git a/backend/app/db/__pycache__/models.cpython-313.pyc b/backend/app/db/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..7491bf3 Binary files /dev/null and b/backend/app/db/__pycache__/models.cpython-313.pyc differ diff --git a/backend/app/db/database.py b/backend/app/db/database.py new file mode 100644 index 0000000..c97b2de --- /dev/null +++ b/backend/app/db/database.py @@ -0,0 +1,15 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from app.core.config import DATABASE_URL + +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/db/models.py b/backend/app/db/models.py new file mode 100644 index 0000000..3ae74a9 --- /dev/null +++ b/backend/app/db/models.py @@ -0,0 +1,13 @@ +from sqlalchemy import Column, Integer, String +from app.db.database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, unique=True, index=True) + hashed_password = Column(String) + display_name = Column(String, nullable=True) + email_id = Column(String, nullable=True) + mobile_no = Column(String, nullable=True) + gender = Column(String, nullable=True) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..dfc1fe3 --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,26 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from app.api import auth, scanner +from app.db.database import engine, Base + +# Initialize the database tables +Base.metadata.create_all(bind=engine) + +app = FastAPI(title="Stock Scanner Pro API") + +# Configure CORS for frontend access +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include Routers +app.include_router(auth.router, prefix="/api/auth", tags=["auth"]) +app.include_router(scanner.router, prefix="/api/scanner", tags=["scanner"]) + +@app.get("/") +def read_root(): + return {"message": "Stock Scanner Backend is running."} diff --git a/backend/app/services/__pycache__/stock_engine.cpython-313.pyc b/backend/app/services/__pycache__/stock_engine.cpython-313.pyc new file mode 100644 index 0000000..2136e4c Binary files /dev/null and b/backend/app/services/__pycache__/stock_engine.cpython-313.pyc differ diff --git a/server.py b/backend/app/services/stock_engine.py similarity index 94% rename from server.py rename to backend/app/services/stock_engine.py index 01b7e56..83ed439 100644 --- a/server.py +++ b/backend/app/services/stock_engine.py @@ -1,20 +1,8 @@ -from fastapi import FastAPI -from fastapi.middleware.cors import CORSMiddleware import yfinance as yf import pandas as pd import numpy as np from concurrent.futures import ThreadPoolExecutor -app = FastAPI() - -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) - # ✅ PERSONAL # ✅ PERSONAL personal_stocks = [ @@ -437,8 +425,7 @@ def analyze(stock): # ========================================= # ✅ API - FAST SCAN # ========================================= -@app.get("/scan") -def scan(): +def get_scan(): sector_output = {} all_stocks = [] @@ -549,8 +536,7 @@ def analyze_ribbon(stock): return None -@app.get("/ribbon") -def ribbon(): +def get_ribbon(): results = [] @@ -561,8 +547,7 @@ def ribbon(): return results -@app.get("/smartmoney") -def smartmoney(): +def get_smartmoney(): results = [] @@ -590,8 +575,7 @@ def smartmoney(): "all": results } -@app.get("/trend") -def trend(): +def get_trend(): results = [] @@ -623,8 +607,7 @@ def trend(): # - Full sector_map (too large) # - Deep MACD + Supertrend # - 6 month data calls -@app.get("/marketintel") -def marketintel(): +def get_marketintel(): try: nifty_news = yf.Ticker("^NSEI").news or [] reliance_news = yf.Ticker("RELIANCE.NS").news or [] diff --git a/backend/private.pem b/backend/private.pem new file mode 100644 index 0000000..b1af8f6 --- /dev/null +++ b/backend/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAtqxw/s/eCLosxX61Zvd0PYicYUoKjbn6gsxe+jb4p0hMNK36 +YsOqYqRPKIh7OosX9CaKDDn8ws6LUt5MAOjwTkNBF8P3bPJ+R+qVXeiqFBIAkRNX +VqxU8eoWNaoNDcNQWSxOIJJanv6UZHbxlv04WLtw5WUtYUbLwO2TcAYghmzk/UTw +p0YjQ0kw6O2E7iQ3i/H7aNqKlEoZoXHcsnDaUsIa3qy6LG7UkGz0LQ3RKu9RqoVO +I8DOV8i1lO3Z5SERiSulfhqzoaD2vWU/sXBRC+yJ3UxZvqIUpR6taToZxPL0aZG9 +XoxFcS2ywogI1XGKWJAYxosgbA8KxKilQFs+EQIDAQABAoIBAE2cThW0sxz6HHN8 +Ng4dLGHIXMho8LruPSS9N80O9e38pYPsNuknQIjQTmFmOxTQa8jrZtNy/5S0tDTt +BVWNoiteH5W9SK4dCNH9NKDFbR1B2MPKd9z3Ms/lKLJ8ITert8NrM3ZbN+09NEbK +0jHYO8xXNsv/hJxDu+WoCnAZ6k+x1IFDXRfV4MrTBX27t4xtfA5KeXQyPzotUeBn +VjVIoRqkCSIJLr+f6ATa+OHlfBjyTbBQscG1/0JNfDDQFmvOYyyc4zpVn0bAINw7 +klvJaXP++DRaZsvNnM1SGpsJ+U86O56ODkX4v2a+H3czhpOA+q0yePHQ/nWr73Nf +7WQYxdECgYEA1OVJZoNPxqVefooY9Tm5mDRXeRVdOormEI8GZkrIAG85w+MD9MMr +2s8FjXDniN4khSClpF9YunB8e77ufu0TgpqhHhXSnn/5JEQ4UO5wiO1EqBvhkFUJ +CXitjYehgwDc5WKvUfPxeMCMvF+rYoHhN1aYIczuN36fhDAXA6pxGyMCgYEA26iy +2RHgu0XNa37Z9KeJCDGNtpj2dYttRAua4UVudxPYn+Q2IdKZ9HPjgWPKgdUjAEup +uuIDhbk1zmQiquvyNMP0HX+p2yG8DX77cdajyvBG5p3BxUX1MxXx4Q6ICVAr4K51 +cZg3YWsIsCsCCO7DkgW1kFRlVptUMMmbJjNoXzsCgYAE32qaqg69YTOUedywYC3b +SfdmkhKcMGmrn1pqJPQG7oTH8v44L+9lBq/92MOz4kG7uk+QP45sVf7DZk9XIF39 +80QUyDMV5Z/yMI2JbKuutp+HqXu0Lf4S9WwjfSM5OF/V8DhLC+ZO+Tk/ZoEptAdP +mO/KdkJNitxjziX4s4H7OQKBgB77oJ51oxlHMz5iWiPkLbP2KWMEGF9kFzlt2Z7E +yFwLdJa4/dmvdv/ACOsLRFkj0xgLlBlEH/MQuMIv5aPuO++tZBV1GGRMUdYlfxoD +iH7rfVSyE87bm0ZlZgS0pAOMR2Qdt3saWVVoX4VZy6Ou6e8C1yVQgirBJhLrnPK6 +dZJZAoGBAK/Nob7Lrxi2kXuJbUOp+Jcir/sPeWE0ZPH9vnAdDkDLrUFbyNWJHHgI +tn1rrPCJeF3+gGdbKcrubHGxJMZGDh3qALs35L6195ZaGys6l+Imp1FxsSFROf/S +Hy2wbjf7fz/gb0U7dfD7ZtVPDvqdFax7NrEdvdcslA0IUiVPqGkC +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/backend/public.pem b/backend/public.pem new file mode 100644 index 0000000..830f7ee --- /dev/null +++ b/backend/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtqxw/s/eCLosxX61Zvd0 +PYicYUoKjbn6gsxe+jb4p0hMNK36YsOqYqRPKIh7OosX9CaKDDn8ws6LUt5MAOjw +TkNBF8P3bPJ+R+qVXeiqFBIAkRNXVqxU8eoWNaoNDcNQWSxOIJJanv6UZHbxlv04 +WLtw5WUtYUbLwO2TcAYghmzk/UTwp0YjQ0kw6O2E7iQ3i/H7aNqKlEoZoXHcsnDa +UsIa3qy6LG7UkGz0LQ3RKu9RqoVOI8DOV8i1lO3Z5SERiSulfhqzoaD2vWU/sXBR +C+yJ3UxZvqIUpR6taToZxPL0aZG9XoxFcS2ywogI1XGKWJAYxosgbA8KxKilQFs+ +EQIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f00c45b..4d48ef8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,10 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.18.1", "lightweight-charts": "^5.2.0", + "node-forge": "^1.4.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.0", + "react-router-dom": "^7.18.2", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" } @@ -13958,9 +13959,9 @@ } }, "node_modules/react-router": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", - "integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -13980,12 +13981,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.18.0", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", - "integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "react-router": "7.18.0" + "react-router": "7.18.2" }, "engines": { "node": ">=20.0.0" diff --git a/package.json b/package.json index 0355f93..1724a3c 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,10 @@ "@testing-library/user-event": "^13.5.0", "axios": "^1.18.1", "lightweight-charts": "^5.2.0", + "node-forge": "^1.4.0", "react": "^19.2.7", "react-dom": "^19.2.7", - "react-router-dom": "^7.18.0", + "react-router-dom": "^7.18.2", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/private.pem b/private.pem new file mode 100644 index 0000000..b87066d --- /dev/null +++ b/private.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuUUB5HsRK6mvKyjAClLckNxDK69MZTW2Yn9qOJZvw3j0R4sl +IhoyBRAm1fxFYthj+klxqw0XKUR7F6DcDuK+3z+a+Ds/uP3tDYbnfT4cr9VEhgq9 +FoLi7AgfH98mqp4VkqDCuYubt4XRtfhpN0TuShT/xImmqLfxkmvZSP/syPoFytEq +X66QZ2QyT4gqzz9/ZMo7EFMeuKA44IBv6qeQzFxSBt0xpeyMXHChn8AeocaOySC3 +kyZBefzmUF2lkOyZKlaPC+0/mUb6ndT1OBfK+EZXzWEbugBWOg/NSy9pnzPRGjAY +qknWs1912+H7t2FEA8BCRZCOK85B05BYeZM+SwIDAQABAoIBAAdhNs6Vkl0FYMtc +z615mHySsYBDds0PQykQPzhq0Z/dKQnSgpOlrHlkJ8/dQRxLV0woccUo+9OyfR63 +UxyqroSxAWjC32psfu1LgtwLxdPBMH4QXnDPnN9fAXJhDqa8LkrNaYQ/id/ZDj3r +27EOZ1l/FaMMAp302cQF1L1iKFnm/ksN9s4G2+8UwrtUWum5LOeD6x22H8443uRf +Lg4IvMC4nLYCQRUAeaKJoTZnw8KeOmKpF2opfu8L1vQ8ubYoZqBX+gNP6GflFG6j +zgoNWfZvRQnDfofH+ZOPcB7vbVKcXKFZ0kHzfw40Nq49GxZndsK30xCUtdDiW7eE +Mgfv9EUCgYEA1q/HUTKBFpvKGu1BwCVTIdvqI0qScWpYst525CbeI36MCkP6ReCX +kvi7259saMRdAaEhIj8EN3BuDu5k8jjNRqkAnkq0pNHkYD3lzXBc4H3RC0LdmEmn +cUUS16sLSkcciI3+SnGjZUbCjso2r2Sy1d/iPhR7L2QpmlzqtPNZxr0CgYEA3OwK +NJMNN1ARv+aOka+mxn6+MsbbWWB0CLSbhg5WRRnmsefDMBjtWsRr+/ufQO7rC+q1 +rv83jiCgJl3u5idS4Hrm/k77iZZzrvJruuR5W5GgvqMTcdut7l4pdqSlvV96D4z7 +29/sze3vVoZv4zIeWxj5QDeT7xLwvjoZpUbHDacCgYAsCjsVCQs6HBNFms4WIJIB +LB/HxZBs+6feaYxyGRcQqPEJWhCJLR1q5OOElhujEkUSBH/LiqnOxZ2OKpFCryxN +BnY+Ao00EmqK46e0kQw8cRLlAH58sv9KWSUYYNocDqJn0NkNZGpkaDOZHxpAuKOH +BDphCcqLWjy+kbkEDbeo8QKBgQCD9m7GJsyvLKHdmi+xMFYTnWOpWwVtZuMIzDFW +Kzw2/JjDzifWlB07qbbDBvOCyvQV4zZxeLvLpwtiv5tTWUv1ERTn9W/lKLyjVOUq +9wzSuLNnDGwyB8Hmb9KerwzdiKmVnmZXWXPPMoBTk+xDrw1Y5xsD0+8G0K6DQptN +EXEXYwKBgQDS5DR5IDGvTycguDhU1QGSW0iEzMfBlZftKAucB/KYcQj8A4+AECKM +9SqcTI2Aneou+7PubpCFDSEckSKZkxb6ibw/ynA1EE55wyjuctyHTpqeHJjoK7jS +R5hyVL5iFBwGxv6oiWeKld+ZbDSB+i89km+kARaXjIB2gZmkepAIyA== +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/public.pem b/public.pem new file mode 100644 index 0000000..bea9d69 --- /dev/null +++ b/public.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuUUB5HsRK6mvKyjAClLc +kNxDK69MZTW2Yn9qOJZvw3j0R4slIhoyBRAm1fxFYthj+klxqw0XKUR7F6DcDuK+ +3z+a+Ds/uP3tDYbnfT4cr9VEhgq9FoLi7AgfH98mqp4VkqDCuYubt4XRtfhpN0Tu +ShT/xImmqLfxkmvZSP/syPoFytEqX66QZ2QyT4gqzz9/ZMo7EFMeuKA44IBv6qeQ +zFxSBt0xpeyMXHChn8AeocaOySC3kyZBefzmUF2lkOyZKlaPC+0/mUb6ndT1OBfK ++EZXzWEbugBWOg/NSy9pnzPRGjAYqknWs1912+H7t2FEA8BCRZCOK85B05BYeZM+ +SwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/src/App.js b/src/App.js index c7f4f07..b10f14d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,4 +1,6 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; +import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; +import axios from "axios"; import Chart from "./components/Chart"; import Scanner from "./components/Scanner"; import Ribbon from "./components/Ribbon"; @@ -7,52 +9,76 @@ import Popup from "./components/Popup"; import AboutScanner from "./components/AboutScanner"; import MarketIntel from "./components/MarketIntel"; import TrendScanner from "./components/TrendScanner"; +import Login from "./components/Login"; +import Signup from "./components/Signup"; +import ProtectedRoute from "./components/ProtectedRoute"; +import Profile from "./components/Profile"; import "./index.css"; -function App() { +function Dashboard() { const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE"); const [signal, setSignal] = useState(null); const [view, setView] = useState("scanner"); + const [userProfile, setUserProfile] = useState(null); + const [dropdownOpen, setDropdownOpen] = useState(false); + const navigate = useNavigate(); + + const fetchProfile = () => { + axios.get("http://127.0.0.1:8000/api/auth/me") + .then(res => setUserProfile(res.data)) + .catch(err => { + if (err.response?.status === 401) handleLogout(); + }); + }; + + useEffect(() => { + fetchProfile(); + }, []); + + const handleLogout = () => { + localStorage.removeItem("token"); + navigate("/login"); + }; return (
-

📈 Stock Scanner Pro

+ {/* HEADER */} +
+

📈 Stock Scanner Pro

+ + {userProfile && ( +
+
setDropdownOpen(!dropdownOpen)} + > +
+ {userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'} +
+ {userProfile.display_name || userProfile.username} + +
+ + {dropdownOpen && ( +
+ + +
+ )} +
+ )} +
- - - - - - - - - + + + + +
@@ -67,17 +93,26 @@ function App() { {signal && } )} - {view === "ribbon" && } - {view === "smartmoney" && } - {view === "marketintel" && } - {view === "trend" && } + {view === "profile" && }
); } +function App() { + return ( + + + } /> + } /> + } /> + + + ); +} + export default App; \ No newline at end of file diff --git a/src/components/Login.js b/src/components/Login.js new file mode 100644 index 0000000..80a7c87 --- /dev/null +++ b/src/components/Login.js @@ -0,0 +1,70 @@ +import React, { useState } from "react"; +import axios from "axios"; +import { useNavigate, Link } from "react-router-dom"; +import forge from "node-forge"; + +function Login() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + const handleLogin = async (e) => { + e.preventDefault(); + setLoading(true); + setError(""); + try { + const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); + const publicKeyPem = resKey.data.public_key; + const publicKey = forge.pki.publicKeyFromPem(publicKeyPem); + const encrypted = publicKey.encrypt(password, 'RSA-OAEP'); + const encryptedBase64 = forge.util.encode64(encrypted); + + const res = await axios.post("http://127.0.0.1:8000/api/auth/login", { + username, + password: encryptedBase64 + }); + + localStorage.setItem("token", res.data.access_token); + navigate("/"); + } catch (err) { + setError(err.response?.data?.detail || "Login failed"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

📈 Login

+
+ setUsername(e.target.value)} + style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }} + required + /> + setPassword(e.target.value)} + style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }} + required + /> + {error &&

{error}

} + +
+

Don't have an account? Sign up

+
+
+ ); +} + +export default Login; diff --git a/src/components/MarketIntel.js b/src/components/MarketIntel.js index a66deef..2a0e3f3 100644 --- a/src/components/MarketIntel.js +++ b/src/components/MarketIntel.js @@ -10,7 +10,7 @@ function MarketIntel() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/marketintel"); + const res = await axios.get("http://127.0.0.1:8000/api/scanner/marketintel"); setData(res.data); } catch (err) { console.error(err); diff --git a/src/components/Profile.js b/src/components/Profile.js new file mode 100644 index 0000000..238a860 --- /dev/null +++ b/src/components/Profile.js @@ -0,0 +1,173 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import forge from "node-forge"; + +function Profile({ userProfile, onProfileUpdated }) { + const [formData, setFormData] = useState({ + display_name: "", + email_id: "", + mobile_no: "", + gender: "", + password: "" + }); + + const [loading, setLoading] = useState(false); + const [message, setMessage] = useState(""); + const [error, setError] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + useEffect(() => { + if (userProfile) { + setFormData({ + display_name: userProfile.display_name || "", + email_id: userProfile.email_id || "", + mobile_no: userProfile.mobile_no || "", + gender: userProfile.gender || "", + password: "" // password field is blank initially + }); + } + }, [userProfile]); + + const handleChange = (e) => { + setFormData({ ...formData, [e.target.name]: e.target.value }); + }; + + const handleSave = async (e) => { + e.preventDefault(); + setLoading(true); + setMessage(""); + setError(""); + + try { + const payload = { ...formData }; + + if (payload.password) { + // Fetch Public Key to encrypt new password + const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); + const publicKey = forge.pki.publicKeyFromPem(resKey.data.public_key); + const encrypted = publicKey.encrypt(payload.password, 'RSA-OAEP'); + payload.password = forge.util.encode64(encrypted); + } else { + delete payload.password; + } + + await axios.put("http://127.0.0.1:8000/api/auth/update-profile", payload); + setMessage("Profile updated successfully! ✅"); + if (onProfileUpdated) onProfileUpdated(); + setFormData(prev => ({ ...prev, password: "" })); // Clear password field + } catch (err) { + setError(err.response?.data?.detail || "Failed to update profile"); + } finally { + setLoading(false); + } + }; + + if (!userProfile) return
Loading Profile...
; + + return ( +
+
+
+
+ {userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'} +
+
+

Profile Settings

+

Manage your personal details and security

+
+
+ +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ +
+ +
+ + setShowPassword(!showPassword)} + style={{ position: 'absolute', right: '15px', cursor: 'pointer', fontSize: '1.2rem', userSelect: 'none' }} + title={showPassword ? "Hide password" : "Show password"} + > + {showPassword ? "🙈" : "👁️"} + +
+
+ + {message &&
{message}
} + {error &&
{error}
} + +
+ +
+
+
+
+ ); +} + +export default Profile; diff --git a/src/components/ProtectedRoute.js b/src/components/ProtectedRoute.js new file mode 100644 index 0000000..d4d17fc --- /dev/null +++ b/src/components/ProtectedRoute.js @@ -0,0 +1,12 @@ +import React from "react"; +import { Navigate } from "react-router-dom"; + +function ProtectedRoute({ children }) { + const token = localStorage.getItem("token"); + if (!token) { + return ; + } + return children; +} + +export default ProtectedRoute; diff --git a/src/components/Ribbon.js b/src/components/Ribbon.js index c93fa41..266e1ce 100644 --- a/src/components/Ribbon.js +++ b/src/components/Ribbon.js @@ -7,7 +7,7 @@ function Ribbon() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/ribbon"); + const res = await axios.get("http://127.0.0.1:8000/api/scanner/ribbon"); setData(res.data || []); setLoading(false); } catch (err) { diff --git a/src/components/Scanner.js b/src/components/Scanner.js index 49d8f9c..9a988fd 100644 --- a/src/components/Scanner.js +++ b/src/components/Scanner.js @@ -7,7 +7,7 @@ function Scanner({ onSelectStock, onSignal }) { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/scan"); + const res = await axios.get("http://127.0.0.1:8000/api/scanner/scan"); const newData = res.data; const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null; diff --git a/src/components/Signup.js b/src/components/Signup.js new file mode 100644 index 0000000..dd4f7aa --- /dev/null +++ b/src/components/Signup.js @@ -0,0 +1,69 @@ +import React, { useState } from "react"; +import axios from "axios"; +import { useNavigate, Link } from "react-router-dom"; +import forge from "node-forge"; + +function Signup() { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const [loading, setLoading] = useState(false); + const navigate = useNavigate(); + + const handleSignup = async (e) => { + e.preventDefault(); + setLoading(true); + setError(""); + try { + const resKey = await axios.get("http://127.0.0.1:8000/api/auth/public-key"); + const publicKeyPem = resKey.data.public_key; + const publicKey = forge.pki.publicKeyFromPem(publicKeyPem); + const encrypted = publicKey.encrypt(password, 'RSA-OAEP'); + const encryptedBase64 = forge.util.encode64(encrypted); + + await axios.post("http://127.0.0.1:8000/api/auth/signup", { + username, + password: encryptedBase64 + }); + + navigate("/login"); + } catch (err) { + setError(err.response?.data?.detail || "Signup failed"); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

✨ Sign Up

+
+ setUsername(e.target.value)} + style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }} + required + /> + setPassword(e.target.value)} + style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }} + required + /> + {error &&

{error}

} + +
+

Already have an account? Login

+
+
+ ); +} + +export default Signup; diff --git a/src/components/SmartMoney.js b/src/components/SmartMoney.js index 32065b7..19c9e2f 100644 --- a/src/components/SmartMoney.js +++ b/src/components/SmartMoney.js @@ -12,7 +12,7 @@ function SmartMoney() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/smartmoney"); + const res = await axios.get("http://127.0.0.1:8000/api/scanner/smartmoney"); setData(res.data); } catch (err) { console.error(err); diff --git a/src/components/TrendScanner.js b/src/components/TrendScanner.js index c7ac2cc..d876751 100644 --- a/src/components/TrendScanner.js +++ b/src/components/TrendScanner.js @@ -12,7 +12,7 @@ function TrendScanner() { const loadData = async () => { try { - const res = await axios.get("http://127.0.0.1:8000/trend"); + const res = await axios.get("http://127.0.0.1:8000/api/scanner/trend"); setData(res.data); } catch (err) { console.error(err); diff --git a/src/index.js b/src/index.js index bd365f0..b1559f9 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,17 @@ import React from "react"; import ReactDOM from "react-dom/client"; -import App from "./App"; +import App from './App'; +import axios from 'axios'; -const root = ReactDOM.createRoot(document.getElementById("root")); +axios.interceptors.request.use((config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +const root = ReactDOM.createRoot(document.getElementById('root')); root.render(