Update.
This commit is contained in:
parent
2d6debe744
commit
e77b2d851d
@ -68,9 +68,9 @@ def get_or_create_session_secret_key(env_path: Path) -> bytes:
|
||||
|
||||
if secret_key_str:
|
||||
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')
|
||||
Fernet(final_secret_key_bytes) # Validate the key
|
||||
print(f"Using existing valid {key_name} from environment.")
|
||||
except ValueError:
|
||||
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
|
||||
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}")
|
||||
|
||||
|
||||
@ -4,7 +4,6 @@ import jinja2
|
||||
from pathlib import Path
|
||||
from aiohttp_session import setup as setup_session
|
||||
from aiohttp_session.cookie_storage import EncryptedCookieStorage
|
||||
import os
|
||||
import aiojobs # Import aiojobs
|
||||
import dotenv # Import dotenv
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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
|
||||
|
||||
|
||||
@ -17,6 +17,13 @@ def setup_routes(app):
|
||||
app.router.add_view("/use_cases", SiteView, name="use_cases")
|
||||
app.router.add_view("/dashboard", SiteView, name="dashboard")
|
||||
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
|
||||
app.router.add_get("/api/users", get_users, name="api_get_users")
|
||||
|
||||
50
retoors/static/css/components/file_browser.css
Normal file
50
retoors/static/css/components/file_browser.css
Normal 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;
|
||||
}
|
||||
@ -11,6 +11,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const editQuotaMessage = document.getElementById('edit-quota-message');
|
||||
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 openModal(modal) {
|
||||
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
|
||||
async function fetchAndRenderUsers() {
|
||||
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
|
||||
fetchAndRenderUsers();
|
||||
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import './components/slider.js';
|
||||
import './components/navigation.js'; // Assuming navigation.js might be needed globally
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Logic for custom-slider on order page
|
||||
const slider = document.querySelector('custom-slider');
|
||||
const priceDisplay = document.getElementById('total_price'); // Corrected ID
|
||||
const priceDisplay = document.getElementById('total_price');
|
||||
|
||||
if (slider && priceDisplay) {
|
||||
// pricePerGb will be read from a data attribute on the slider
|
||||
const pricePerGb = parseFloat(slider.dataset.pricePerGb);
|
||||
|
||||
const updatePrice = () => {
|
||||
@ -14,10 +15,71 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
priceDisplay.textContent = `$${totalPrice}`;
|
||||
};
|
||||
|
||||
// Initial price update
|
||||
updatePrice();
|
||||
|
||||
// Update price on slider change (using 'input' event for consistency with HTML)
|
||||
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');
|
||||
}
|
||||
});
|
||||
@ -11,6 +11,7 @@
|
||||
<li><a href="/support" aria-label="Support Page">Support</a></li>
|
||||
{% if request['user'] %}
|
||||
<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>
|
||||
{% else %}
|
||||
<li><a href="/login" class="btn-outline-nav" aria-label="Sign In to your account">Sign In</a></li>
|
||||
|
||||
@ -12,10 +12,10 @@
|
||||
<div class="sidebar-menu">
|
||||
<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="#"><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="#"><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="/shared"><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li>
|
||||
<li><a href="/recent"><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li>
|
||||
<li><a href="/favorites"><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li>
|
||||
<li><a href="/trash"><img src="/static/images/icon-professionals.svg" alt="Trash Icon" class="icon"> Trash</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -53,11 +53,11 @@
|
||||
<div class="dashboard-content-header">
|
||||
<h2>Welcome back, {{ user.full_name }}!</h2>
|
||||
<div class="dashboard-actions">
|
||||
<button class="btn-primary">+ New</button>
|
||||
<button class="btn-outline">Download</button>
|
||||
<button class="btn-outline">Upload</button>
|
||||
<button class="btn-outline">Share</button>
|
||||
<button class="btn-outline">Delete</button>
|
||||
<button class="btn-primary" onclick="alert('+ New feature coming soon!')">+ New</button>
|
||||
<button class="btn-outline" onclick="alert('Download feature coming soon!')">Download</button>
|
||||
<button class="btn-outline" onclick="alert('Upload feature coming soon!')">Upload</button>
|
||||
<button class="btn-outline" onclick="alert('Share feature coming soon!')">Share</button>
|
||||
<button class="btn-outline" onclick="alert('Delete feature coming soon!')">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
15
retoors/templates/pages/favorites.html
Normal file
15
retoors/templates/pages/favorites.html
Normal 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 %}
|
||||
24
retoors/templates/pages/file_browser.html
Normal file
24
retoors/templates/pages/file_browser.html
Normal 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 %}
|
||||
@ -31,7 +31,7 @@
|
||||
<p class="error">{{ errors.password }}</p>
|
||||
{% endif %}
|
||||
</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>
|
||||
</form>
|
||||
<p class="create-account-link">Don't have an account? <a href="/register">Create an Account</a></p>
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
<li>Dedicated Account Manager</li>
|
||||
<li>Advanced Analytics</li>
|
||||
</ul>
|
||||
<a href="/register" class="btn-primary">Contact Sales</a>
|
||||
<a href="/support" class="btn-primary">Contact Sales</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
55
retoors/templates/pages/privacy.html
Normal file
55
retoors/templates/pages/privacy.html
Normal 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 %}
|
||||
15
retoors/templates/pages/recent.html
Normal file
15
retoors/templates/pages/recent.html
Normal 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 %}
|
||||
15
retoors/templates/pages/shared.html
Normal file
15
retoors/templates/pages/shared.html
Normal 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 %}
|
||||
@ -13,7 +13,7 @@
|
||||
<p>Find answers to your questions, troubleshoot issues, or contact our support team for personalized assistance.</p>
|
||||
<div class="search-support">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
@ -24,25 +24,25 @@
|
||||
<img src="/static/images/icon-students.svg" alt="Getting Started Icon" class="icon">
|
||||
<h3>Getting Started</h3>
|
||||
<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 class="category-card">
|
||||
<img src="/static/images/icon-professionals.svg" alt="Billing Icon" class="icon">
|
||||
<h3>Billing & Payments</h3>
|
||||
<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 class="category-card">
|
||||
<img src="/static/images/icon-students.svg" alt="Troubleshooting Icon" class="icon">
|
||||
<h3>Troubleshooting</h3>
|
||||
<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 class="category-card">
|
||||
<img src="/static/images/icon-professionals.svg" alt="Account Management Icon" class="icon">
|
||||
<h3>Account Management</h3>
|
||||
<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>
|
||||
</section>
|
||||
@ -54,13 +54,13 @@
|
||||
<img src="/static/images/icon-students.svg" alt="Live Chat Icon" class="icon">
|
||||
<h3>Live Chat</h3>
|
||||
<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 class="contact-card">
|
||||
<img src="/static/images/icon-professionals.svg" alt="Support Ticket Icon" class="icon">
|
||||
<h3>Submit a Ticket</h3>
|
||||
<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 class="contact-card">
|
||||
<img src="/static/images/icon-students.svg" alt="Phone Support Icon" class="icon">
|
||||
|
||||
32
retoors/templates/pages/terms.html
Normal file
32
retoors/templates/pages/terms.html
Normal 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 %}
|
||||
15
retoors/templates/pages/trash.html
Normal file
15
retoors/templates/pages/trash.html
Normal 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 %}
|
||||
@ -1,10 +1,7 @@
|
||||
from aiohttp import web
|
||||
import aiohttp_jinja2
|
||||
from aiohttp_session import get_session
|
||||
from ..services.user_service import UserService
|
||||
from ..models import QuotaUpdateModel, RegistrationModel
|
||||
from typing import Dict, Any, List
|
||||
import json
|
||||
|
||||
async def get_users(request: web.Request) -> web.Response:
|
||||
user_service: UserService = request.app["user_service"]
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
from aiohttp import web
|
||||
import aiohttp_jinja2
|
||||
from aiohttp_session import get_session
|
||||
from pydantic import ValidationError
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from ..services.user_service import UserService
|
||||
from ..helpers.auth import login_required
|
||||
from ..models import QuotaUpdateModel
|
||||
from .auth import CustomPydanticView
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent.parent.parent / "project"
|
||||
|
||||
|
||||
class SiteView(web.View):
|
||||
async def get(self):
|
||||
@ -23,6 +23,18 @@ class SiteView(web.View):
|
||||
return await self.support()
|
||||
elif self.request.path == "/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(
|
||||
"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")}
|
||||
)
|
||||
|
||||
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):
|
||||
template_name = "pages/order.html"
|
||||
|
||||
@ -2,11 +2,8 @@ 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
|
||||
from pytest_mock import MockerFixture # Import MockerFixture
|
||||
from unittest import mock # For AsyncMock
|
||||
import aiojobs # Import aiojobs to patch it
|
||||
import datetime # Import datetime
|
||||
|
||||
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
import pytest
|
||||
from aiohttp import web
|
||||
from aiohttp_session import get_session
|
||||
from unittest.mock import AsyncMock, patch, call
|
||||
from unittest.mock import call
|
||||
|
||||
from retoors.services.user_service import UserService
|
||||
|
||||
@pytest.fixture
|
||||
async def admin_client(client):
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import pytest
|
||||
from unittest.mock import call
|
||||
import datetime
|
||||
import asyncio
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import patch
|
||||
from retoors.helpers.env_manager import ensure_env_file_exists, get_or_create_session_secret_key
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
|
||||
@ -120,4 +120,80 @@ async def test_order_post_authorized(client):
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import json
|
||||
from retoors.services.user_service import UserService
|
||||
import bcrypt
|
||||
|
||||
Loading…
Reference in New Issue
Block a user