This commit is contained in:
retoor 2025-11-08 22:36:29 +01:00
parent 2d6debe744
commit e77b2d851d
26 changed files with 518 additions and 48 deletions

View File

@ -68,9 +68,9 @@ def get_or_create_session_secret_key(env_path: Path) -> bytes:
if secret_key_str: if secret_key_str:
try: try:
# Fernet expects bytes, so encode the string from env var # Try to validate the key directly
Fernet(secret_key_str.encode('utf-8'))
final_secret_key_bytes = secret_key_str.encode('utf-8') final_secret_key_bytes = secret_key_str.encode('utf-8')
Fernet(final_secret_key_bytes) # Validate the key
print(f"Using existing valid {key_name} from environment.") print(f"Using existing valid {key_name} from environment.")
except ValueError: except ValueError:
print(f"Existing {key_name} in .env is invalid. Generating a new one.") print(f"Existing {key_name} in .env is invalid. Generating a new one.")
@ -83,7 +83,7 @@ def get_or_create_session_secret_key(env_path: Path) -> bytes:
# Append to .env file # Append to .env file
with open(env_path, 'a') as f: with open(env_path, 'a') as f:
f.write(f'\n{key_name}={generated_key_str}\n') f.write(f'\n{str(key_name)}={generated_key_str}\n')
print(f"Generated and added {key_name} to {env_path}") print(f"Generated and added {key_name} to {env_path}")

View File

@ -4,7 +4,6 @@ import jinja2
from pathlib import Path from pathlib import Path
from aiohttp_session import setup as setup_session from aiohttp_session import setup as setup_session
from aiohttp_session.cookie_storage import EncryptedCookieStorage from aiohttp_session.cookie_storage import EncryptedCookieStorage
import os
import aiojobs # Import aiojobs import aiojobs # Import aiojobs
import dotenv # Import dotenv import dotenv # Import dotenv

View File

@ -1,5 +1,5 @@
from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView
from .views.site import SiteView, OrderView from .views.site import SiteView, OrderView, FileBrowserView
from .views.admin import get_users, add_user, update_user_quota, delete_user, get_user_details, delete_team from .views.admin import get_users, add_user, update_user_quota, delete_user, get_user_details, delete_team
@ -17,6 +17,13 @@ def setup_routes(app):
app.router.add_view("/use_cases", SiteView, name="use_cases") app.router.add_view("/use_cases", SiteView, name="use_cases")
app.router.add_view("/dashboard", SiteView, name="dashboard") app.router.add_view("/dashboard", SiteView, name="dashboard")
app.router.add_view("/order", OrderView, name="order") app.router.add_view("/order", OrderView, name="order")
app.router.add_view("/terms", SiteView, name="terms")
app.router.add_view("/privacy", SiteView, name="privacy")
app.router.add_view("/shared", SiteView, name="shared")
app.router.add_view("/recent", SiteView, name="recent")
app.router.add_view("/favorites", SiteView, name="favorites")
app.router.add_view("/trash", SiteView, name="trash")
app.router.add_view("/files", FileBrowserView, name="file_browser")
# Admin API routes for user and team management # Admin API routes for user and team management
app.router.add_get("/api/users", get_users, name="api_get_users") app.router.add_get("/api/users", get_users, name="api_get_users")

View File

@ -0,0 +1,50 @@
/* retoors/static/css/components/file_browser.css */
.file-browser-section {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
background-color: var(--color-background-light);
border-radius: var(--border-radius);
box-shadow: var(--shadow-elevation-low);
}
.file-browser-section h1 {
color: var(--color-primary);
text-align: center;
margin-bottom: 1.5rem;
}
.file-list ul {
list-style: none;
padding: 0;
}
.file-list li {
background-color: var(--color-surface);
margin-bottom: 0.5rem;
padding: 0.8rem 1rem;
border-radius: var(--border-radius-small);
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--color-border);
}
.file-list li a {
color: var(--color-text);
text-decoration: none;
flex-grow: 1;
font-weight: var(--font-weight-medium);
}
.file-list li a:hover {
color: var(--color-primary-dark);
text-decoration: underline;
}
.file-list p {
text-align: center;
color: var(--color-text-secondary);
font-style: italic;
}

View File

@ -11,6 +11,12 @@ document.addEventListener('DOMContentLoaded', () => {
const editQuotaMessage = document.getElementById('edit-quota-message'); const editQuotaMessage = document.getElementById('edit-quota-message');
const userDetailsContent = document.getElementById('user-details-content'); const userDetailsContent = document.getElementById('user-details-content');
// Main order form elements
const mainOrderForm = document.querySelector('.order-form');
const customSlider = document.querySelector('custom-slider');
const totalPriceSpan = document.getElementById('total_price');
const pricePerGb = parseFloat(customSlider.dataset.pricePerGb);
// Function to open a modal // Function to open a modal
function openModal(modal) { function openModal(modal) {
modal.style.display = 'block'; modal.style.display = 'block';
@ -41,6 +47,19 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
// Function to update total price display
function updateTotalPrice() {
const storageAmount = parseFloat(customSlider.value);
const total = (storageAmount * pricePerGb).toFixed(2);
totalPriceSpan.textContent = `$${total}`;
}
// Initial price update and event listener for slider
if (customSlider) {
updateTotalPrice(); // Set initial price
customSlider.addEventListener('input', updateTotalPrice);
}
// Fetch and render users // Fetch and render users
async function fetchAndRenderUsers() { async function fetchAndRenderUsers() {
try { try {
@ -249,6 +268,47 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
// Main User Quota Update Form Submission
mainOrderForm.addEventListener('submit', async (event) => {
event.preventDefault(); // Prevent default form submission
const storageAmount = parseFloat(customSlider.value);
if (isNaN(storageAmount) || storageAmount <= 0) {
alert('Please select a valid storage amount.');
return;
}
try {
// Assuming the current user's email can be retrieved or is available globally
// For now, we'll assume a placeholder 'current_user_email'
// In a real application, this would come from a session or a global JS variable
const currentUserEmail = '{{ user.email }}'; // This needs to be passed from the backend
const response = await fetch(`/api/users/${currentUserEmail}/quota`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ new_quota_gb: storageAmount }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to update main user quota.');
}
alert(data.message || 'Main user quota updated successfully!');
// Re-fetch and render users to update the main user's quota display
fetchAndRenderUsers();
// Optionally, update the main quota display directly if not covered by fetchAndRenderUsers
// For now, relying on fetchAndRenderUsers to update all quota displays
} catch (error) {
console.error('Error updating main user quota:', error);
alert(`Failed to update main user quota: ${error.message}`);
}
});
// Initial fetch and render // Initial fetch and render
fetchAndRenderUsers(); fetchAndRenderUsers();

View File

@ -1,11 +1,12 @@
import './components/slider.js'; import './components/slider.js';
import './components/navigation.js'; // Assuming navigation.js might be needed globally
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Logic for custom-slider on order page
const slider = document.querySelector('custom-slider'); const slider = document.querySelector('custom-slider');
const priceDisplay = document.getElementById('total_price'); // Corrected ID const priceDisplay = document.getElementById('total_price');
if (slider && priceDisplay) { if (slider && priceDisplay) {
// pricePerGb will be read from a data attribute on the slider
const pricePerGb = parseFloat(slider.dataset.pricePerGb); const pricePerGb = parseFloat(slider.dataset.pricePerGb);
const updatePrice = () => { const updatePrice = () => {
@ -14,10 +15,71 @@ document.addEventListener('DOMContentLoaded', () => {
priceDisplay.textContent = `$${totalPrice}`; priceDisplay.textContent = `$${totalPrice}`;
}; };
// Initial price update
updatePrice(); updatePrice();
// Update price on slider change (using 'input' event for consistency with HTML)
slider.addEventListener('input', updatePrice); slider.addEventListener('input', updatePrice);
} }
// Logic for pricing page toggle
const pricingToggle = document.querySelector('.pricing-toggle');
if (pricingToggle) {
const monthlyBtn = pricingToggle.querySelector('[data-period="monthly"]');
const annuallyBtn = pricingToggle.querySelector('[data-period="annually"]');
const pricingCards = document.querySelectorAll('.pricing-card');
const monthlyPrices = {
"Free": 0,
"Personal": 9,
"Professional": 29,
"Business": 99
};
const annualPrices = {
"Free": 0,
"Personal": 90, // 9 * 10 (assuming 2 months free)
"Professional": 290, // 29 * 10
"Business": 990 // 99 * 10
};
function updatePricingDisplay(period) {
pricingCards.forEach(card => {
const planName = card.querySelector('h3').textContent;
const priceElement = card.querySelector('.price');
let price = 0;
let periodText = '';
if (period === 'monthly') {
price = monthlyPrices[planName];
periodText = '/month';
} else {
price = annualPrices[planName];
periodText = '/year';
}
if (planName === "Free") {
priceElement.innerHTML = `$${price}<span>${periodText}</span>`;
} else if (planName === "Business") {
// Business plan might have custom pricing, keep it as is or adjust
priceElement.innerHTML = `$${price}<span>${periodText}</span>`;
}
else {
priceElement.innerHTML = `$${price}<span>${periodText}</span>`;
}
});
}
monthlyBtn.addEventListener('click', () => {
monthlyBtn.classList.add('active');
annuallyBtn.classList.remove('active');
updatePricingDisplay('monthly');
});
annuallyBtn.addEventListener('click', () => {
annuallyBtn.classList.add('active');
monthlyBtn.classList.remove('active');
updatePricingDisplay('annually');
});
// Initial display based on active button (default to monthly)
updatePricingDisplay('monthly');
}
}); });

View File

@ -11,6 +11,7 @@
<li><a href="/support" aria-label="Support Page">Support</a></li> <li><a href="/support" aria-label="Support Page">Support</a></li>
{% if request['user'] %} {% if request['user'] %}
<li><a href="/dashboard" aria-label="User Dashboard">Dashboard</a></li> <li><a href="/dashboard" aria-label="User Dashboard">Dashboard</a></li>
<li><a href="/files" aria-label="File Browser">File Browser</a></li>
<li><a href="/logout" class="btn-primary-nav" aria-label="Logout">Logout</a></li> <li><a href="/logout" class="btn-primary-nav" aria-label="Logout">Logout</a></li>
{% else %} {% else %}
<li><a href="/login" class="btn-outline-nav" aria-label="Sign In to your account">Sign In</a></li> <li><a href="/login" class="btn-outline-nav" aria-label="Sign In to your account">Sign In</a></li>

View File

@ -12,10 +12,10 @@
<div class="sidebar-menu"> <div class="sidebar-menu">
<ul> <ul>
<li><a href="/dashboard" class="active"><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li> <li><a href="/dashboard" class="active"><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li>
<li><a href="#"><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li> <li><a href="/shared"><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li>
<li><a href="#"><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li> <li><a href="/recent"><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li>
<li><a href="#"><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li> <li><a href="/favorites"><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li>
<li><a href="#"><img src="/static/images/icon-professionals.svg" alt="Trash Icon" class="icon"> Trash</a></li> <li><a href="/trash"><img src="/static/images/icon-professionals.svg" alt="Trash Icon" class="icon"> Trash</a></li>
</ul> </ul>
</div> </div>
@ -53,11 +53,11 @@
<div class="dashboard-content-header"> <div class="dashboard-content-header">
<h2>Welcome back, {{ user.full_name }}!</h2> <h2>Welcome back, {{ user.full_name }}!</h2>
<div class="dashboard-actions"> <div class="dashboard-actions">
<button class="btn-primary">+ New</button> <button class="btn-primary" onclick="alert('+ New feature coming soon!')">+ New</button>
<button class="btn-outline">Download</button> <button class="btn-outline" onclick="alert('Download feature coming soon!')">Download</button>
<button class="btn-outline">Upload</button> <button class="btn-outline" onclick="alert('Upload feature coming soon!')">Upload</button>
<button class="btn-outline">Share</button> <button class="btn-outline" onclick="alert('Share feature coming soon!')">Share</button>
<button class="btn-outline">Delete</button> <button class="btn-outline" onclick="alert('Delete feature coming soon!')">Delete</button>
</div> </div>
</div> </div>

View File

@ -0,0 +1,15 @@
{% extends "layouts/base.html" %}
{% block title %}Favorites - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Favorites</h1>
<p>Your favorite files and folders will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
{% endblock %}

View File

@ -0,0 +1,24 @@
{% extends "layouts/base.html" %}
{% block title %}File Browser{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/file_browser.css">
{% endblock %}
{% block content %}
<main>
<section class="file-browser-section">
<h1>File Browser</h1>
<div class="file-list">
{% if files %}
<ul>
{% for file in files %}
<li><a href="/files/{{ file }}" target="_blank">{{ file }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No files found in the project directory.</p>
{% endif %}
</div>
</section>
</main>
{% endblock %}

View File

@ -31,7 +31,7 @@
<p class="error">{{ errors.password }}</p> <p class="error">{{ errors.password }}</p>
{% endif %} {% endif %}
</div> </div>
<a href="#" class="forgot-password-link">Forgot Password?</a> <a href="/forgot_password" class="forgot-password-link">Forgot Password?</a>
<button type="submit" class="btn-primary">Log In Securely</button> <button type="submit" class="btn-primary">Log In Securely</button>
</form> </form>
<p class="create-account-link">Don't have an account? <a href="/register">Create an Account</a></p> <p class="create-account-link">Don't have an account? <a href="/register">Create an Account</a></p>

View File

@ -62,7 +62,7 @@
<li>Dedicated Account Manager</li> <li>Dedicated Account Manager</li>
<li>Advanced Analytics</li> <li>Advanced Analytics</li>
</ul> </ul>
<a href="/register" class="btn-primary">Contact Sales</a> <a href="/support" class="btn-primary">Contact Sales</a>
</div> </div>
</section> </section>

View File

@ -0,0 +1,55 @@
{% extends "layouts/base.html" %}
{% block title %}Privacy Policy{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Privacy Policy</h1>
<p>This Privacy Policy describes how Retoor's Cloud Solutions collects, uses, and discloses your information when you use our service.</p>
<h2>1. Information We Collect</h2>
<h3>Personal Data</h3>
<p>While using our Service, we may ask you to provide us with certain personally identifiable information that can be used to contact or identify you. Personally identifiable information may include, but is not limited to: Email address, First name and last name, Phone number, Address, State, Province, ZIP/Postal code, City, Usage Data.</p>
<h3>Usage Data</h3>
<p>Usage Data is collected automatically when using the Service. Usage Data may include information such as your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that you visit, the time and date of your visit, the time spent on those pages, unique device identifiers and other diagnostic data.</p>
<h2>2. How We Use Your Information</h2>
<p>Retoor's Cloud Solutions uses the collected data for various purposes:</p>
<ul>
<li>To provide and maintain our Service</li>
<li>To notify you about changes to our Service</li>
<li>To allow you to participate in interactive features of our Service when you choose to do so</li>
<li>To provide customer support</li>
<li>To gather analysis or valuable information so that we can improve our Service</li>
<li>To monitor the usage of our Service</li>
<li>To detect, prevent and address technical issues</li>
</ul>
<h2>3. Disclosure Of Your Information</h2>
<p>We may disclose your Personal Data in the good faith belief that such action is necessary to:</p>
<ul>
<li>To comply with a legal obligation</li>
<li>To protect and defend the rights or property of Retoor's Cloud Solutions</li>
<li>To prevent or investigate possible wrongdoing in connection with the Service</li>
<li>To protect the personal safety of users of the Service or the public</li>
<li>To protect against legal liability</li>
</ul>
<h2>4. Security Of Your Information</h2>
<p>The security of your data is important to us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While we strive to use commercially acceptable means to protect your Personal Data, we cannot guarantee its absolute security.</p>
<h2>5. Your Data Protection Rights</h2>
<p>Depending on your location, you may have the following data protection rights:</p>
<ul>
<li>The right to access, update or to delete the information we have on you.</li>
<li>The right to rectify your information.</li>
<li>The right to object to our processing of your Personal Data.</li>
<li>The right to request the restriction of the processing of your personal information.</li>
<li>The right to data portability.</li>
<li>The right to withdraw consent.</li>
</ul>
<h2>6. Changes to This Privacy Policy</h2>
<p>We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page.</p>
<h2>7. Contact Us</h2>
<p>If you have any questions about this Privacy Policy, please contact us.</p>
</section>
</main>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "layouts/base.html" %}
{% block title %}Recent Files - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Recent Files</h1>
<p>Your recently accessed files will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "layouts/base.html" %}
{% block title %}Shared with me - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Shared with me</h1>
<p>Files and folders that have been shared with you will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
{% endblock %}

View File

@ -13,7 +13,7 @@
<p>Find answers to your questions, troubleshoot issues, or contact our support team for personalized assistance.</p> <p>Find answers to your questions, troubleshoot issues, or contact our support team for personalized assistance.</p>
<div class="search-support"> <div class="search-support">
<input type="text" placeholder="Search our knowledge base..." class="search-input"> <input type="text" placeholder="Search our knowledge base..." class="search-input">
<button class="btn-primary">Search</button> <button class="btn-primary" onclick="alert('Search functionality coming soon!')">Search</button>
</div> </div>
</section> </section>
@ -24,25 +24,25 @@
<img src="/static/images/icon-students.svg" alt="Getting Started Icon" class="icon"> <img src="/static/images/icon-students.svg" alt="Getting Started Icon" class="icon">
<h3>Getting Started</h3> <h3>Getting Started</h3>
<p>Guides to help you set up your account and start using Retoor's.</p> <p>Guides to help you set up your account and start using Retoor's.</p>
<a href="#" class="btn-link">View Articles</a> <a href="#" class="btn-link" onclick="alert('Knowledge base articles coming soon!')">View Articles</a>
</div> </div>
<div class="category-card"> <div class="category-card">
<img src="/static/images/icon-professionals.svg" alt="Billing Icon" class="icon"> <img src="/static/images/icon-professionals.svg" alt="Billing Icon" class="icon">
<h3>Billing & Payments</h3> <h3>Billing & Payments</h3>
<p>Information about your subscription, invoices, and payment methods.</p> <p>Information about your subscription, invoices, and payment methods.</p>
<a href="#" class="btn-link">View Articles</a> <a href="#" class="btn-link" onclick="alert('Knowledge base articles coming soon!')">View Articles</a>
</div> </div>
<div class="category-card"> <div class="category-card">
<img src="/static/images/icon-students.svg" alt="Troubleshooting Icon" class="icon"> <img src="/static/images/icon-students.svg" alt="Troubleshooting Icon" class="icon">
<h3>Troubleshooting</h3> <h3>Troubleshooting</h3>
<p>Solutions to common issues and technical problems.</p> <p>Solutions to common issues and technical problems.</p>
<a href="#" class="btn-link">View Articles</a> <a href="#" class="btn-link" onclick="alert('Knowledge base articles coming soon!')">View Articles</a>
</div> </div>
<div class="category-card"> <div class="category-card">
<img src="/static/images/icon-professionals.svg" alt="Account Management Icon" class="icon"> <img src="/static/images/icon-professionals.svg" alt="Account Management Icon" class="icon">
<h3>Account Management</h3> <h3>Account Management</h3>
<p>Manage your profile, security settings, and user permissions.</p> <p>Manage your profile, security settings, and user permissions.</p>
<a href="#" class="btn-link">View Articles</a> <a href="#" class="btn-link" onclick="alert('Knowledge base articles coming soon!')">View Articles</a>
</div> </div>
</div> </div>
</section> </section>
@ -54,13 +54,13 @@
<img src="/static/images/icon-students.svg" alt="Live Chat Icon" class="icon"> <img src="/static/images/icon-students.svg" alt="Live Chat Icon" class="icon">
<h3>Live Chat</h3> <h3>Live Chat</h3>
<p>Get instant support from our team. Available 9 AM - 5 PM EST.</p> <p>Get instant support from our team. Available 9 AM - 5 PM EST.</p>
<button class="btn-primary">Chat with Us Now</button> <button class="btn-primary" onclick="alert('Live chat coming soon!')">Chat with Us Now</button>
</div> </div>
<div class="contact-card"> <div class="contact-card">
<img src="/static/images/icon-professionals.svg" alt="Support Ticket Icon" class="icon"> <img src="/static/images/icon-professionals.svg" alt="Support Ticket Icon" class="icon">
<h3>Submit a Ticket</h3> <h3>Submit a Ticket</h3>
<p>For non-urgent inquiries, submit a detailed support ticket.</p> <p>For non-urgent inquiries, submit a detailed support ticket.</p>
<a href="#" class="btn-primary">Submit a Ticket</a> <a href="#" class="btn-primary" onclick="alert('Ticket submission coming soon!')">Submit a Ticket</a>
</div> </div>
<div class="contact-card"> <div class="contact-card">
<img src="/static/images/icon-students.svg" alt="Phone Support Icon" class="icon"> <img src="/static/images/icon-students.svg" alt="Phone Support Icon" class="icon">

View File

@ -0,0 +1,32 @@
{% extends "layouts/base.html" %}
{% block title %}Terms of Service{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Terms of Service</h1>
<p>These are the terms of service for Retoor's Cloud Solutions. Please read them carefully.</p>
<h2>1. Acceptance of Terms</h2>
<p>By accessing and using our services, you agree to be bound by these Terms of Service and all terms incorporated by reference. If you do not agree to all of these terms, do not use our services.</p>
<h2>2. Changes to Terms</h2>
<p>We reserve the right to modify or revise these Terms at any time. We will notify you of any changes by posting the new Terms on this page. Your continued use of the services after any such changes constitutes your acceptance of the new Terms of Service.</p>
<h2>3. Privacy Policy</h2>
<p>Please refer to our Privacy Policy for information on how we collect, use, and disclose information from our users.</p>
<h2>4. User Conduct</h2>
<p>You agree not to use the services for any unlawful purpose or in any way that might harm, abuse, or otherwise interfere with the services or any other user.</p>
<h2>5. Termination</h2>
<p>We may terminate or suspend your access to our services immediately, without prior notice or liability, for any reason whatsoever, including without limitation if you breach the Terms.</p>
<h2>6. Disclaimer of Warranties</h2>
<p>Our services are provided on an "AS IS" and "AS AVAILABLE" basis. We do not warrant that the services will be uninterrupted, secure, or error-free.</p>
<h2>7. Limitation of Liability</h2>
<p>In no event shall Retoor's Cloud Solutions, nor its directors, employees, partners, agents, suppliers, or affiliates, be liable for any indirect, incidental, special, consequential or punitive damages, including without limitation, loss of profits, data, use, goodwill, or other intangible losses, resulting from (i) your access to or use of or inability to access or use the Service; (ii) any conduct or content of any third party on the Service; (iii) any content obtained from the Service; and (iv) unauthorized access, use or alteration of your transmissions or content, whether based on warranty, contract, tort (including negligence) or any other legal theory, whether or not we have been informed of the possibility of such damage, and even if a remedy set forth herein is found to have failed of its essential purpose.</p>
<h2>8. Governing Law</h2>
<p>These Terms shall be governed and construed in accordance with the laws of [Your Jurisdiction], without regard to its conflict of law provisions.</p>
<h2>9. Contact Us</h2>
<p>If you have any questions about these Terms, please contact us.</p>
</section>
</main>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "layouts/base.html" %}
{% block title %}Trash - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Trash</h1>
<p>Files and folders you have deleted will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
{% endblock %}

View File

@ -1,10 +1,7 @@
from aiohttp import web from aiohttp import web
import aiohttp_jinja2
from aiohttp_session import get_session from aiohttp_session import get_session
from ..services.user_service import UserService from ..services.user_service import UserService
from ..models import QuotaUpdateModel, RegistrationModel from ..models import QuotaUpdateModel, RegistrationModel
from typing import Dict, Any, List
import json
async def get_users(request: web.Request) -> web.Response: async def get_users(request: web.Request) -> web.Response:
user_service: UserService = request.app["user_service"] user_service: UserService = request.app["user_service"]

View File

@ -1,13 +1,13 @@
from aiohttp import web from aiohttp import web
import aiohttp_jinja2 import aiohttp_jinja2
from aiohttp_session import get_session import os
from pydantic import ValidationError from pathlib import Path
from ..services.user_service import UserService
from ..helpers.auth import login_required from ..helpers.auth import login_required
from ..models import QuotaUpdateModel
from .auth import CustomPydanticView from .auth import CustomPydanticView
PROJECT_DIR = Path(__file__).parent.parent.parent / "project"
class SiteView(web.View): class SiteView(web.View):
async def get(self): async def get(self):
@ -23,6 +23,18 @@ class SiteView(web.View):
return await self.support() return await self.support()
elif self.request.path == "/use_cases": elif self.request.path == "/use_cases":
return await self.use_cases() return await self.use_cases()
elif self.request.path == "/terms":
return await self.terms()
elif self.request.path == "/privacy":
return await self.privacy()
elif self.request.path == "/shared":
return await self.shared()
elif self.request.path == "/recent":
return await self.recent()
elif self.request.path == "/favorites":
return await self.favorites()
elif self.request.path == "/trash":
return await self.trash()
return aiohttp_jinja2.render_template( return aiohttp_jinja2.render_template(
"pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")} "pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
) )
@ -60,6 +72,52 @@ class SiteView(web.View):
"pages/use_cases.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")} "pages/use_cases.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
) )
async def terms(self):
return aiohttp_jinja2.render_template(
"pages/terms.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def privacy(self):
return aiohttp_jinja2.render_template(
"pages/privacy.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def shared(self):
return aiohttp_jinja2.render_template(
"pages/shared.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def recent(self):
return aiohttp_jinja2.render_template(
"pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def favorites(self):
return aiohttp_jinja2.render_template(
"pages/favorites.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
async def trash(self):
return aiohttp_jinja2.render_template(
"pages/trash.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
class FileBrowserView(web.View):
@login_required
async def get(self):
files = []
if PROJECT_DIR.is_dir():
for item in os.listdir(PROJECT_DIR):
item_path = PROJECT_DIR / item
if item_path.is_file():
files.append(item)
return aiohttp_jinja2.render_template(
"pages/file_browser.html",
self.request,
{"request": self.request, "files": files, "user": self.request.get("user")},
)
class OrderView(CustomPydanticView): class OrderView(CustomPydanticView):
template_name = "pages/order.html" template_name = "pages/order.html"

View File

@ -2,11 +2,8 @@ import pytest
from pathlib import Path from pathlib import Path
import json import json
from retoors.main import create_app from retoors.main import create_app
from retoors.services.user_service import UserService
from retoors.services.config_service import ConfigService from retoors.services.config_service import ConfigService
from pytest_mock import MockerFixture # Import MockerFixture from pytest_mock import MockerFixture # Import MockerFixture
from unittest import mock # For AsyncMock
import aiojobs # Import aiojobs to patch it
import datetime # Import datetime import datetime # Import datetime

View File

@ -1,9 +1,6 @@
import pytest import pytest
from aiohttp import web from unittest.mock import call
from aiohttp_session import get_session
from unittest.mock import AsyncMock, patch, call
from retoors.services.user_service import UserService
@pytest.fixture @pytest.fixture
async def admin_client(client): async def admin_client(client):

View File

@ -1,5 +1,3 @@
import pytest
from unittest.mock import call
import datetime import datetime
import asyncio import asyncio

View File

@ -1,7 +1,5 @@
import pytest import pytest
from pathlib import Path from unittest.mock import patch
import os
from unittest.mock import patch, MagicMock
from retoors.helpers.env_manager import ensure_env_file_exists, get_or_create_session_secret_key from retoors.helpers.env_manager import ensure_env_file_exists, get_or_create_session_secret_key
from cryptography.fernet import Fernet from cryptography.fernet import Fernet

View File

@ -120,4 +120,80 @@ async def test_order_post_authorized(client):
assert "Total Storage Used:" in text assert "Total Storage Used:" in text
async def test_terms_get(client):
resp = await client.get("/terms")
assert resp.status == 200
text = await resp.text()
assert "Terms of Service" in text
assert "By accessing and using our services, you agree to be bound by these Terms of Service" in text
async def test_privacy_get(client):
resp = await client.get("/privacy")
assert resp.status == 200
text = await resp.text()
assert "Privacy Policy" in text
assert "This Privacy Policy describes how Retoor's Cloud Solutions collects, uses, and discloses your information" in text
async def test_shared_get(client):
resp = await client.get("/shared")
assert resp.status == 200
text = await resp.text()
assert "Shared with me" in text
assert "Files and folders that have been shared with you will appear here." in text
async def test_recent_get(client):
resp = await client.get("/recent")
assert resp.status == 200
text = await resp.text()
assert "Recent Files" in text
assert "Your recently accessed files will appear here." in text
async def test_favorites_get(client):
resp = await client.get("/favorites")
assert resp.status == 200
text = await resp.text()
assert "Favorites" in text
assert "Your favorite files and folders will appear here." in text
async def test_trash_get(client):
resp = await client.get("/trash")
assert resp.status == 200
text = await resp.text()
assert "Trash" in text
assert "Files and folders you have deleted will appear here." in text
async def test_file_browser_get_unauthorized(client):
resp = await client.get("/files", allow_redirects=False)
assert resp.status == 302
assert resp.headers["Location"] == "/login"
async def test_file_browser_get_authorized(client):
await client.post(
"/register",
data={
"full_name": "Test User",
"email": "test@example.com",
"password": "password",
"confirm_password": "password",
},
)
await client.post(
"/login", data={"email": "test@example.com", "password": "password"}
)
resp = await client.get("/files")
assert resp.status == 200
text = await resp.text()
assert "File Browser" in text
# Check for some expected files from the project directory
assert "example.jpg" in text
assert "rexample7.jpg" in text

View File

@ -1,5 +1,4 @@
import pytest import pytest
from pathlib import Path
import json import json
from retoors.services.user_service import UserService from retoors.services.user_service import UserService
import bcrypt import bcrypt