rsnarsna commited on
Commit
81d8c42
·
1 Parent(s): a65bc49

first commit

Browse files
.gitignore ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+
204
+ # Ruff stuff:
205
+ .ruff_cache/
206
+
207
+ # PyPI configuration file
208
+ .pypirc
209
+
210
+ # Marimo
211
+ marimo/_static/
212
+ marimo/_lsp/
213
+ __marimo__/
214
+
215
+ # Streamlit
216
+ .streamlit/secrets.toml
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official lightweight Python image
2
+ FROM python:3.10-slim
3
+
4
+ # Set the working directory inside the container
5
+ WORKDIR /app
6
+
7
+ # Copy the requirements file into the container
8
+ COPY backend/requirements.txt /app/backend/requirements.txt
9
+
10
+ # Install the dependencies
11
+ RUN pip install --no-cache-dir -r/app/backend/requirements.txt
12
+
13
+ # Copy the entire directory into the container
14
+ COPY . /app
15
+
16
+ # Expose the correct port (Hugging Face Spaces uses 7860 by default, change if needed)
17
+ EXPOSE 7860
18
+
19
+ # Command to run the FastAPI application using uvicorn
20
+ CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
- title: Fullstack Simple Auth Docs Upload
3
- emoji: 🐨
4
- colorFrom: purple
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- license: apache-2.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
1
+ # Task 1: FastAPI File Management Application
2
+
3
+ A modern, fast, and secure web application built with **FastAPI**, **MySQL**, and **Jinja2**. This application allows users to register, log in, manage their profiles, and securely upload, download, and delete files through a beautiful dark-themed, glassmorphic UI.
4
+
5
+ ---
6
+
7
+ ## 🚀 Features
8
+
9
+ - **User Authentication**: Secure signup and login functionality using hashed passwords (bcrypt) and cookie-based session management.
10
+ - **File Management**: Upload (up to 2 files at once), download, and delete files securely.
11
+ - **Interactive Dashboard**: A user-friendly dashboard to view all stored files, track upload times, and manage data.
12
+ - **RESTful API**: Along with the frontend, the app provides standard JSON API endpoints for profile management and system interactions.
13
+ - **Glassmorphic UI**: A stunning, responsive frontend built with customized CSS and Jinja2 templates.
14
+
15
+ ---
16
+
17
+ ## 🛠️ Tech Stack
18
+
19
+ - **Backend**: FastAPI (Python)
20
+ - **Database**: MySQL (via SQLAlchemy ORM)
21
+ - **Frontend**: HTML5, CSS3 (Glassmorphism), Jinja2 Templates
22
+ - **Authentication**: JWT token-based auth stored in HTTP-only cookies
23
+ - **File Storage**: Local filesystem (`backend/uploaded_files/`)
24
+
25
+ ---
26
+
27
+ ## 📋 Prerequisites
28
+
29
+ Before you begin, ensure you have the following installed:
30
+ - Python 3.9+
31
+ - MySQL Server (running locally or remotely)
32
+ - `pip` (Python package manager)
33
+
34
+ ---
35
+
36
+ ## ⚙️ Installation & Setup
37
+
38
+ 1. **Clone or Download the Repository**
39
+ Navigate to the project directory:
40
+ ```bash
41
+ cd "g:\Soft Mania\internship\task 1"
42
+ ```
43
+
44
+ 2. **Set Up a Virtual Environment**
45
+ ```bash
46
+ python -m venv venv
47
+ # On Windows:
48
+ .\venv\Scripts\activate
49
+ # On macOS/Linux:
50
+ source venv/bin/activate
51
+ ```
52
+
53
+ 3. **Install Dependencies**
54
+ Install all required packages from `req.txt`:
55
+ ```bash
56
+ pip install -r req.txt
57
+ ```
58
+
59
+ 4. **Database Configuration**
60
+ Ensure your MySQL server is running. Create a database (e.g., `testbd`).
61
+ Update the `DATABASE_URL` string in `backend/main.py` if your database credentials differ from:
62
+ ```python
63
+ SQLALCHEMY_DATABASE_URL = "mysql+pymysql://root:root@localhost/testbd"
64
+ ```
65
+
66
+ 5. **Run the Application**
67
+ Start the FastAPI development server using Uvicorn:
68
+ ```bash
69
+ python -m uvicorn backend.main:app --host 0.0.0.0 --port 8890 --reload
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 📖 Usage Guide
75
+
76
+ Once the server is running, the application is accessible through your web browser.
77
+
78
+ ### 🌐 Web Interface (UI)
79
+ - **Home / Login**: Navigate to `http://localhost:8890/login` to access the login portal.
80
+ - **Sign Up**: If you are a new user, click "Sign up" on the login page or navigate to `http://localhost:8890/signup` to create a new account.
81
+ - **Dashboard**: Upon logging in, you will be redirected to `http://localhost:8890/dashboard`.
82
+ - **Uploading**: Use the "Upload Files" panel to select and upload up to 2 files (PDF, PNG, JPG/JPEG).
83
+ - **Managing Files**: View your uploaded files in the "Your Files" table. Click **⬇ Download** to save them locally, or **🗑 Delete** to remove them permanently from the server.
84
+ - **Logout**: Click the "Logout" button in the top right corner of the dashboard to securely end your session.
85
+
86
+ ### 🔌 API Endpoints (For Developers)
87
+ The application also exposes JSON endpoints that can be tested via tools like Postman or cURL.
88
+ *(Note: Some UI and API routes share paths depending on the method and `Accept` headers).*
89
+
90
+ - `POST /signup` - Register a new user (Form Data or JSON).
91
+ - `POST /login` - Authenticate and receive an access token.
92
+ - `GET /users/me` - Retrieve current logged-in user details.
93
+ - `POST /upload` - Upload files via API.
94
+ - `GET /files` - List all files belonging to the auth user.
95
+ - `DELETE /files/{id}` - Delete a specific file.
96
+
97
  ---
98
+
99
+ ## 📂 Project Structure
100
+
101
+ ```text
102
+ task 1/
103
+
104
+ ├── backend/
105
+ │ ├── main.py # Main FastAPI application & routes
106
+ │ ├── templates/ # Jinja2 HTML Templates
107
+ │ │ ├── base.html # Global layout wrapper
108
+ │ │ ├── login.html # Login page
109
+ │ │ ├── signup.html # Registration page
110
+ │ │ └── dashboard.html # User file management dashboard
111
+ │ ├── static/
112
+ │ │ └── style.css # Design system & Glassmorphic styles
113
+ │ └── uploaded_files/ # Secure directory for user uploads
114
+
115
+ ├── req.txt # Project dependencies list
116
+ ├── .gitignore # Files ignored by version control
117
+ └── README.md # This documentation file
118
+ ```
119
+
120
  ---
121
 
122
+ ## 🔒 Security Notes
123
+ - Passwords are securely hashed using `bcrypt` before being stored in MySQL.
124
+ - Uploaded files are renamed with unique identifiers to prevent overwriting and path traversal attacks.
125
+ - Session tokens are stored in `httponly` browser cookies for the UI flow to mitigate XSS risks.
backend/main.py ADDED
@@ -0,0 +1,518 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Optional, List
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ from fastapi import FastAPI, Depends, HTTPException, status, Request, Form
7
+ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
8
+ from fastapi.staticfiles import StaticFiles
9
+ from fastapi.templating import Jinja2Templates
10
+ from fastapi.responses import RedirectResponse
11
+ from pydantic import BaseModel, EmailStr
12
+ from passlib.context import CryptContext
13
+
14
+ from sqlalchemy import create_engine, Column, Integer, String, Text, ForeignKey, TIMESTAMP
15
+ from sqlalchemy.sql import func
16
+ from sqlalchemy.orm import sessionmaker, declarative_base, relationship, Session
17
+
18
+ # ==========================================
19
+ # Database Configuration
20
+ # ==========================================
21
+ SQLALCHEMY_DATABASE_URL = os.getenv("DATABASE_URL", "mysql+mysqlconnector://root:@localhost:3306/fullstack_test")
22
+
23
+ engine = create_engine(SQLALCHEMY_DATABASE_URL)
24
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
25
+
26
+ Base = declarative_base()
27
+
28
+ def get_db():
29
+ db = SessionLocal()
30
+ try:
31
+ yield db
32
+ finally:
33
+ db.close()
34
+
35
+ # ==========================================
36
+ # Models
37
+ # ==========================================
38
+ class User(Base):
39
+ __tablename__ = "users"
40
+
41
+ id = Column(Integer, primary_key=True, index=True)
42
+ name = Column(String(100), nullable=False)
43
+ age = Column(Integer, nullable=False)
44
+ address = Column(Text, nullable=False)
45
+ email = Column(String(100), unique=True, index=True, nullable=False)
46
+ mobile_number = Column(String(20), nullable=False)
47
+ password_hash = Column(String(255), nullable=False)
48
+ created_at = Column(TIMESTAMP, server_default=func.now())
49
+
50
+ files = relationship("File", back_populates="owner")
51
+
52
+ class File(Base):
53
+ __tablename__ = "files"
54
+
55
+ id = Column(Integer, primary_key=True, index=True)
56
+ filename = Column(String(255), nullable=False)
57
+ file_type = Column(String(50), nullable=False)
58
+ user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False)
59
+ upload_timestamp = Column(TIMESTAMP, server_default=func.now())
60
+
61
+ owner = relationship("User", back_populates="files")
62
+
63
+ # ==========================================
64
+ # Schemas
65
+ # ==========================================
66
+ class UserCreate(BaseModel):
67
+ name: str
68
+ age: int
69
+ address: str
70
+ email: EmailStr
71
+ mobile_number: str
72
+ password: str
73
+
74
+ class UserLogin(BaseModel):
75
+ email: EmailStr
76
+ password: str
77
+
78
+ class UserResponse(BaseModel):
79
+ id: int
80
+ name: str
81
+ email: str
82
+
83
+ class Config:
84
+ from_attributes = True
85
+
86
+ class Token(BaseModel):
87
+ access_token: str
88
+ token_type: str
89
+
90
+ class TokenData(BaseModel):
91
+ email: Optional[str] = None
92
+
93
+ # ==========================================
94
+ # FastAPI App setup
95
+ # ==========================================
96
+ # Create DB tables
97
+ Base.metadata.create_all(bind=engine)
98
+
99
+ app = FastAPI(title="Task 1 API")
100
+
101
+ # --- Static files & Templates ---
102
+ BASE_DIR = Path(__file__).resolve().parent
103
+ app.mount("/static", StaticFiles(directory=str(BASE_DIR / "static")), name="static")
104
+ templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
105
+
106
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
107
+ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
108
+
109
+ # --- Helper Functions ---
110
+ def verify_password(plain_password, hashed_password):
111
+ return pwd_context.verify(plain_password[:72], hashed_password)
112
+
113
+ def get_password_hash(password):
114
+ return pwd_context.hash(password[:72])
115
+
116
+ def get_user_by_email(db: Session, email: str):
117
+ return db.query(User).filter(User.email == email).first()
118
+
119
+ # ==========================================
120
+ # API Endpoints
121
+ # ==========================================
122
+ @app.post("/signup", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
123
+ def signup(user: UserCreate, db: Session = Depends(get_db)):
124
+ db_user = get_user_by_email(db, email=user.email)
125
+ if db_user:
126
+ raise HTTPException(status_code=400, detail="Email already registered")
127
+
128
+ hashed_password = get_password_hash(user.password)
129
+
130
+ new_user = User(
131
+ name=user.name,
132
+ age=user.age,
133
+ address=user.address,
134
+ email=user.email,
135
+ mobile_number=user.mobile_number,
136
+ password_hash=hashed_password
137
+ )
138
+
139
+ db.add(new_user)
140
+ db.commit()
141
+ db.refresh(new_user)
142
+
143
+ return new_user
144
+
145
+ @app.post("/login", response_model=Token)
146
+ def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
147
+ # OAuth2PasswordRequestForm uses 'username' and 'password'
148
+ # We map 'username' to 'email' in our DB
149
+ user = get_user_by_email(db, email=form_data.username)
150
+ if not user:
151
+ raise HTTPException(
152
+ status_code=status.HTTP_401_UNAUTHORIZED,
153
+ detail="Invalid credentials",
154
+ headers={"WWW-Authenticate": "Bearer"},
155
+ )
156
+
157
+ if not verify_password(form_data.password, user.password_hash):
158
+ raise HTTPException(
159
+ status_code=status.HTTP_401_UNAUTHORIZED,
160
+ detail="Invalid credentials",
161
+ headers={"WWW-Authenticate": "Bearer"},
162
+ )
163
+
164
+ # In a real app we'd create a proper JWT token here
165
+ # For simplicity, returning a simple token string
166
+ access_token = f"fake-jwt-token-for-{user.email}"
167
+
168
+ return {"access_token": access_token, "token_type": "bearer"}
169
+
170
+ @app.get("/users/me", response_model=UserResponse)
171
+ def read_users_me(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
172
+ # This is a mock function since we aren't decoding real JWTs yet
173
+ email = token.replace("fake-jwt-token-for-", "")
174
+ user = get_user_by_email(db, email=email)
175
+ if user is None:
176
+ raise HTTPException(status_code=401, detail="Invalid token")
177
+ return user
178
+
179
+ # ==========================================
180
+ # File Handling Endpoints
181
+ # ==========================================
182
+
183
+ UPLOAD_DIR = "uploads"
184
+ os.makedirs(UPLOAD_DIR, exist_ok=True)
185
+ ALLOWED_EXTENSIONS = {".pdf", ".png", ".jpeg", ".jpg"}
186
+
187
+ from fastapi import UploadFile, File as FastAPIFile
188
+ from fastapi.responses import FileResponse
189
+ import shutil
190
+
191
+ @app.post("/upload")
192
+ async def upload_file(
193
+ file: UploadFile = FastAPIFile(...),
194
+ token: str = Depends(oauth2_scheme),
195
+ db: Session = Depends(get_db)
196
+ ):
197
+ # Authenticate user
198
+ email = token.replace("fake-jwt-token-for-", "")
199
+ user = get_user_by_email(db, email=email)
200
+ if not user:
201
+ raise HTTPException(status_code=401, detail="Invalid token")
202
+
203
+ # Validate file type
204
+ file_ext = os.path.splitext(file.filename)[1].lower()
205
+ if file_ext not in ALLOWED_EXTENSIONS:
206
+ raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}")
207
+
208
+ # Generate custom filename
209
+ custom_filename = f"{user.id}_{user.name.replace(' ', '_')}_{file.filename}"
210
+ file_path = os.path.join(UPLOAD_DIR, custom_filename)
211
+
212
+ # Save to disk
213
+ with open(file_path, "wb") as buffer:
214
+ shutil.copyfileobj(file.file, buffer)
215
+
216
+ # Save metadata to database
217
+ db_file = File(
218
+ filename=custom_filename,
219
+ file_type=file_ext,
220
+ user_id=user.id
221
+ )
222
+ db.add(db_file)
223
+ db.commit()
224
+ db.refresh(db_file)
225
+
226
+ return {"message": "File uploaded successfully", "file_id": db_file.id, "filename": custom_filename}
227
+
228
+
229
+ @app.get("/download/{file_id}")
230
+ async def download_file(
231
+ file_id: int,
232
+ token: str = Depends(oauth2_scheme),
233
+ db: Session = Depends(get_db)
234
+ ):
235
+ # Authenticate user
236
+ email = token.replace("fake-jwt-token-for-", "")
237
+ user = get_user_by_email(db, email=email)
238
+ if not user:
239
+ raise HTTPException(status_code=401, detail="Invalid token")
240
+
241
+ # Fetch file record (Ensuring users can only see their own files)
242
+ file_record = db.query(File).filter(File.id == file_id, File.user_id == user.id).first()
243
+ if not file_record:
244
+ raise HTTPException(status_code=404, detail="File not found")
245
+
246
+ file_path = os.path.join(UPLOAD_DIR, file_record.filename)
247
+ if not os.path.exists(file_path):
248
+ raise HTTPException(status_code=404, detail="File is missing on the server")
249
+
250
+ return FileResponse(path=file_path, filename=file_record.filename)
251
+
252
+
253
+ # --- File metadata response schema ---
254
+ class FileMetaResponse(BaseModel):
255
+ id: int
256
+ filename: str
257
+ file_type: str
258
+ upload_timestamp: Optional[datetime] = None
259
+
260
+ class Config:
261
+ from_attributes = True
262
+
263
+
264
+ @app.get("/files", response_model=List[FileMetaResponse])
265
+ async def list_files(
266
+ token: str = Depends(oauth2_scheme),
267
+ db: Session = Depends(get_db)
268
+ ):
269
+ """List all files belonging to the authenticated user."""
270
+ email = token.replace("fake-jwt-token-for-", "")
271
+ user = get_user_by_email(db, email=email)
272
+ if not user:
273
+ raise HTTPException(status_code=401, detail="Invalid token")
274
+
275
+ files = db.query(File).filter(File.user_id == user.id).all()
276
+ return files
277
+
278
+
279
+ @app.put("/files/{file_id}")
280
+ async def update_file(
281
+ file_id: int,
282
+ file: UploadFile = FastAPIFile(...),
283
+ token: str = Depends(oauth2_scheme),
284
+ db: Session = Depends(get_db)
285
+ ):
286
+ """Replace an existing file with a new upload."""
287
+ email = token.replace("fake-jwt-token-for-", "")
288
+ user = get_user_by_email(db, email=email)
289
+ if not user:
290
+ raise HTTPException(status_code=401, detail="Invalid token")
291
+
292
+ file_record = db.query(File).filter(File.id == file_id, File.user_id == user.id).first()
293
+ if not file_record:
294
+ raise HTTPException(status_code=404, detail="File not found")
295
+
296
+ # Validate new file type
297
+ file_ext = os.path.splitext(file.filename)[1].lower()
298
+ if file_ext not in ALLOWED_EXTENSIONS:
299
+ raise HTTPException(status_code=400, detail=f"Invalid file type. Allowed: {', '.join(ALLOWED_EXTENSIONS)}")
300
+
301
+ # Remove old physical file
302
+ old_path = os.path.join(UPLOAD_DIR, file_record.filename)
303
+ if os.path.exists(old_path):
304
+ os.remove(old_path)
305
+
306
+ # Save new file
307
+ custom_filename = f"{user.id}_{user.name.replace(' ', '_')}_{file.filename}"
308
+ new_path = os.path.join(UPLOAD_DIR, custom_filename)
309
+ with open(new_path, "wb") as buffer:
310
+ shutil.copyfileobj(file.file, buffer)
311
+
312
+ # Update DB record
313
+ file_record.filename = custom_filename
314
+ file_record.file_type = file_ext
315
+ db.commit()
316
+ db.refresh(file_record)
317
+
318
+ return {"message": "File updated successfully", "file_id": file_record.id, "filename": custom_filename}
319
+
320
+
321
+ @app.delete("/files/{file_id}")
322
+ async def delete_file(
323
+ file_id: int,
324
+ token: str = Depends(oauth2_scheme),
325
+ db: Session = Depends(get_db)
326
+ ):
327
+ """Delete a file record and its physical file."""
328
+ email = token.replace("fake-jwt-token-for-", "")
329
+ user = get_user_by_email(db, email=email)
330
+ if not user:
331
+ raise HTTPException(status_code=401, detail="Invalid token")
332
+
333
+ file_record = db.query(File).filter(File.id == file_id, File.user_id == user.id).first()
334
+ if not file_record:
335
+ raise HTTPException(status_code=404, detail="File not found")
336
+
337
+ # Remove physical file
338
+ file_path = os.path.join(UPLOAD_DIR, file_record.filename)
339
+ if os.path.exists(file_path):
340
+ os.remove(file_path)
341
+
342
+ db.delete(file_record)
343
+ db.commit()
344
+
345
+ return {"message": "File deleted successfully"}
346
+
347
+
348
+ # ==========================================================
349
+ # HTML Page Routes (Jinja2 Templates + cookie-based session)
350
+ # ==========================================================
351
+
352
+ def _get_current_user_from_cookie(request: Request, db: Session):
353
+ """Read the auth token from a cookie and return the User or None."""
354
+ token = request.cookies.get("access_token", "")
355
+ if not token:
356
+ return None
357
+ email = token.replace("fake-jwt-token-for-", "")
358
+ return get_user_by_email(db, email=email)
359
+
360
+
361
+ @app.get("/")
362
+ def root_redirect():
363
+ return RedirectResponse(url="/login", status_code=302)
364
+
365
+
366
+ # ---- Sign-Up Page ----
367
+ @app.get("/signup")
368
+ def page_signup(request: Request):
369
+ return templates.TemplateResponse("signup.html", {"request": request})
370
+
371
+
372
+ @app.post("/signup")
373
+ def page_signup_post(
374
+ request: Request,
375
+ name: str = Form(...),
376
+ age: int = Form(...),
377
+ address: str = Form(...),
378
+ email: str = Form(...),
379
+ mobile_number: str = Form(...),
380
+ password: str = Form(...),
381
+ db: Session = Depends(get_db),
382
+ ):
383
+ existing = get_user_by_email(db, email=email)
384
+ if existing:
385
+ return templates.TemplateResponse("signup.html", {
386
+ "request": request,
387
+ "message": "Email already registered",
388
+ "msg_type": "error",
389
+ })
390
+
391
+ new_user = User(
392
+ name=name, age=age, address=address,
393
+ email=email, mobile_number=mobile_number,
394
+ password_hash=get_password_hash(password),
395
+ )
396
+ db.add(new_user)
397
+ db.commit()
398
+
399
+ response = RedirectResponse(url="/login", status_code=302)
400
+ return response
401
+
402
+
403
+ # ---- Login Page ----
404
+ @app.get("/login")
405
+ def page_login(request: Request):
406
+ return templates.TemplateResponse("login.html", {"request": request})
407
+
408
+
409
+ @app.post("/login")
410
+ def page_login_post(
411
+ request: Request,
412
+ email: str = Form(...),
413
+ password: str = Form(...),
414
+ db: Session = Depends(get_db),
415
+ ):
416
+ user = get_user_by_email(db, email=email)
417
+ if not user or not verify_password(password, user.password_hash):
418
+ return templates.TemplateResponse("login.html", {
419
+ "request": request,
420
+ "message": "Invalid email or password",
421
+ "msg_type": "error",
422
+ })
423
+
424
+ token = f"fake-jwt-token-for-{user.email}"
425
+ response = RedirectResponse(url="/dashboard", status_code=302)
426
+ response.set_cookie(key="access_token", value=token, httponly=True)
427
+ return response
428
+
429
+
430
+ # ---- Dashboard ----
431
+ @app.get("/dashboard")
432
+ def page_dashboard(request: Request, db: Session = Depends(get_db)):
433
+ user = _get_current_user_from_cookie(request, db)
434
+ if not user:
435
+ return RedirectResponse(url="/login", status_code=302)
436
+
437
+ files = db.query(File).filter(File.user_id == user.id).all()
438
+ return templates.TemplateResponse("dashboard.html", {
439
+ "request": request,
440
+ "user": user,
441
+ "files": files,
442
+ })
443
+
444
+
445
+ # ---- Upload from browser ----
446
+ @app.post("/upload")
447
+ async def page_upload(
448
+ request: Request,
449
+ file1: UploadFile = FastAPIFile(...),
450
+ file2: Optional[UploadFile] = FastAPIFile(None),
451
+ db: Session = Depends(get_db),
452
+ ):
453
+ user = _get_current_user_from_cookie(request, db)
454
+ if not user:
455
+ return RedirectResponse(url="/login", status_code=302)
456
+
457
+ uploaded = 0
458
+ for f in [file1, file2]:
459
+ if f is None or f.filename == "":
460
+ continue
461
+ file_ext = os.path.splitext(f.filename)[1].lower()
462
+ if file_ext not in ALLOWED_EXTENSIONS:
463
+ continue # silently skip invalid types
464
+ custom_filename = f"{user.id}_{user.name.replace(' ', '_')}_{f.filename}"
465
+ dest = os.path.join(UPLOAD_DIR, custom_filename)
466
+ with open(dest, "wb") as buf:
467
+ shutil.copyfileobj(f.file, buf)
468
+ db_file = File(filename=custom_filename, file_type=file_ext, user_id=user.id)
469
+ db.add(db_file)
470
+ uploaded += 1
471
+
472
+ db.commit()
473
+ return RedirectResponse(url="/dashboard", status_code=302)
474
+
475
+
476
+ # ---- Download from browser ----
477
+ @app.get("/download/{file_id}")
478
+ async def page_download(file_id: int, request: Request, db: Session = Depends(get_db)):
479
+ user = _get_current_user_from_cookie(request, db)
480
+ if not user:
481
+ return RedirectResponse(url="/login", status_code=302)
482
+
483
+ file_record = db.query(File).filter(File.id == file_id, File.user_id == user.id).first()
484
+ if not file_record:
485
+ return RedirectResponse(url="/dashboard", status_code=302)
486
+
487
+ file_path = os.path.join(UPLOAD_DIR, file_record.filename)
488
+ if not os.path.exists(file_path):
489
+ return RedirectResponse(url="/dashboard", status_code=302)
490
+
491
+ from fastapi.responses import FileResponse as FR
492
+ return FR(path=file_path, filename=file_record.filename)
493
+
494
+
495
+ # ---- Delete from browser ----
496
+ @app.get("/delete/{file_id}")
497
+ def page_delete(file_id: int, request: Request, db: Session = Depends(get_db)):
498
+ user = _get_current_user_from_cookie(request, db)
499
+ if not user:
500
+ return RedirectResponse(url="/login", status_code=302)
501
+
502
+ file_record = db.query(File).filter(File.id == file_id, File.user_id == user.id).first()
503
+ if file_record:
504
+ file_path = os.path.join(UPLOAD_DIR, file_record.filename)
505
+ if os.path.exists(file_path):
506
+ os.remove(file_path)
507
+ db.delete(file_record)
508
+ db.commit()
509
+
510
+ return RedirectResponse(url="/dashboard", status_code=302)
511
+
512
+
513
+ # ---- Logout ----
514
+ @app.get("/logout")
515
+ def page_logout():
516
+ response = RedirectResponse(url="/login", status_code=302)
517
+ response.delete_cookie("access_token")
518
+ return response
backend/requirements.txt ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==25.1.0
2
+ annotated-doc==0.0.4
3
+ annotated-types==0.7.0
4
+ anyio==4.12.1
5
+ bcrypt==4.0.1
6
+ certifi==2026.2.25
7
+ cffi==2.0.0
8
+ charset-normalizer==3.4.4
9
+ click==8.3.1
10
+ colorama==0.4.6
11
+ cryptography==46.0.5
12
+ dnspython==2.8.0
13
+ ecdsa==0.19.1
14
+ email-validator==2.3.0
15
+ fastapi==0.135.1
16
+ greenlet==3.3.2
17
+ h11==0.16.0
18
+ idna==3.11
19
+ Jinja2==3.1.6
20
+ MarkupSafe==3.0.3
21
+ mysql-connector-python==9.6.0
22
+ passlib==1.7.4
23
+ pyasn1==0.6.2
24
+ pycparser==3.0
25
+ pydantic==2.12.5
26
+ pydantic_core==2.41.5
27
+ PyMySQL==1.1.2
28
+ python-jose==3.5.0
29
+ python-multipart==0.0.22
30
+ requests==2.32.5
31
+ rsa==4.9.1
32
+ six==1.17.0
33
+ SQLAlchemy==2.0.47
34
+ starlette==0.52.1
35
+ typing-inspection==0.4.2
36
+ typing_extensions==4.15.0
37
+ urllib3==2.6.3
38
+ uvicorn==0.41.0
backend/static/style.css ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ================================================
2
+ Task 1 — Design System
3
+ ================================================ */
4
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
5
+
6
+ :root {
7
+ --bg-primary: #0f0f1a;
8
+ --bg-card: rgba(255,255,255,0.05);
9
+ --bg-card-hover:rgba(255,255,255,0.08);
10
+ --accent: #6c63ff;
11
+ --accent-glow: rgba(108,99,255,0.35);
12
+ --success: #2ecc71;
13
+ --danger: #e74c3c;
14
+ --text: #e8e8f0;
15
+ --text-muted: #8888a8;
16
+ --border: rgba(255,255,255,0.08);
17
+ --radius: 14px;
18
+ --transition: .25s cubic-bezier(.4,0,.2,1);
19
+ }
20
+
21
+ *, *::before, *::after { box-sizing: border-box; margin:0; padding:0; }
22
+
23
+ body {
24
+ font-family: 'Inter', system-ui, sans-serif;
25
+ background: var(--bg-primary);
26
+ background-image:
27
+ radial-gradient(ellipse 80% 60% at 50% -20%, rgba(108,99,255,.15), transparent),
28
+ radial-gradient(ellipse 60% 50% at 80% 100%, rgba(99,200,255,.08), transparent);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ line-height: 1.6;
32
+ }
33
+
34
+ a { color: var(--accent); text-decoration: none; transition: color var(--transition); }
35
+ a:hover { color: #8b83ff; }
36
+
37
+ /* ---- Navbar ---- */
38
+ .navbar {
39
+ display: flex; align-items: center; justify-content: space-between;
40
+ padding: 1rem 2rem;
41
+ background: rgba(15,15,26,.85);
42
+ backdrop-filter: blur(12px);
43
+ border-bottom: 1px solid var(--border);
44
+ position: sticky; top:0; z-index: 100;
45
+ }
46
+ .navbar .brand { font-size:1.25rem; font-weight:700; color: var(--accent); }
47
+ .navbar .nav-links { display:flex; gap:1.2rem; align-items:center; }
48
+ .navbar .nav-links a { font-size:.9rem; font-weight:500; color:var(--text-muted); }
49
+ .navbar .nav-links a:hover { color:var(--text); }
50
+ .navbar .user-badge {
51
+ background: var(--bg-card); padding:.35rem .9rem; border-radius:20px;
52
+ font-size:.85rem; color:var(--accent); border:1px solid var(--border);
53
+ }
54
+
55
+ /* ---- Container ---- */
56
+ .container { max-width:960px; margin:0 auto; padding:2rem 1.5rem; }
57
+
58
+ /* ---- Cards ---- */
59
+ .card {
60
+ background: var(--bg-card);
61
+ backdrop-filter: blur(16px);
62
+ border: 1px solid var(--border);
63
+ border-radius: var(--radius);
64
+ padding: 2.5rem;
65
+ transition: transform var(--transition), box-shadow var(--transition);
66
+ }
67
+ .card:hover {
68
+ transform: translateY(-2px);
69
+ box-shadow: 0 8px 32px rgba(0,0,0,.25);
70
+ }
71
+ .card-narrow { max-width:480px; margin:3rem auto; }
72
+
73
+ .card h2 {
74
+ font-size:1.6rem; font-weight:700; margin-bottom:.3rem;
75
+ background: linear-gradient(135deg, var(--accent), #63c6ff);
76
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
77
+ }
78
+ .card .subtitle { color:var(--text-muted); margin-bottom:1.8rem; font-size:.92rem; }
79
+
80
+ /* ---- Forms ---- */
81
+ .form-group { margin-bottom:1.15rem; }
82
+ .form-group label {
83
+ display:block; font-size:.82rem; font-weight:600;
84
+ color:var(--text-muted); margin-bottom:.35rem; text-transform:uppercase; letter-spacing:.05em;
85
+ }
86
+ .form-group input,
87
+ .form-group textarea,
88
+ .form-group select {
89
+ width:100%; padding:.7rem 1rem;
90
+ background: rgba(255,255,255,0.04);
91
+ border:1px solid var(--border); border-radius:8px;
92
+ color:var(--text); font-size:.95rem; font-family:inherit;
93
+ transition: border-color var(--transition), box-shadow var(--transition);
94
+ }
95
+ .form-group input:focus,
96
+ .form-group textarea:focus {
97
+ outline:none; border-color:var(--accent);
98
+ box-shadow: 0 0 0 3px var(--accent-glow);
99
+ }
100
+ .form-group textarea { resize:vertical; min-height:70px; }
101
+
102
+ .form-row { display:grid; grid-template-columns:1fr 1fr; gap:0 1rem; }
103
+
104
+ /* ---- Buttons ---- */
105
+ .btn {
106
+ display:inline-flex; align-items:center; justify-content:center; gap:.4rem;
107
+ padding:.72rem 1.6rem; border:none; border-radius:8px;
108
+ font-family:inherit; font-size:.92rem; font-weight:600;
109
+ cursor:pointer; transition: all var(--transition);
110
+ }
111
+ .btn-primary {
112
+ background: linear-gradient(135deg, var(--accent), #4f46e5);
113
+ color:#fff; box-shadow: 0 4px 15px var(--accent-glow);
114
+ }
115
+ .btn-primary:hover { transform:translateY(-1px); box-shadow:0 6px 20px var(--accent-glow); }
116
+
117
+ .btn-danger { background:var(--danger); color:#fff; }
118
+ .btn-danger:hover { background:#c0392b; }
119
+
120
+ .btn-sm { padding:.4rem .9rem; font-size:.82rem; border-radius:6px; }
121
+
122
+ .btn-outline {
123
+ background:transparent; border:1px solid var(--border); color:var(--text-muted);
124
+ }
125
+ .btn-outline:hover { border-color:var(--accent); color:var(--accent); }
126
+
127
+ /* ---- Flash Messages ---- */
128
+ .flash {
129
+ padding:.85rem 1.2rem; border-radius:8px; margin-bottom:1.5rem;
130
+ font-size:.9rem; font-weight:500;
131
+ animation: slideDown .35s ease-out;
132
+ }
133
+ .flash-success { background:rgba(46,204,113,.12); border:1px solid rgba(46,204,113,.3); color:var(--success); }
134
+ .flash-error { background:rgba(231,76,60,.12); border:1px solid rgba(231,76,60,.3); color:var(--danger); }
135
+
136
+ @keyframes slideDown {
137
+ from { opacity:0; transform:translateY(-10px); }
138
+ to { opacity:1; transform:translateY(0); }
139
+ }
140
+
141
+ /* ---- File Upload Area ---- */
142
+ .upload-area {
143
+ border:2px dashed var(--border); border-radius:var(--radius);
144
+ padding:2rem; text-align:center; cursor:pointer;
145
+ transition: border-color var(--transition), background var(--transition);
146
+ }
147
+ .upload-area:hover { border-color:var(--accent); background:rgba(108,99,255,.04); }
148
+ .upload-area .icon { font-size:2rem; margin-bottom:.5rem; }
149
+ .upload-area p { color:var(--text-muted); font-size:.88rem; }
150
+ .upload-area input[type="file"] { display:none; }
151
+
152
+ .file-inputs { display:flex; gap:1rem; margin-top:1rem; flex-wrap:wrap; }
153
+ .file-input-wrapper {
154
+ flex:1; min-width:200px;
155
+ background:rgba(255,255,255,.03); border:1px solid var(--border);
156
+ border-radius:8px; padding:.6rem 1rem;
157
+ display:flex; align-items:center; gap:.5rem;
158
+ font-size:.88rem; color:var(--text-muted);
159
+ }
160
+ .file-input-wrapper input[type="file"] { flex:1; color:var(--text-muted); font-size:.85rem; }
161
+ .file-input-wrapper input[type="file"]::file-selector-button {
162
+ background:var(--accent); color:#fff; border:none; padding:.3rem .7rem;
163
+ border-radius:5px; font-size:.8rem; cursor:pointer; margin-right:.5rem;
164
+ }
165
+
166
+ /* ---- Table ---- */
167
+ .table-wrap { overflow-x:auto; margin-top:1.5rem; }
168
+ table { width:100%; border-collapse:collapse; }
169
+ thead th {
170
+ text-align:left; padding:.75rem 1rem; font-size:.78rem; font-weight:600;
171
+ color:var(--text-muted); text-transform:uppercase; letter-spacing:.06em;
172
+ border-bottom:1px solid var(--border);
173
+ }
174
+ tbody td {
175
+ padding:.75rem 1rem; font-size:.9rem;
176
+ border-bottom:1px solid var(--border);
177
+ }
178
+ tbody tr { transition: background var(--transition); }
179
+ tbody tr:hover { background:var(--bg-card-hover); }
180
+
181
+ .badge {
182
+ display:inline-block; padding:.15rem .55rem; border-radius:4px;
183
+ font-size:.75rem; font-weight:600; text-transform:uppercase;
184
+ }
185
+ .badge-pdf { background:rgba(231,76,60,.15); color:#e74c3c; }
186
+ .badge-png { background:rgba(46,204,113,.15); color:#2ecc71; }
187
+ .badge-jpg { background:rgba(243,156,18,.15); color:#f39c12; }
188
+ .badge-jpeg { background:rgba(243,156,18,.15); color:#f39c12; }
189
+
190
+ .actions { display:flex; gap:.5rem; }
191
+
192
+ /* ---- Empty state ---- */
193
+ .empty-state {
194
+ text-align:center; padding:3rem 1rem; color:var(--text-muted);
195
+ }
196
+ .empty-state .icon { font-size:3rem; margin-bottom:.8rem; opacity:.5; }
197
+
198
+ /* ---- Footer text ---- */
199
+ .form-footer { text-align:center; margin-top:1.4rem; font-size:.88rem; color:var(--text-muted); }
200
+
201
+ /* ---- Responsive ---- */
202
+ @media (max-width:600px) {
203
+ .form-row { grid-template-columns:1fr; }
204
+ .card-narrow { margin:1.5rem 1rem; padding:1.5rem; }
205
+ .navbar { padding:.8rem 1rem; }
206
+ .file-inputs { flex-direction:column; }
207
+ }
backend/templates/base.html ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="description" content="Task 1 — Full-Stack File Management Application">
7
+ <title>{% block title %}Task 1 App{% endblock %}</title>
8
+ <link rel="stylesheet" href="/static/style.css">
9
+ </head>
10
+ <body>
11
+
12
+ <!-- Navbar -->
13
+ <nav class="navbar">
14
+ <a href="/dashboard" class="brand">📁 Task 1</a>
15
+ <div class="nav-links">
16
+ {% if user %}
17
+ <span class="user-badge">👤 {{ user.name }}</span>
18
+ <a href="/logout">Logout</a>
19
+ {% else %}
20
+ <a href="/login">Login</a>
21
+ <a href="/signup">Sign Up</a>
22
+ {% endif %}
23
+ </div>
24
+ </nav>
25
+
26
+ <!-- Flash Messages -->
27
+ <div class="container">
28
+ {% if message %}
29
+ <div class="flash flash-{{ msg_type | default('success') }}">{{ message }}</div>
30
+ {% endif %}
31
+
32
+ {% block content %}{% endblock %}
33
+ </div>
34
+
35
+ </body>
36
+ </html>
backend/templates/dashboard.html ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Dashboard — Task 1{% endblock %}
3
+
4
+ {% block content %}
5
+
6
+ <!-- Upload Card -->
7
+ <div class="card" style="margin-bottom:2rem;">
8
+ <h2>Upload Files</h2>
9
+ <p class="subtitle">Accepted formats: PDF, PNG, JPEG (max 2 files at a time)</p>
10
+
11
+ <form method="POST" action="/upload" enctype="multipart/form-data" id="upload-form">
12
+ <div class="file-inputs">
13
+ <div class="file-input-wrapper">
14
+ <span>📄 File 1</span>
15
+ <input type="file" name="file1" accept=".pdf,.png,.jpg,.jpeg" required>
16
+ </div>
17
+ </div>
18
+ <button type="submit" class="btn btn-primary" style="margin-top:1.2rem;">⬆ Upload</button>
19
+ </form>
20
+ </div>
21
+
22
+ <!-- File List Card -->
23
+ <div class="card">
24
+ <h2>Your Files</h2>
25
+ <p class="subtitle">{{ files | length }} file{{ 's' if files | length != 1 else '' }} stored</p>
26
+
27
+ {% if files %}
28
+ <div class="table-wrap">
29
+ <table>
30
+ <thead>
31
+ <tr>
32
+ <th>#</th>
33
+ <th>Filename</th>
34
+ <th>Type</th>
35
+ <th>Uploaded</th>
36
+ <th>Actions</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ {% for f in files %}
41
+ <tr>
42
+ <td>{{ loop.index }}</td>
43
+ <td>{{ f.filename }}</td>
44
+ <td>
45
+ {% set ext = f.file_type | replace('.','') %}
46
+ <span class="badge badge-{{ ext }}">{{ ext | upper }}</span>
47
+ </td>
48
+ <td>{{ f.upload_timestamp.strftime('%d %b %Y, %H:%M') if f.upload_timestamp else '—' }}</td>
49
+ <td class="actions">
50
+ <a href="/download/{{ f.id }}" class="btn btn-outline btn-sm">⬇ Download</a>
51
+ <form action="/delete/{{ f.id }}" method="get" style="display:inline;"
52
+ onsubmit="return confirm('Delete this file?')">
53
+ <button type="submit" class="btn btn-danger btn-sm">🗑 Delete</button>
54
+ </form>
55
+ </td>
56
+ </tr>
57
+ {% endfor %}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ {% else %}
62
+ <div class="empty-state">
63
+ <div class="icon">📂</div>
64
+ <p>No files yet — upload your first file above!</p>
65
+ </div>
66
+ {% endif %}
67
+ </div>
68
+
69
+ {% endblock %}
backend/templates/login.html ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Login — Task 1{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="card card-narrow">
6
+ <h2>Welcome Back</h2>
7
+ <p class="subtitle">Sign in to access your dashboard</p>
8
+
9
+ <form method="POST" action="/login" id="login-form">
10
+ <div class="form-group">
11
+ <label for="email">Email</label>
12
+ <input type="email" id="email" name="email" placeholder="you@example.com" required>
13
+ </div>
14
+
15
+ <div class="form-group">
16
+ <label for="password">Password</label>
17
+ <input type="password" id="password" name="password" placeholder="••••••••" required>
18
+ </div>
19
+
20
+ <button type="submit" class="btn btn-primary" style="width:100%; margin-top:.5rem;">Log In</button>
21
+ </form>
22
+
23
+ <p class="form-footer">Don't have an account? <a href="/signup">Sign up</a></p>
24
+ </div>
25
+ {% endblock %}
backend/templates/signup.html ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+ {% block title %}Sign Up — Task 1{% endblock %}
3
+
4
+ {% block content %}
5
+ <div class="card card-narrow">
6
+ <h2>Create Account</h2>
7
+ <p class="subtitle">Fill in your details to get started</p>
8
+
9
+ <form method="POST" action="/signup" id="signup-form">
10
+ <div class="form-group">
11
+ <label for="name">Full Name</label>
12
+ <input type="text" id="name" name="name" placeholder="John Doe" required>
13
+ </div>
14
+
15
+ <div class="form-row">
16
+ <div class="form-group">
17
+ <label for="age">Age</label>
18
+ <input type="number" id="age" name="age" placeholder="25" min="1" max="150" required>
19
+ </div>
20
+ <div class="form-group">
21
+ <label for="mobile_number">Mobile Number</label>
22
+ <input type="tel" id="mobile_number" name="mobile_number" placeholder="9876543210" required>
23
+ </div>
24
+ </div>
25
+
26
+ <div class="form-group">
27
+ <label for="address">Address</label>
28
+ <textarea id="address" name="address" placeholder="123 Main Street, City" required></textarea>
29
+ </div>
30
+
31
+ <div class="form-group">
32
+ <label for="email">Email</label>
33
+ <input type="email" id="email" name="email" placeholder="you@example.com" required>
34
+ </div>
35
+
36
+ <div class="form-group">
37
+ <label for="password">Password</label>
38
+ <input type="password" id="password" name="password" placeholder="••••••••" minlength="6" required>
39
+ </div>
40
+
41
+ <button type="submit" class="btn btn-primary" style="width:100%; margin-top:.5rem;">Sign Up</button>
42
+ </form>
43
+
44
+ <p class="form-footer">Already have an account? <a href="/login">Log in</a></p>
45
+ </div>
46
+ {% endblock %}
task.md ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Full-Stack Developer Pre-Interview
2
+ Assessment
3
+
4
+ Objective
5
+
6
+ The purpose of this assessment is to evaluate your ability to design, develop, host, and
7
+ deliver a full-stack web application using Python for the backend, MySQL for the
8
+ database, and a frontend stack of your choice.
9
+
10
+ You will be assessed on functionality, deployment skills, code quality, database design,
11
+ and documentation.
12
+
13
+ Requirements
14
+
15
+ 1. Frontend
16
+
17
+ . You may use any frontend technology (HTML/CSS/JS, React, Angular, Vue, etc.).
18
+ . The application should include:
19
+ Sign-Up Page
20
+ o Fields: Name, Age, Address, Email, Mobile Number, Password.
21
+ o On successful signup, details must be stored in MySQL.
22
+ o After signup, redirect to the login page.
23
+ Login Page
24
+ o Validate credentials against data stored in MySQL.
25
+ o On success, redirect to the main dashboard.
26
+
27
+ 2. Backend
28
+
29
+ . Must be written only in Python.
30
+ . You may use Flask, Django, FastAPI, or any Python web framework.
31
+ . Must handle:
32
+ Sign-up -> Store user details in MySQL.
33
+ Login -> Validate credentials.
34
+ File download -> Provide a test file for download (static or generated).
35
+ File upload -> Two fields, restricted to PDF, PNG, or JPEG.
36
+
37
+ 3. File Handling
38
+
39
+ . Uploaded files should be:
40
+ Restricted to PDF, PNG, or JPEG.
41
+ Stored in a directory on the server where the backend is hosted.
42
+ Saved with filenames including the user's name (from login).
43
+
44
+ 4. Database
45
+
46
+ . Must use MySQL.
47
+ . Should store:
48
+ User details (from sign-up).
49
+ Uploaded file metadata (filename, file type, upload timestamp, user
50
+ association).
51
+
52
+ Non-Functional Requirements
53
+
54
+ . Code must be modular, clean, and documented.
55
+ . Include error handling for invalid inputs and file types.
56
+ . Apply basic security best practices (password hashing, input validation).
57
+ . The application must be hosted on a platform of your choice (e.g., Vercel, Netify,
58
+ Render, AWS, GCP, Azure etc.).
59
+ . The application must be publicly accessible through a link.
60
+
61
+ Deliverables
62
+
63
+ 1. GitHub Repository containing:
64
+ Source code (frontend + backend).
65
+ Database schema (SQL file or migration).
66
+ A README.md with:
67
+ o Local setup instructions.
68
+ o Hosting details (platform used + deployed link).
69
+ o Database setup.
70
+ o Any assumptions made.
71
+ 2. Publicly accessible application URL (hosted version).
72
+
73
+ Time Limit
74
+
75
+ You have 3 days to complete and submit your solution.
76
+
77
+ Submission Guidelines
78
+
79
+ . Share the GitHub repository link.
80
+ . Share the hosted application link (must be publicly accessible).
81
+ . Ensure setup instructions are clear for both local and hosted versions.
82
+
83
+ Also, please refer to the videos provided in the following link, as we may ask questions
84
+ based on them during the interview.
85
+
86
+ https://documents.softmania.in/external/manual/appendix/article/links?p=8925397ddf
87
+ 335351a1488377eefdb7c5522becd5be0e42b8c4706fc7e92f8faf
uploads/4_rsna_13_2_feb.pdf ADDED
Binary file (84.2 kB). View file