User Auth Done

This commit is contained in:
2026-08-05 22:00:49 +05:30
parent dbac720ce0
commit 076c4ff68f
33 changed files with 758 additions and 79 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

106
backend/app/api/auth.py Normal file
View File

@@ -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"}

View File

@@ -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()

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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)

Binary file not shown.

Binary file not shown.

View File

@@ -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()

13
backend/app/db/models.py Normal file
View File

@@ -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)

26
backend/app/main.py Normal file
View File

@@ -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."}

View File

@@ -1,20 +1,8 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import yfinance as yf import yfinance as yf
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# ✅ PERSONAL # ✅ PERSONAL
# ✅ PERSONAL # ✅ PERSONAL
personal_stocks = [ personal_stocks = [
@@ -437,8 +425,7 @@ def analyze(stock):
# ========================================= # =========================================
# ✅ API - FAST SCAN # ✅ API - FAST SCAN
# ========================================= # =========================================
@app.get("/scan") def get_scan():
def scan():
sector_output = {} sector_output = {}
all_stocks = [] all_stocks = []
@@ -549,8 +536,7 @@ def analyze_ribbon(stock):
return None return None
@app.get("/ribbon") def get_ribbon():
def ribbon():
results = [] results = []
@@ -561,8 +547,7 @@ def ribbon():
return results return results
@app.get("/smartmoney") def get_smartmoney():
def smartmoney():
results = [] results = []
@@ -590,8 +575,7 @@ def smartmoney():
"all": results "all": results
} }
@app.get("/trend") def get_trend():
def trend():
results = [] results = []
@@ -623,8 +607,7 @@ def trend():
# - Full sector_map (too large) # - Full sector_map (too large)
# - Deep MACD + Supertrend # - Deep MACD + Supertrend
# - 6 month data calls # - 6 month data calls
@app.get("/marketintel") def get_marketintel():
def marketintel():
try: try:
nifty_news = yf.Ticker("^NSEI").news or [] nifty_news = yf.Ticker("^NSEI").news or []
reliance_news = yf.Ticker("RELIANCE.NS").news or [] reliance_news = yf.Ticker("RELIANCE.NS").news or []

27
backend/private.pem Normal file
View File

@@ -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-----

9
backend/public.pem Normal file
View File

@@ -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-----

17
package-lock.json generated
View File

@@ -14,9 +14,10 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.18.1", "axios": "^1.18.1",
"lightweight-charts": "^5.2.0", "lightweight-charts": "^5.2.0",
"node-forge": "^1.4.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^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", "react-scripts": "5.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
} }
@@ -13958,9 +13959,9 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "7.18.0", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.0.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz",
"integrity": "sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==", "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cookie": "^1.0.1", "cookie": "^1.0.1",
@@ -13980,12 +13981,12 @@
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "7.18.0", "version": "7.18.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.0.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz",
"integrity": "sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==", "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"react-router": "7.18.0" "react-router": "7.18.2"
}, },
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"

View File

@@ -9,9 +9,10 @@
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.18.1", "axios": "^1.18.1",
"lightweight-charts": "^5.2.0", "lightweight-charts": "^5.2.0",
"node-forge": "^1.4.0",
"react": "^19.2.7", "react": "^19.2.7",
"react-dom": "^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", "react-scripts": "5.0.1",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },

27
private.pem Normal file
View File

@@ -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-----

9
public.pem Normal file
View File

@@ -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-----

View File

@@ -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 Chart from "./components/Chart";
import Scanner from "./components/Scanner"; import Scanner from "./components/Scanner";
import Ribbon from "./components/Ribbon"; import Ribbon from "./components/Ribbon";
@@ -7,52 +9,76 @@ import Popup from "./components/Popup";
import AboutScanner from "./components/AboutScanner"; import AboutScanner from "./components/AboutScanner";
import MarketIntel from "./components/MarketIntel"; import MarketIntel from "./components/MarketIntel";
import TrendScanner from "./components/TrendScanner"; 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"; import "./index.css";
function App() { function Dashboard() {
const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE"); const [selectedStock, setSelectedStock] = useState("NSE:RELIANCE");
const [signal, setSignal] = useState(null); const [signal, setSignal] = useState(null);
const [view, setView] = useState("scanner"); 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 ( return (
<div className="app-container"> <div className="app-container">
<h1>📈 Stock Scanner Pro</h1> {/* HEADER */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', position: 'relative' }}>
<h1 style={{ margin: 0 }}>📈 Stock Scanner Pro</h1>
{userProfile && (
<div style={{ position: 'relative' }}>
<div
style={{ display: 'flex', alignItems: 'center', gap: '12px', cursor: 'pointer', background: 'var(--glass-bg)', padding: '8px 16px', borderRadius: '30px', border: '1px solid var(--border-color)', backdropFilter: 'blur(10px)' }}
onClick={() => setDropdownOpen(!dropdownOpen)}
>
<div style={{ width: '35px', height: '35px', borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', display: 'flex', justifyContent: 'center', alignItems: 'center', fontWeight: 'bold', fontSize: '1.2rem', color: 'white' }}>
{userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
</div>
<span style={{ fontWeight: 600 }}>{userProfile.display_name || userProfile.username}</span>
<span style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}></span>
</div>
{dropdownOpen && (
<div className="glass-card animate-fade-in" style={{ position: 'absolute', top: '55px', right: '0', width: '220px', zIndex: 100, padding: '10px' }}>
<button className="nav-btn" style={{ width: '100%', marginBottom: '8px', border: 'none', whiteSpace: 'nowrap' }} onClick={() => { setView('profile'); setDropdownOpen(false); }}>
Profile Settings
</button>
<button className="nav-btn" style={{ width: '100%', border: 'none', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', whiteSpace: 'nowrap' }} onClick={handleLogout}>
🚪 Logout
</button>
</div>
)}
</div>
)}
</div>
<div className="nav-container"> <div className="nav-container">
<button <button className={`nav-btn ${view === "scanner" ? "active" : ""}`} onClick={() => setView("scanner")}>📊 Scanner</button>
className={`nav-btn ${view === "scanner" ? "active" : ""}`} <button className={`nav-btn ${view === "ribbon" ? "active" : ""}`} onClick={() => setView("ribbon")}>📈 Ribbon Strategy</button>
onClick={() => setView("scanner")} <button className={`nav-btn ${view === "smartmoney" ? "active" : ""}`} onClick={() => setView("smartmoney")}>🏦 Smart Money</button>
> <button className={`nav-btn ${view === "trend" ? "active" : ""}`} onClick={() => setView("trend")}> Trend Analysis</button>
📊 Scanner <button className={`nav-btn ${view === "marketintel" ? "active" : ""}`} onClick={() => setView("marketintel")}>📰 Market Intel</button>
</button>
<button
className={`nav-btn ${view === "ribbon" ? "active" : ""}`}
onClick={() => setView("ribbon")}
>
📈 Ribbon Strategy
</button>
<button
className={`nav-btn ${view === "smartmoney" ? "active" : ""}`}
onClick={() => setView("smartmoney")}
>
🏦 Smart Money
</button>
<button
className={`nav-btn ${view === "trend" ? "active" : ""}`}
onClick={() => setView("trend")}
>
Trend Analysis
</button>
<button
className={`nav-btn ${view === "marketintel" ? "active" : ""}`}
onClick={() => setView("marketintel")}
>
📰 Market Intel
</button>
</div> </div>
<div className="animate-fade-in"> <div className="animate-fade-in">
@@ -67,17 +93,26 @@ function App() {
{signal && <Popup signal={signal} />} {signal && <Popup signal={signal} />}
</> </>
)} )}
{view === "ribbon" && <Ribbon />} {view === "ribbon" && <Ribbon />}
{view === "smartmoney" && <SmartMoney />} {view === "smartmoney" && <SmartMoney />}
{view === "marketintel" && <MarketIntel />} {view === "marketintel" && <MarketIntel />}
{view === "trend" && <TrendScanner />} {view === "trend" && <TrendScanner />}
{view === "profile" && <Profile userProfile={userProfile} onProfileUpdated={fetchProfile} />}
</div> </div>
</div> </div>
); );
} }
function App() {
return (
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route path="/signup" element={<Signup />} />
<Route path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
</Routes>
</Router>
);
}
export default App; export default App;

70
src/components/Login.js Normal file
View File

@@ -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 (
<div className="app-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div className="glass-card" style={{ width: '400px', textAlign: 'center' }}>
<h2 className="text-blue">📈 Login</h2>
<form onSubmit={handleLogin} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
{error && <p className="text-red">{error}</p>}
<button type="submit" className="nav-btn active" style={{ justifyContent: 'center' }} disabled={loading}>
{loading ? "Logging in..." : "Login"}
</button>
</form>
<p style={{ marginTop: '20px' }}>Don't have an account? <Link to="/signup" className="text-blue">Sign up</Link></p>
</div>
</div>
);
}
export default Login;

View File

@@ -10,7 +10,7 @@ function MarketIntel() {
const loadData = async () => { const loadData = async () => {
try { 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); setData(res.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

173
src/components/Profile.js Normal file
View File

@@ -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 <div className="glass-card text-center">Loading Profile...</div>;
return (
<div className="animate-fade-in" style={{ display: 'flex', justifyContent: 'center', marginTop: '20px' }}>
<div className="glass-card" style={{ width: '100%', maxWidth: '600px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '20px', marginBottom: '30px' }}>
<div style={{ width: '80px', height: '80px', borderRadius: '50%', background: 'linear-gradient(135deg, var(--accent-blue), var(--accent-green))', display: 'flex', justifyContent: 'center', alignItems: 'center', fontSize: '2.5rem', fontWeight: 'bold', color: 'white', boxShadow: 'var(--shadow-glow)' }}>
{userProfile.display_name ? userProfile.display_name.charAt(0).toUpperCase() : '?'}
</div>
<div>
<h2 style={{ margin: 0, fontSize: '1.8rem' }} className="text-blue">Profile Settings</h2>
<p className="text-muted" style={{ margin: '5px 0 0 0' }}>Manage your personal details and security</p>
</div>
</div>
<form onSubmit={handleSave} style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Username (Read Only)</label>
<input
type="text"
value={userProfile.username}
disabled
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'rgba(0,0,0,0.2)', color: 'var(--text-muted)', cursor: 'not-allowed' }}
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Display Name</label>
<input
type="text" name="display_name" value={formData.display_name} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="Enter your name"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Email Address</label>
<input
type="email" name="email_id" value={formData.email_id} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="your.email@example.com"
autoComplete="off"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Mobile Number</label>
<input
type="text" name="mobile_no" value={formData.mobile_no} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="+91 9999999999"
/>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-secondary" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Gender</label>
<select
name="gender" value={formData.gender} onChange={handleChange}
style={{ padding: '12px', borderRadius: '8px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
>
<option value="">Select Gender</option>
<option value="Male">Male</option>
<option value="Female">Female</option>
<option value="Other">Other</option>
</select>
</div>
</div>
<div style={{ borderTop: '1px solid var(--border-color)', margin: '10px 0' }}></div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label className="text-orange" style={{ fontSize: '0.9rem', fontWeight: 600 }}>Change Password (Optional)</label>
<div style={{ position: 'relative', display: 'flex', alignItems: 'center' }}>
<input
type={showPassword ? "text" : "password"}
name="password"
value={formData.password}
onChange={handleChange}
style={{ width: '100%', padding: '12px', paddingRight: '45px', borderRadius: '8px', border: '1px solid rgba(245, 158, 11, 0.3)', background: 'var(--bg-tertiary)', color: 'white' }}
placeholder="Leave blank to keep current password"
autoComplete="new-password"
/>
<span
onClick={() => setShowPassword(!showPassword)}
style={{ position: 'absolute', right: '15px', cursor: 'pointer', fontSize: '1.2rem', userSelect: 'none' }}
title={showPassword ? "Hide password" : "Show password"}
>
{showPassword ? "🙈" : "👁️"}
</span>
</div>
</div>
{message && <div style={{ padding: '10px', background: 'var(--accent-green-bg)', color: 'var(--accent-green)', borderRadius: '8px', border: '1px solid rgba(16, 185, 129, 0.2)' }}>{message}</div>}
{error && <div style={{ padding: '10px', background: 'var(--accent-red-bg)', color: 'var(--accent-red)', borderRadius: '8px', border: '1px solid rgba(239, 68, 68, 0.2)' }}>{error}</div>}
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: '10px' }}>
<button type="submit" className="nav-btn active" style={{ padding: '12px 30px', fontSize: '1rem' }} disabled={loading}>
{loading ? "Saving..." : "Save Changes"}
</button>
</div>
</form>
</div>
</div>
);
}
export default Profile;

View File

@@ -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 <Navigate to="/login" replace />;
}
return children;
}
export default ProtectedRoute;

View File

@@ -7,7 +7,7 @@ function Ribbon() {
const loadData = async () => { const loadData = async () => {
try { 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 || []); setData(res.data || []);
setLoading(false); setLoading(false);
} catch (err) { } catch (err) {

View File

@@ -7,7 +7,7 @@ function Scanner({ onSelectStock, onSignal }) {
const loadData = async () => { const loadData = async () => {
try { 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 newData = res.data;
const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null; const best = newData.best_5 && newData.best_5.length > 0 ? newData.best_5[0] : null;

69
src/components/Signup.js Normal file
View File

@@ -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 (
<div className="app-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh' }}>
<div className="glass-card" style={{ width: '400px', textAlign: 'center' }}>
<h2 className="text-green"> Sign Up</h2>
<form onSubmit={handleSignup} style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={e => setPassword(e.target.value)}
style={{ padding: '10px', borderRadius: '5px', border: '1px solid var(--border-color)', background: 'var(--bg-tertiary)', color: 'white' }}
required
/>
{error && <p className="text-red">{error}</p>}
<button type="submit" className="nav-btn active" style={{ justifyContent: 'center', background: 'var(--accent-green)', borderColor: 'var(--accent-green)' }} disabled={loading}>
{loading ? "Signing up..." : "Sign Up"}
</button>
</form>
<p style={{ marginTop: '20px' }}>Already have an account? <Link to="/login" className="text-blue">Login</Link></p>
</div>
</div>
);
}
export default Signup;

View File

@@ -12,7 +12,7 @@ function SmartMoney() {
const loadData = async () => { const loadData = async () => {
try { 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); setData(res.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@@ -12,7 +12,7 @@ function TrendScanner() {
const loadData = async () => { const loadData = async () => {
try { 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); setData(res.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

View File

@@ -1,8 +1,17 @@
import React from "react"; import React from "react";
import ReactDOM from "react-dom/client"; 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( root.render(
<React.StrictMode> <React.StrictMode>
<App /> <App />