Compare commits
1 Commits
main
...
user_auth_
| Author | SHA1 | Date | |
|---|---|---|---|
| 076c4ff68f |
BIN
backend/app/__pycache__/main.cpython-313.pyc
Normal file
BIN
backend/app/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/auth.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/auth.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/api/__pycache__/scanner.cpython-313.pyc
Normal file
BIN
backend/app/api/__pycache__/scanner.cpython-313.pyc
Normal file
Binary file not shown.
106
backend/app/api/auth.py
Normal file
106
backend/app/api/auth.py
Normal 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"}
|
||||||
27
backend/app/api/scanner.py
Normal file
27
backend/app/api/scanner.py
Normal 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()
|
||||||
BIN
backend/app/core/__pycache__/config.cpython-313.pyc
Normal file
BIN
backend/app/core/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/core/__pycache__/security.cpython-313.pyc
Normal file
BIN
backend/app/core/__pycache__/security.cpython-313.pyc
Normal file
Binary file not shown.
6
backend/app/core/config.py
Normal file
6
backend/app/core/config.py
Normal 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
|
||||||
61
backend/app/core/security.py
Normal file
61
backend/app/core/security.py
Normal 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)
|
||||||
BIN
backend/app/db/__pycache__/database.cpython-313.pyc
Normal file
BIN
backend/app/db/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
backend/app/db/__pycache__/models.cpython-313.pyc
Normal file
BIN
backend/app/db/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
15
backend/app/db/database.py
Normal file
15
backend/app/db/database.py
Normal 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
13
backend/app/db/models.py
Normal 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
26
backend/app/main.py
Normal 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."}
|
||||||
BIN
backend/app/services/__pycache__/stock_engine.cpython-313.pyc
Normal file
BIN
backend/app/services/__pycache__/stock_engine.cpython-313.pyc
Normal file
Binary file not shown.
@@ -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
27
backend/private.pem
Normal 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
9
backend/public.pem
Normal 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
17
package-lock.json
generated
@@ -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"
|
||||||
|
|||||||
@@ -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
27
private.pem
Normal 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
9
public.pem
Normal 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-----
|
||||||
117
src/App.js
117
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 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
70
src/components/Login.js
Normal 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;
|
||||||
@@ -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
173
src/components/Profile.js
Normal 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;
|
||||||
12
src/components/ProtectedRoute.js
Normal file
12
src/components/ProtectedRoute.js
Normal 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;
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
69
src/components/Signup.js
Normal 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;
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
13
src/index.js
13
src/index.js
@@ -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 />
|
||||||
|
|||||||
Reference in New Issue
Block a user