Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions docs/BACKUP_RECOVERY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,19 @@ This document outlines the backup and recovery procedures for the StreamFlix Pos

### Automated Backups
- **Frequency**: Daily at 2:00 AM UTC
- Low Traffic Window: 2:00 AM UTC is statistically the time of lowest user activity across our target demographics.
Performance Impact: Running backups consumes CPU and I/O. Performing this during off-peak hours prevents API latency for active users.
- **Retention Policy**:
- Daily backups: 7 days
- Weekly backups: 4 weeks
- Monthly backups: 12 months
- Daily backups: 7 days : Handles the most common scenario"I accidentally deleted something yesterday."
- Weekly backups: 4 weeks : Useful for identifying trends or issues that weren't noticed immediately.
- Monthly backups: 12 months : Required for long-term auditing and compliance without keeping 365 separate daily files (which would waste storage)

- **Storage Location**: `./backups/` directory
- **Backup Method**: PostgreSQL `pg_dump` (logical backup)
- **Backup Method**: PostgreSQL `pg_dump` (logical backup)
- this has been choosen because of fallowings :
Portability: Logical SQL exports can be restored across different operating systems or slightly different PostgreSQL versions.
Granularity: It allows us to edit the SQL file if we only need to recover a specific table or row, which is impossible with a binary snapshot.
Size: Text-based SQL dumps compress highly efficiently with GZIP/ZIP.

### Manual Backup

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/index.html", "/dashboard.html", "/css/**", "/js/**").permitAll()
.requestMatchers("/index.html", "/dashboard.html", "/css/**", "/js/**", "/admin.html").permitAll()
.requestMatchers("/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/verify", "/api/v1/auth/forgot-password", "/api/v1/auth/reset-password", "/api/v1/auth/admin/register").permitAll()
.requestMatchers("/api/v1/media/admin/**","/api/v1/media/movie/admin/**","/api/v1/media/series/admin/**")
.hasAnyRole("Junior", "Mid-level", "Senior")
Expand Down
54 changes: 54 additions & 0 deletions src/main/resources/static/admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Panel</title>
<link rel="stylesheet" href="css/admin.css">
</head>
<body>
<div class="container">
<h1>Admin Panel - Add New Movie</h1>
<form id="add-movie-form">
<div class="form-group">
<label for="title">Title</label>
<input type="text" id="title" name="title" required>
</div>
<div class="form-group">
<label for="ageRating">Age Rating</label>
<input type="text" id="ageRating" name="ageRating" required>
</div>
<div class="form-group">
<label for="releaseDate">Release Date</label>
<input type="date" id="releaseDate" name="releaseDate" required>
</div>
<div class="form-group">
<label for="durationInSecond">Duration (seconds)</label>
<input type="number" id="durationInSecond" name="durationInSecond" required>
</div>
<div class="form-group">
<label for="externalId">External ID (e.g., TMDB ID)</label>
<input type="text" id="externalId" name="externalId">
</div>
<div class="form-group">
<label for="posterUrl">Poster URL</label>
<input type="url" id="posterUrl" name="posterUrl">
</div>
<div class="form-group">
<label for="backdropUrl">Backdrop URL</label>
<input type="url" id="backdropUrl" name="backdropUrl">
</div>
<div class="form-group">
<label for="overview">Overview</label>
<textarea id="overview" name="overview" rows="4"></textarea>
</div>
<div class="form-group">
<label for="externalRating">External Rating</label>
<input type="number" step="0.1" id="externalRating" name="externalRating">
</div>
<button type="submit">Add Movie</button>
</form>
<div id="message-container"></div>
</div>
<script src="js/admin.js"></script>
</body>
</html>
63 changes: 63 additions & 0 deletions src/main/resources/static/css/admin.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
body {
font-family: sans-serif;
background-color: #f4f4f4;
padding: 20px;
}

.container {
max-width: 600px;
margin: auto;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.form-group {
margin-bottom: 15px;
}

.form-group label {
display: block;
margin-bottom: 5px;
}

.form-group input,
.form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}

button {
background-color: #007bff;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
cursor: pointer;
}

button:hover {
background-color: #0056b3;
}

#message-container {
margin-top: 20px;
padding: 10px;
border-radius: 4px;
}

.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}

.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
2 changes: 2 additions & 0 deletions src/main/resources/static/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<nav class="navbar">
<a href="dashboard.html" class="navbar-brand">🎬 StreamFlix</a>
<div class="navbar-nav">
<a href="/admin.html" id="admin-link" style="display:none;">Admin Panel</a>
<a href="#" onclick="showSection('browse')" class="nav-link">Browse</a>
<a href="#" onclick="showSection('watchlist')" class="nav-link">My List</a>
<a href="#" onclick="showSection('progress')" class="nav-link">Continue Watching</a>
Expand Down Expand Up @@ -317,6 +318,7 @@ <h4>Preferences</h4>
</div>
</div>


<script src="js/config.js"></script>
<script src="js/utils.js"></script>
<script src="js/api.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ <h2>Create New Password</h2>

<!-- Verification Instructions -->
<div id="verification-info" class="info-box hidden">
<h3> Registration Successful!</h3>
<h3> Registration Successful!</h3>
<p>A verification token has been generated. Check the server logs for the token.</p>
<p>In a production environment, this would be sent via email.</p>
<div class="form-group">
Expand Down
46 changes: 46 additions & 0 deletions src/main/resources/static/js/admin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
document.getElementById('add-movie-form').addEventListener('submit', function(event) {
event.preventDefault();

const formData = new FormData(this);
const movieData = {};
formData.forEach((value, key) => {
movieData[key] = value;
});

// The mediaType is required by the backend, but not in the form
movieData['mediaType'] = 'MOVIE';

const token = localStorage.getItem('jwt');
if (!token) {
showMessage('You must be logged in to add a movie.', 'error');
return;
}

fetch('/api/v1/media/movie/admin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify(movieData)
})
.then(response => {
if (!response.ok) {
return response.json().then(err => { throw new Error(err.message || 'An error occurred') });
}
return response.json();
})
.then(data => {
showMessage('Movie added successfully! Movie ID: ' + data.id, 'success');
document.getElementById('add-movie-form').reset();
})
.catch(error => {
showMessage('Error: ' + error.message, 'error');
});
});

function showMessage(message, type) {
const messageContainer = document.getElementById('message-container');
messageContainer.textContent = message;
messageContainer.className = type;
}
15 changes: 15 additions & 0 deletions src/main/resources/static/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,21 @@ async function initDashboard() {
const email = localStorage.getItem(STORAGE_KEYS.USER_EMAIL);
document.getElementById('user-email').textContent = email;

// Show admin link if user is an admin
try {
const token = localStorage.getItem('jwt');
const payload = JSON.parse(atob(token.split('.')[1]));
const roles = payload.roles || [];
if (roles.includes('ROLE_ADMIN')) {
const adminLink = document.getElementById('admin-link');
if(adminLink) {
adminLink.style.display = 'block';
}
}
} catch (e) {
console.error('Error parsing JWT for admin check:', e);
}

// Load profiles
await loadProfiles();

Expand Down