Initial commit, working.
This commit is contained in:
commit
67cf0e1cea
2
retoors/MANIFEST.in
Normal file
2
retoors/MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
recursive-include retoors/static *
|
||||
recursive-include retoors/templates *
|
||||
22
retoors/Makefile
Normal file
22
retoors/Makefile
Normal file
@ -0,0 +1,22 @@
|
||||
.PHONY: install run test coverage clean all
|
||||
|
||||
install:
|
||||
pip install -r requirements.txt
|
||||
|
||||
run:
|
||||
python -m retoors.main
|
||||
|
||||
test:
|
||||
pytest
|
||||
|
||||
coverage:
|
||||
pytest --cov=retoors --cov-report=term-missing --cov-report=html
|
||||
|
||||
clean:
|
||||
find . -type f -name "*.pyc" -delete
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||
find . -type d -name ".pytest_cache" -exec rm -rf {} +
|
||||
rm -f .coverage
|
||||
rm -rf htmlcov
|
||||
|
||||
all: install test
|
||||
3
retoors/pytest.ini
Normal file
3
retoors/pytest.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
pythonpath = .
|
||||
asyncio_mode = auto
|
||||
7
retoors/requirements.txt
Normal file
7
retoors/requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
aiohttp
|
||||
jinja2
|
||||
aiohttp_session
|
||||
bcrypt
|
||||
pytest
|
||||
pytest-aiohttp
|
||||
aiohttp-test-utils
|
||||
0
retoors/retoors/__init__.py
Normal file
0
retoors/retoors/__init__.py
Normal file
12
retoors/retoors/helpers/auth.py
Normal file
12
retoors/retoors/helpers/auth.py
Normal file
@ -0,0 +1,12 @@
|
||||
from functools import wraps
|
||||
from aiohttp import web
|
||||
from aiohttp_session import get_session
|
||||
|
||||
def login_required(func):
|
||||
@wraps(func)
|
||||
async def wrapper(self, *args, **kwargs):
|
||||
session = await get_session(self.request)
|
||||
if 'user_email' not in session:
|
||||
return web.HTTPFound('/login')
|
||||
return await func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
61
retoors/retoors/main.py
Normal file
61
retoors/retoors/main.py
Normal file
@ -0,0 +1,61 @@
|
||||
from aiohttp import web
|
||||
import aiohttp_jinja2
|
||||
import jinja2
|
||||
from pathlib import Path
|
||||
import aiohttp_session
|
||||
from aiohttp_session import setup as setup_session
|
||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||
import os
|
||||
|
||||
from .routes import setup_routes
|
||||
from .services.user_service import UserService
|
||||
from .services.config_service import ConfigService
|
||||
from .middlewares import user_middleware, error_middleware
|
||||
|
||||
|
||||
async def setup_services(app: web.Application):
|
||||
base_path = Path(__file__).parent
|
||||
data_path = base_path.parent / "data"
|
||||
app["user_service"] = UserService(data_path / "users.json")
|
||||
app["config_service"] = ConfigService(data_path / "config.json")
|
||||
yield
|
||||
|
||||
|
||||
def create_app():
|
||||
app = web.Application()
|
||||
|
||||
# The order of middleware registration matters.
|
||||
# They are executed in the order they are added.
|
||||
app.middlewares.append(error_middleware)
|
||||
|
||||
# Setup session
|
||||
secret_key = os.urandom(32)
|
||||
setup_session(app, EncryptedCookieStorage(secret_key))
|
||||
|
||||
app.middlewares.append(user_middleware)
|
||||
|
||||
# Setup paths
|
||||
base_path = Path(__file__).parent
|
||||
static_path = base_path / "static"
|
||||
templates_path = base_path / "templates"
|
||||
|
||||
# Setup Jinja2
|
||||
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(str(templates_path)))
|
||||
|
||||
# Setup services
|
||||
app.cleanup_ctx.append(setup_services)
|
||||
|
||||
# Setup routes
|
||||
setup_routes(app)
|
||||
app.router.add_static("/static", path=str(static_path), name="static")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def main():
|
||||
app = create_app()
|
||||
web.run_app(app)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
23
retoors/retoors/middlewares.py
Normal file
23
retoors/retoors/middlewares.py
Normal file
@ -0,0 +1,23 @@
|
||||
from aiohttp import web
|
||||
from aiohttp_session import get_session
|
||||
import aiohttp_jinja2
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def user_middleware(request, handler):
|
||||
session = await get_session(request)
|
||||
request["user"] = None
|
||||
if "user_email" in session:
|
||||
user_service = request.app["user_service"]
|
||||
request["user"] = user_service.get_user_by_email(session["user_email"])
|
||||
return await handler(request)
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def error_middleware(request, handler):
|
||||
try:
|
||||
return await handler(request)
|
||||
except web.HTTPException as ex:
|
||||
raise # Re-raise HTTPException to see original traceback
|
||||
except Exception:
|
||||
raise # Re-raise generic Exception to see original traceback
|
||||
9
retoors/retoors/models.py
Normal file
9
retoors/retoors/models.py
Normal file
@ -0,0 +1,9 @@
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
class RegistrationModel(BaseModel):
|
||||
username: str = Field(min_length=3, max_length=50)
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=8)
|
||||
|
||||
class QuotaUpdateModel(BaseModel):
|
||||
storage_amount: float = Field(gt=0, le=1000)
|
||||
11
retoors/retoors/routes.py
Normal file
11
retoors/retoors/routes.py
Normal file
@ -0,0 +1,11 @@
|
||||
from .views.auth import LoginView, RegistrationView, LogoutView
|
||||
from .views.site import SiteView, OrderView
|
||||
|
||||
|
||||
def setup_routes(app):
|
||||
app.router.add_view("/login", LoginView, name="login")
|
||||
app.router.add_view("/register", RegistrationView, name="register")
|
||||
app.router.add_view("/logout", LogoutView, name="logout")
|
||||
app.router.add_view("/", SiteView, name="index")
|
||||
app.router.add_view("/dashboard", SiteView, name="dashboard")
|
||||
app.router.add_view("/order", OrderView, name="order")
|
||||
0
retoors/retoors/services/__init__.py
Normal file
0
retoors/retoors/services/__init__.py
Normal file
16
retoors/retoors/services/config_service.py
Normal file
16
retoors/retoors/services/config_service.py
Normal file
@ -0,0 +1,16 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
class ConfigService:
|
||||
def __init__(self, config_path: Path):
|
||||
self._config_path = config_path
|
||||
self._config = self._load_config()
|
||||
|
||||
def _load_config(self):
|
||||
if not self._config_path.exists():
|
||||
return {}
|
||||
with open(self._config_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def get_price_per_gb(self) -> float:
|
||||
return self._config.get("price_per_gb", 0.0)
|
||||
54
retoors/retoors/services/user_service.py
Normal file
54
retoors/retoors/services/user_service.py
Normal file
@ -0,0 +1,54 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Dict, Optional, Any
|
||||
import hashlib
|
||||
|
||||
class UserService:
|
||||
def __init__(self, users_path: Path):
|
||||
self._users_path = users_path
|
||||
self._users = self._load_users()
|
||||
|
||||
def _load_users(self) -> List[Dict[str, Any]]:
|
||||
if not self._users_path.exists():
|
||||
return []
|
||||
with open(self._users_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def _save_users(self):
|
||||
with open(self._users_path, "w") as f:
|
||||
json.dump(self._users, f, indent=4)
|
||||
|
||||
def get_user_by_email(self, email: str) -> Optional[Dict[str, Any]]:
|
||||
return next((user for user in self._users if user["email"] == email), None)
|
||||
|
||||
def create_user(self, username: str, email: str, password: str) -> Dict[str, Any]:
|
||||
if self.get_user_by_email(email):
|
||||
raise ValueError("User with this email already exists")
|
||||
|
||||
hashed_password = hashlib.sha256(password.encode()).hexdigest()
|
||||
user = {
|
||||
"username": username,
|
||||
"email": email,
|
||||
"password": hashed_password,
|
||||
"storage_quota_gb": 5, # Default quota
|
||||
"storage_used_gb": 0
|
||||
}
|
||||
self._users.append(user)
|
||||
self._save_users()
|
||||
return user
|
||||
|
||||
def authenticate_user(self, email: str, password: str) -> bool:
|
||||
user = self.get_user_by_email(email)
|
||||
if not user:
|
||||
return False
|
||||
hashed_password = hashlib.sha256(password.encode()).hexdigest()
|
||||
return user["password"] == hashed_password
|
||||
|
||||
def update_user_quota(self, email: str, new_quota_gb: float):
|
||||
user = self.get_user_by_email(email)
|
||||
if not user:
|
||||
raise ValueError("User not found")
|
||||
|
||||
user["storage_quota_gb"] = new_quota_gb
|
||||
self._save_users()
|
||||
|
||||
64
retoors/retoors/static/css/base.css
Normal file
64
retoors/retoors/static/css/base.css
Normal file
@ -0,0 +1,64 @@
|
||||
:root {
|
||||
--primary-color: #0A2540;
|
||||
--secondary-color: #6c757d;
|
||||
--background-color: #F8F5F2;
|
||||
--text-color: #343a40;
|
||||
--container-bg-color: #ffffff;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
margin: 0;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
section {
|
||||
background-color: var(--container-bg-color);
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 2rem;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.hero {
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
color: var(--primary-color);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
0
retoors/retoors/static/css/components/button.css
Normal file
0
retoors/retoors/static/css/components/button.css
Normal file
9
retoors/retoors/static/css/components/custom.css
Normal file
9
retoors/retoors/static/css/components/custom.css
Normal file
@ -0,0 +1,9 @@
|
||||
.error {
|
||||
color: #dc3545;
|
||||
background-color: #f8d7da;
|
||||
border-color: #f5c6cb;
|
||||
padding: .75rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
54
retoors/retoors/static/css/components/dashboard.css
Normal file
54
retoors/retoors/static/css/components/dashboard.css
Normal file
@ -0,0 +1,54 @@
|
||||
.dashboard-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.storage-overview {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.storage-gauge {
|
||||
background-color: #e9ecef;
|
||||
border-radius: .25rem;
|
||||
height: 20px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.storage-gauge-bar {
|
||||
background-color: var(--primary-color);
|
||||
height: 100%;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
.storage-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.9rem;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.file-list table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.file-list th, .file-list td {
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.file-list th {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.order-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.price-display {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
22
retoors/retoors/static/css/components/form.css
Normal file
22
retoors/retoors/static/css/components/form.css
Normal file
@ -0,0 +1,22 @@
|
||||
.form-container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 1rem;
|
||||
}
|
||||
73
retoors/retoors/static/css/components/index.css
Normal file
73
retoors/retoors/static/css/components/index.css
Normal file
@ -0,0 +1,73 @@
|
||||
.hero h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
font-size: 1.25rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.features {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
gap: 2rem;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.feature-card img {
|
||||
height: 48px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.feature-card h2 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.feature-card p {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
#families {
|
||||
background-color: #E0F7FA;
|
||||
}
|
||||
|
||||
#professionals {
|
||||
background-color: #FFECB3;
|
||||
}
|
||||
|
||||
#students {
|
||||
background-color: #E8F5E9;
|
||||
}
|
||||
|
||||
.cta {
|
||||
text-align: center;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
padding-top: 2rem;
|
||||
}
|
||||
|
||||
.cta .btn-primary {
|
||||
background-color: #007BFF;
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.2rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
58
retoors/retoors/static/css/components/navigation.css
Normal file
58
retoors/retoors/static/css/components/navigation.css
Normal file
@ -0,0 +1,58 @@
|
||||
header {
|
||||
background-color: var(--container-bg-color);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid #EAEAEA;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
nav .logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav .logo img {
|
||||
height: 32px;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
nav ul li a {
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
nav ul li a.btn-nav {
|
||||
background-color: #007BFF;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #007BFF;
|
||||
}
|
||||
|
||||
nav ul li a.btn-nav:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
18
retoors/retoors/static/css/components/slider.css
Normal file
18
retoors/retoors/static/css/components/slider.css
Normal file
@ -0,0 +1,18 @@
|
||||
custom-slider {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
custom-slider .slider-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
custom-slider input[type="range"] {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
custom-slider .slider-value {
|
||||
font-weight: bold;
|
||||
}
|
||||
1
retoors/retoors/static/images/icon-families.svg
Normal file
1
retoors/retoors/static/images/icon-families.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#0A2540" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
|
||||
|
After Width: | Height: | Size: 396 B |
1
retoors/retoors/static/images/icon-professionals.svg
Normal file
1
retoors/retoors/static/images/icon-professionals.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#0A2540" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 469 B |
1
retoors/retoors/static/images/icon-students.svg
Normal file
1
retoors/retoors/static/images/icon-students.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#0A2540" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-award"><circle cx="12" cy="8" r="7"></circle><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 321 B |
4
retoors/retoors/static/images/logo.svg
Normal file
4
retoors/retoors/static/images/logo.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 0L0 12V32H32V12L16 0Z" fill="#0A2540"/>
|
||||
<path d="M24 24H8V12H24V24Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 200 B |
0
retoors/retoors/static/js/components/__init__.js
Normal file
0
retoors/retoors/static/js/components/__init__.js
Normal file
0
retoors/retoors/static/js/components/navigation.js
Normal file
0
retoors/retoors/static/js/components/navigation.js
Normal file
30
retoors/retoors/static/js/components/slider.js
Normal file
30
retoors/retoors/static/js/components/slider.js
Normal file
@ -0,0 +1,30 @@
|
||||
class CustomSlider extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.min = this.getAttribute('min') || 0;
|
||||
this.max = this.getAttribute('max') || 100;
|
||||
this.value = this.getAttribute('value') || 50;
|
||||
this.step = this.getAttribute('step') || 1;
|
||||
this.name = this.getAttribute('name') || 'slider';
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.innerHTML = `
|
||||
<div class="slider-container">
|
||||
<input type="range" min="${this.min}" max="${this.max}" value="${this.value}" step="${this.step}" name="${this.name}">
|
||||
<span class="slider-value">${this.value} GB</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.input = this.querySelector('input[type="range"]');
|
||||
this.valueDisplay = this.querySelector('.slider-value');
|
||||
|
||||
this.input.addEventListener('input', () => {
|
||||
this.value = this.input.value;
|
||||
this.valueDisplay.textContent = `${this.value} GB`;
|
||||
this.dispatchEvent(new CustomEvent('value-change', { detail: { value: this.value } }));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('custom-slider', CustomSlider);
|
||||
20
retoors/retoors/static/js/main.js
Normal file
20
retoors/retoors/static/js/main.js
Normal file
@ -0,0 +1,20 @@
|
||||
import './components/slider.js';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const slider = document.querySelector('custom-slider');
|
||||
const priceDisplay = document.getElementById('price-display');
|
||||
|
||||
if (slider && priceDisplay) {
|
||||
const pricePerGb = 0.5; // This should be fetched from the config
|
||||
|
||||
const updatePrice = () => {
|
||||
const value = slider.value;
|
||||
const price = (value * pricePerGb).toFixed(2);
|
||||
priceDisplay.textContent = `$${price}`;
|
||||
};
|
||||
|
||||
updatePrice();
|
||||
|
||||
slider.addEventListener('value-change', updatePrice);
|
||||
}
|
||||
});
|
||||
3
retoors/retoors/templates/components/footer.html
Normal file
3
retoors/retoors/templates/components/footer.html
Normal file
@ -0,0 +1,3 @@
|
||||
<footer>
|
||||
<p>© 2025 Retoors. All rights reserved.</p>
|
||||
</footer>
|
||||
21
retoors/retoors/templates/components/navigation.html
Normal file
21
retoors/retoors/templates/components/navigation.html
Normal file
@ -0,0 +1,21 @@
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/" class="logo">
|
||||
<img src="/static/images/logo.svg" alt="HomeBase Storage" />
|
||||
<span>HomeBase Storage</span>
|
||||
</a>
|
||||
<ul>
|
||||
<li><a href="#">Features</a></li>
|
||||
<li><a href="#">Pricing</a></li>
|
||||
<li><a href="#">Reviews</a></li>
|
||||
<li><a href="#">Support</a></li>
|
||||
{% if request['user'] %}
|
||||
<li><a href="/dashboard">Dashboard</a></li>
|
||||
<li><a href="/logout" class="btn-nav">Logout</a></li>
|
||||
{% else %}
|
||||
<li><a href="/login">Login</a></li>
|
||||
<li><a href="/register" class="btn-nav">Start Your Free Trial</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
22
retoors/retoors/templates/layouts/base.html
Normal file
22
retoors/retoors/templates/layouts/base.html
Normal file
@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Retoors Storage{% endblock %}</title>
|
||||
<link rel="stylesheet" href="/static/css/base.css">
|
||||
<link rel="stylesheet" href="/static/css/components/custom.css">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% include "components/navigation.html" %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
{% include "components/footer.html" %}
|
||||
|
||||
<script src="/static/js/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
74
retoors/retoors/templates/pages/dashboard.html
Normal file
74
retoors/retoors/templates/pages/dashboard.html
Normal file
@ -0,0 +1,74 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}Dashboard{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/components/slider.css">
|
||||
<link rel="stylesheet" href="/static/css/components/dashboard.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="dashboard-header">
|
||||
<h2>Welcome, {{ user.email }}</h2>
|
||||
</section>
|
||||
|
||||
<section class="storage-overview">
|
||||
<h3>Storage Overview</h3>
|
||||
<div class="storage-gauge">
|
||||
<div class="storage-gauge-bar" style="width: {{ (user.storage_used_gb / user.storage_quota_gb) * 100 }}%;"></div>
|
||||
</div>
|
||||
<div class="storage-info">
|
||||
<span>{{ user.storage_used_gb }} GB used of {{ user.storage_quota_gb }} GB</span>
|
||||
<span>{{ ((user.storage_used_gb / user.storage_quota_gb) * 100)|round(2) }}%</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>My Files</h2>
|
||||
<div class="file-list">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Size</th>
|
||||
<th>Last Modified</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>documents.zip</td>
|
||||
<td>256 MB</td>
|
||||
<td>2025-11-08</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>photos.zip</td>
|
||||
<td>1.2 GB</td>
|
||||
<td>2025-11-07</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>project_alpha.zip</td>
|
||||
<td>512 MB</td>
|
||||
<td>2025-11-05</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Order More Storage</h2>
|
||||
<form action="/order" method="post" class="order-form">
|
||||
<div class="form-group">
|
||||
<label for="storage_amount">Storage (GB)</label>
|
||||
<custom-slider min="5" max="1000" value="{{ user.storage_quota_gb }}" step="5" name="storage_amount"></custom-slider>
|
||||
{% if errors.storage_amount %}
|
||||
<p class="error">{{ errors.storage_amount }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="price-display">
|
||||
<p>Price: <span id="price-display"></span></p>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Order</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
8
retoors/retoors/templates/pages/errors/404.html
Normal file
8
retoors/retoors/templates/pages/errors/404.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>404 - Page Not Found</h1>
|
||||
<p>The page you are looking for does not exist.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
8
retoors/retoors/templates/pages/errors/500.html
Normal file
8
retoors/retoors/templates/pages/errors/500.html
Normal file
@ -0,0 +1,8 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>500 - Internal Server Error</h1>
|
||||
<p>Something went wrong on our end. Please try again later.</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
36
retoors/retoors/templates/pages/index.html
Normal file
36
retoors/retoors/templates/pages/index.html
Normal file
@ -0,0 +1,36 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}Welcome to Retoors Storage{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/components/index.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<h1>Solutions for Everyone</h1>
|
||||
<p>Securely store and manage your data with HomeBase Storage.</p>
|
||||
</section>
|
||||
|
||||
<section class="features">
|
||||
<div class="feature-card" id="families">
|
||||
<img src="/static/images/icon-families.svg" alt="Families Icon">
|
||||
<h2>For Families</h2>
|
||||
<p>Securely backup and share precious photos and videos. Keep memories safe for generations.</p>
|
||||
</div>
|
||||
<div class="feature-card" id="professionals">
|
||||
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon">
|
||||
<h2>For Professionals</h2>
|
||||
<p>Organize important work documents, collaborate with teams, and access files from anywhere.</p>
|
||||
</div>
|
||||
<div class="feature-card" id="students">
|
||||
<img src="/static/images/icon-students.svg" alt="Students Icon">
|
||||
<h2>For Students</h2>
|
||||
<p>Store projects, notes, and research papers. Access study materials across your devices.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cta">
|
||||
<a href="/register" class="btn-primary">Find Your Perfect Plan</a>
|
||||
</section>
|
||||
{% endblock %}
|
||||
29
retoors/retoors/templates/pages/login.html
Normal file
29
retoors/retoors/templates/pages/login.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}Login{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form-container">
|
||||
<h2>Login to your Account</h2>
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form action="/login" method="post">
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
{% if errors.email %}
|
||||
<p class="error">{{ errors.email }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</.label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
{% if errors.password %}
|
||||
<p class="error">{{ errors.password }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Login</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
40
retoors/retoors/templates/pages/register.html
Normal file
40
retoors/retoors/templates/pages/register.html
Normal file
@ -0,0 +1,40 @@
|
||||
{% extends "layouts/base.html" %}
|
||||
|
||||
{% block title %}Register{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="form-container">
|
||||
<h2>Create an Account</h2>
|
||||
{% if error %}
|
||||
<p class="error">{{ error }}</p>
|
||||
{% endif %}
|
||||
<form action="/register" method="post">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" required>
|
||||
{% if errors.username %}
|
||||
<p class="error">{{ errors.username }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
{% if errors.email %}
|
||||
<p class="error">{{ errors.email }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
{% if errors.password %}
|
||||
<p class="error">{{ errors.password }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Register</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endblock %}
|
||||
0
retoors/retoors/views/__init__.py
Normal file
0
retoors/retoors/views/__init__.py
Normal file
106
retoors/retoors/views/auth.py
Normal file
106
retoors/retoors/views/auth.py
Normal file
@ -0,0 +1,106 @@
|
||||
from aiohttp import web
|
||||
import aiohttp_jinja2
|
||||
from aiohttp_session import get_session, new_session
|
||||
from aiohttp_pydantic import PydanticView
|
||||
from pydantic import BaseModel, EmailStr, Field, ValidationError
|
||||
|
||||
from ..services.user_service import UserService
|
||||
from ..models import RegistrationModel
|
||||
|
||||
|
||||
class LoginModel(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
class CustomPydanticView(PydanticView):
|
||||
template_name: str = ""
|
||||
|
||||
async def on_validation_error(
|
||||
self, exception: ValidationError, context: str
|
||||
):
|
||||
errors = {
|
||||
err["loc"][0]: err["msg"] for err in exception.errors()
|
||||
}
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name, self.request, {"errors": errors, "request": self.request}
|
||||
)
|
||||
|
||||
|
||||
class LoginView(CustomPydanticView):
|
||||
template_name = "pages/login.html"
|
||||
|
||||
async def get(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name, self.request, {"request": self.request, "errors": {}}
|
||||
)
|
||||
|
||||
async def post(self):
|
||||
try:
|
||||
login_data = LoginModel(**await self.request.post())
|
||||
except ValidationError as e:
|
||||
errors = {err["loc"][0]: err["msg"] for err in e.errors()}
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name, self.request, {"errors": errors, "request": self.request}
|
||||
)
|
||||
|
||||
user_service: UserService = self.request.app["user_service"]
|
||||
if user_service.authenticate_user(login_data.email, login_data.password):
|
||||
session = await new_session(self.request)
|
||||
session["user_email"] = login_data.email
|
||||
return web.HTTPFound("/dashboard")
|
||||
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name,
|
||||
self.request,
|
||||
{"error": "Invalid email or password", "request": self.request, "errors": {}},
|
||||
)
|
||||
|
||||
|
||||
class RegistrationView(CustomPydanticView):
|
||||
template_name = "pages/register.html"
|
||||
|
||||
async def get(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name, self.request, {"request": self.request, "errors": {}}
|
||||
)
|
||||
|
||||
async def post(self):
|
||||
try:
|
||||
user_data = RegistrationModel(**await self.request.post())
|
||||
except ValidationError as e:
|
||||
errors = {err["loc"][0]: err["msg"] for err in e.errors()}
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name, self.request, {"errors": errors, "request": self.request}
|
||||
)
|
||||
|
||||
data = await self.request.post()
|
||||
if user_data.password != data.get("confirm_password"):
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name,
|
||||
self.request,
|
||||
{"error": "Passwords do not match", "request": self.request, "errors": {}},
|
||||
)
|
||||
|
||||
user_service: UserService = self.request.app["user_service"]
|
||||
try:
|
||||
user_service.create_user(user_data.username, user_data.email, user_data.password)
|
||||
except ValueError:
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name,
|
||||
self.request,
|
||||
{
|
||||
"error": "User with this email already exists",
|
||||
"request": self.request,
|
||||
"errors": {},
|
||||
},
|
||||
)
|
||||
|
||||
return web.HTTPFound("/login")
|
||||
|
||||
|
||||
class LogoutView(web.View):
|
||||
async def get(self):
|
||||
session = await get_session(self.request)
|
||||
session.clear()
|
||||
return web.HTTPFound("/")
|
||||
47
retoors/retoors/views/site.py
Normal file
47
retoors/retoors/views/site.py
Normal file
@ -0,0 +1,47 @@
|
||||
from aiohttp import web
|
||||
import aiohttp_jinja2
|
||||
from aiohttp_session import get_session
|
||||
from pydantic import ValidationError
|
||||
|
||||
from ..services.user_service import UserService
|
||||
from ..helpers.auth import login_required
|
||||
from ..models import QuotaUpdateModel
|
||||
from .auth import CustomPydanticView
|
||||
|
||||
|
||||
class SiteView(web.View):
|
||||
async def get(self):
|
||||
if self.request.path == "/dashboard":
|
||||
return await self.dashboard()
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
@login_required
|
||||
async def dashboard(self):
|
||||
return aiohttp_jinja2.render_template(
|
||||
"pages/dashboard.html",
|
||||
self.request,
|
||||
{"user": self.request["user"], "request": self.request, "errors": {}},
|
||||
)
|
||||
|
||||
|
||||
class OrderView(CustomPydanticView):
|
||||
template_name = "pages/dashboard.html"
|
||||
|
||||
@login_required
|
||||
async def post(self):
|
||||
try:
|
||||
quota_data = QuotaUpdateModel(**await self.request.post())
|
||||
except ValidationError as e:
|
||||
errors = {err["loc"][0]: err["msg"] for err in e.errors()}
|
||||
return aiohttp_jinja2.render_template(
|
||||
self.template_name, self.request, {"errors": errors, "request": self.request, "user": self.request.get("user")}
|
||||
)
|
||||
|
||||
session = await get_session(self.request)
|
||||
user_email = session.get("user_email")
|
||||
user_service: UserService = self.request.app["user_service"]
|
||||
user_service.update_user_quota(user_email, quota_data.storage_amount)
|
||||
|
||||
return web.HTTPFound("/dashboard")
|
||||
17
retoors/setup.py
Normal file
17
retoors/setup.py
Normal file
@ -0,0 +1,17 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="retoors",
|
||||
version="0.1.0",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"aiohttp",
|
||||
"jinja2",
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"retoors=retoors.main:main",
|
||||
],
|
||||
},
|
||||
)
|
||||
33
retoors/tests/conftest.py
Normal file
33
retoors/tests/conftest.py
Normal file
@ -0,0 +1,33 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import json
|
||||
from retoors.main import create_app
|
||||
from retoors.services.user_service import UserService
|
||||
from retoors.services.config_service import ConfigService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(event_loop, aiohttp_client):
|
||||
app = create_app()
|
||||
|
||||
# Create temporary data files for testing
|
||||
base_path = Path(__file__).parent.parent
|
||||
data_path = base_path / "data"
|
||||
data_path.mkdir(exist_ok=True)
|
||||
|
||||
users_file = data_path / "users.json"
|
||||
with open(users_file, "w") as f:
|
||||
json.dump([], f)
|
||||
|
||||
config_file = data_path / "config.json"
|
||||
with open(config_file, "w") as f:
|
||||
json.dump({"price_per_gb": 0.0}, f)
|
||||
|
||||
app["user_service"] = UserService(users_file)
|
||||
app["config_service"] = ConfigService(config_file)
|
||||
|
||||
yield event_loop.run_until_complete(aiohttp_client(app))
|
||||
|
||||
# Clean up temporary files
|
||||
users_file.unlink(missing_ok=True)
|
||||
config_file.unlink(missing_ok=True)
|
||||
130
retoors/tests/test_auth.py
Normal file
130
retoors/tests/test_auth.py
Normal file
@ -0,0 +1,130 @@
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
async def test_login_get(client):
|
||||
resp = await client.get("/login")
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "Login to your Account" in text
|
||||
|
||||
|
||||
async def test_register_get(client):
|
||||
resp = await client.get("/register")
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "Create an Account" in text
|
||||
assert resp.url.path == "/register"
|
||||
|
||||
|
||||
async def test_register_post_password_mismatch(client):
|
||||
resp = await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "wrong_password",
|
||||
},
|
||||
)
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "Passwords do not match" in text
|
||||
|
||||
|
||||
async def test_register_post_user_exists(client):
|
||||
await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
resp = await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser2",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "User with this email already exists" in text
|
||||
|
||||
|
||||
async def test_register_post_invalid_email(client):
|
||||
resp = await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "invalid-email",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "value is not a valid email address" in text
|
||||
|
||||
|
||||
async def test_register_post_short_password(client):
|
||||
resp = await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "short",
|
||||
"confirm_password": "short",
|
||||
},
|
||||
)
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "ensure this value has at least 8 characters" in text
|
||||
|
||||
|
||||
async def test_login_post(client):
|
||||
await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
resp = await client.post(
|
||||
"/login", data={"email": "test@example.com", "password": "password"}, allow_redirects=False
|
||||
)
|
||||
assert resp.status == 302
|
||||
assert resp.headers["Location"] == "/dashboard"
|
||||
|
||||
|
||||
async def test_login_post_invalid_credentials(client):
|
||||
resp = await client.post(
|
||||
"/login", data={"email": "test@example.com", "password": "wrong_password"}
|
||||
)
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "Invalid email or password" in text
|
||||
|
||||
|
||||
async def test_logout(client):
|
||||
await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
await client.post(
|
||||
"/login", data={"email": "test@example.com", "password": "password"}
|
||||
)
|
||||
resp = await client.get("/logout", allow_redirects=False)
|
||||
assert resp.status == 302
|
||||
assert resp.headers["Location"] == "/"
|
||||
89
retoors/tests/test_site.py
Normal file
89
retoors/tests/test_site.py
Normal file
@ -0,0 +1,89 @@
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
|
||||
|
||||
async def test_index_get(client):
|
||||
resp = await client.get("/")
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "Solutions for Everyone" in text
|
||||
|
||||
|
||||
async def test_dashboard_get_unauthorized(client):
|
||||
resp = await client.get("/dashboard", allow_redirects=False)
|
||||
assert resp.status == 302
|
||||
assert resp.headers["Location"] == "/login"
|
||||
|
||||
|
||||
async def test_dashboard_get_authorized(client):
|
||||
await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
await client.post(
|
||||
"/login", data={"email": "test@example.com", "password": "password"}
|
||||
)
|
||||
resp = await client.get("/dashboard")
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "Welcome, test@example.com" in text
|
||||
|
||||
|
||||
async def test_order_post_unauthorized(client):
|
||||
resp = await client.post(
|
||||
"/order", data={"storage_amount": "10.5"}, allow_redirects=False
|
||||
)
|
||||
assert resp.status == 302
|
||||
assert resp.headers["Location"] == "/login"
|
||||
|
||||
|
||||
async def test_order_post_authorized(client):
|
||||
await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
await client.post(
|
||||
"/login", data={"email": "test@example.com", "password": "password"}
|
||||
)
|
||||
resp = await client.post("/order", data={"storage_amount": "10.5"}, allow_redirects=False)
|
||||
assert resp.status == 302
|
||||
assert resp.headers["Location"] == "/dashboard"
|
||||
|
||||
# Verify that the user's quota was updated
|
||||
user_service = client.app["user_service"]
|
||||
user = user_service.get_user_by_email("test@example.com")
|
||||
assert user["storage_quota_gb"] == 10.5
|
||||
|
||||
|
||||
async def test_order_post_invalid_amount(client):
|
||||
await client.post(
|
||||
"/register",
|
||||
data={
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "password",
|
||||
"confirm_password": "password",
|
||||
},
|
||||
)
|
||||
await client.post(
|
||||
"/login", data={"email": "test@example.com", "password": "password"}
|
||||
)
|
||||
resp = await client.post("/order", data={"storage_amount": "0"})
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "ensure this value is greater than 0" in text
|
||||
|
||||
resp = await client.post("/order", data={"storage_amount": "1001"})
|
||||
assert resp.status == 200
|
||||
text = await resp.text()
|
||||
assert "ensure this value is less than or equal to 1000" in text
|
||||
Loading…
Reference in New Issue
Block a user