Add sermon retention policy feature
Implemented a configurable retention policy system for sermons with automatic cleanup: - Added settings table to store retention policy configuration - Created API endpoints for getting/setting retention policy - Added Database Settings section to admin page with retention options (forever, 1-10 years) - Implemented manual cleanup endpoint for on-demand deletion - Added automated daily cleanup task via Nitro plugin - Sermons are deleted based on their date field according to the retention policy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
128
pages/admin.vue
128
pages/admin.vue
@@ -346,8 +346,68 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Database Settings Section -->
|
||||||
|
<div class="bg-white shadow-lg rounded-lg p-8 mt-8">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900 mb-6">Database Settings</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label for="retention-policy" class="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Sermon Retention Policy
|
||||||
|
</label>
|
||||||
|
<p class="text-sm text-gray-600 mb-3">
|
||||||
|
Choose how long to keep sermons in the database. Older sermons will be automatically deleted based on this policy.
|
||||||
|
</p>
|
||||||
|
<select
|
||||||
|
id="retention-policy"
|
||||||
|
v-model="retentionPolicy"
|
||||||
|
@change="handleRetentionPolicyChange"
|
||||||
|
:disabled="savingRetentionPolicy"
|
||||||
|
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<option value="forever">Keep Forever</option>
|
||||||
|
<option value="1_month">1 Month</option>
|
||||||
|
<option value="3_months">3 Months</option>
|
||||||
|
<option value="6_months">6 Months</option>
|
||||||
|
<option value="1_year">1 Year</option>
|
||||||
|
<option value="3_years">3 Years</option>
|
||||||
|
<option value="5_years">5 Years</option>
|
||||||
|
<option value="10_years">10 Years</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="retentionPolicyError" class="text-red-600 text-sm">
|
||||||
|
{{ retentionPolicyError }}
|
||||||
|
</div>
|
||||||
|
<div v-if="retentionPolicySuccess" class="text-green-600 text-sm">
|
||||||
|
{{ retentionPolicySuccess }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="border-t pt-4 mt-4">
|
||||||
|
<button
|
||||||
|
@click="handleManualCleanup"
|
||||||
|
:disabled="cleaningUp"
|
||||||
|
class="px-6 py-2 bg-orange-600 text-white rounded-md hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed font-medium"
|
||||||
|
>
|
||||||
|
{{ cleaningUp ? 'Running Cleanup...' : 'Run Cleanup Now' }}
|
||||||
|
</button>
|
||||||
|
<p class="text-xs text-gray-500 mt-2">
|
||||||
|
Manually trigger the cleanup process to delete sermons according to the retention policy.
|
||||||
|
This will also run automatically on a daily schedule.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="cleanupError" class="text-red-600 text-sm">
|
||||||
|
{{ cleanupError }}
|
||||||
|
</div>
|
||||||
|
<div v-if="cleanupSuccess" class="text-green-600 text-sm">
|
||||||
|
{{ cleanupSuccess }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -384,6 +444,21 @@ const error = ref('')
|
|||||||
const success = ref('')
|
const success = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// Database settings state
|
||||||
|
const retentionPolicy = ref('forever')
|
||||||
|
const savingRetentionPolicy = ref(false)
|
||||||
|
const retentionPolicyError = ref('')
|
||||||
|
const retentionPolicySuccess = ref('')
|
||||||
|
const cleaningUp = ref(false)
|
||||||
|
const cleanupError = ref('')
|
||||||
|
const cleanupSuccess = ref('')
|
||||||
|
|
||||||
|
// Fetch current retention policy
|
||||||
|
const { data: retentionPolicyData } = await useFetch('/api/settings/retention-policy')
|
||||||
|
if (retentionPolicyData.value) {
|
||||||
|
retentionPolicy.value = retentionPolicyData.value.retentionPolicy
|
||||||
|
}
|
||||||
|
|
||||||
// Check if selected sermon is archived
|
// Check if selected sermon is archived
|
||||||
const isSelectedSermonArchived = computed(() => {
|
const isSelectedSermonArchived = computed(() => {
|
||||||
if (!selectedSermonId.value || !allSermons.value) return false
|
if (!selectedSermonId.value || !allSermons.value) return false
|
||||||
@@ -648,4 +723,55 @@ async function handleLogout() {
|
|||||||
await $fetch('/api/auth/logout', { method: 'POST' })
|
await $fetch('/api/auth/logout', { method: 'POST' })
|
||||||
await navigateTo('/login')
|
await navigateTo('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRetentionPolicyChange() {
|
||||||
|
retentionPolicyError.value = ''
|
||||||
|
retentionPolicySuccess.value = ''
|
||||||
|
savingRetentionPolicy.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch('/api/settings/retention-policy', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
retentionPolicy: retentionPolicy.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
retentionPolicySuccess.value = 'Retention policy saved successfully!'
|
||||||
|
|
||||||
|
// Clear success message after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
retentionPolicySuccess.value = ''
|
||||||
|
}, 3000)
|
||||||
|
} catch (e: any) {
|
||||||
|
retentionPolicyError.value = e.data?.message || 'Failed to save retention policy'
|
||||||
|
} finally {
|
||||||
|
savingRetentionPolicy.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleManualCleanup() {
|
||||||
|
if (!confirm('Are you sure you want to run the cleanup process?\n\nThis will permanently delete all sermons older than the retention policy. This action cannot be undone.')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupError.value = ''
|
||||||
|
cleanupSuccess.value = ''
|
||||||
|
cleaningUp.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await $fetch('/api/sermons/cleanup', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
|
||||||
|
cleanupSuccess.value = result.message + ` (${result.deletedCount} sermon${result.deletedCount !== 1 ? 's' : ''} deleted)`
|
||||||
|
|
||||||
|
// Refresh sermon list after cleanup
|
||||||
|
await refreshSermons()
|
||||||
|
} catch (e: any) {
|
||||||
|
cleanupError.value = e.data?.message || 'Failed to run cleanup'
|
||||||
|
} finally {
|
||||||
|
cleaningUp.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
57
server/api/sermons/cleanup.post.ts
Normal file
57
server/api/sermons/cleanup.post.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { getSessionUsername } from '~/server/utils/auth'
|
||||||
|
import { getUserByUsername, getSetting, deleteOldSermons } from '~/server/utils/database'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Check if user is authenticated and is admin
|
||||||
|
const username = await getSessionUsername(event)
|
||||||
|
if (!username) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getUserByUsername(username)
|
||||||
|
if (!user || user.is_admin !== 1) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Forbidden - Admin access required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the retention policy setting
|
||||||
|
const setting = getSetting('sermon_retention_policy')
|
||||||
|
const retentionPolicy = setting ? setting.value : 'forever'
|
||||||
|
|
||||||
|
// Map retention policy to days
|
||||||
|
const retentionDaysMap: Record<string, number> = {
|
||||||
|
'forever': 0, // 0 means no deletion
|
||||||
|
'1_month': 30,
|
||||||
|
'3_months': 90,
|
||||||
|
'6_months': 180,
|
||||||
|
'1_year': 365,
|
||||||
|
'3_years': 1095,
|
||||||
|
'5_years': 1825,
|
||||||
|
'10_years': 3650
|
||||||
|
}
|
||||||
|
|
||||||
|
const retentionDays = retentionDaysMap[retentionPolicy] || 0
|
||||||
|
|
||||||
|
if (retentionDays === 0) {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'Retention policy is set to forever, no sermons deleted',
|
||||||
|
deletedCount: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old sermons
|
||||||
|
const result = deleteOldSermons(retentionDays)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Deleted sermons older than ${retentionDays} days`,
|
||||||
|
deletedCount: result.changes,
|
||||||
|
retentionPolicy
|
||||||
|
}
|
||||||
|
})
|
||||||
29
server/api/settings/retention-policy.get.ts
Normal file
29
server/api/settings/retention-policy.get.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { getSessionUsername } from '~/server/utils/auth'
|
||||||
|
import { getUserByUsername, getSetting } from '~/server/utils/database'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Check if user is authenticated and is admin
|
||||||
|
const username = await getSessionUsername(event)
|
||||||
|
if (!username) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getUserByUsername(username)
|
||||||
|
if (!user || user.is_admin !== 1) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Forbidden - Admin access required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the retention policy setting, default to 'forever' if not set
|
||||||
|
const setting = getSetting('sermon_retention_policy')
|
||||||
|
const retentionPolicy = setting ? setting.value : 'forever'
|
||||||
|
|
||||||
|
return {
|
||||||
|
retentionPolicy
|
||||||
|
}
|
||||||
|
})
|
||||||
42
server/api/settings/retention-policy.post.ts
Normal file
42
server/api/settings/retention-policy.post.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { getSessionUsername } from '~/server/utils/auth'
|
||||||
|
import { getUserByUsername, setSetting } from '~/server/utils/database'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Check if user is authenticated and is admin
|
||||||
|
const username = await getSessionUsername(event)
|
||||||
|
if (!username) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = getUserByUsername(username)
|
||||||
|
if (!user || user.is_admin !== 1) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Forbidden - Admin access required'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the retention policy from the request body
|
||||||
|
const body = await readBody(event)
|
||||||
|
const { retentionPolicy } = body
|
||||||
|
|
||||||
|
// Validate the retention policy value
|
||||||
|
const validPolicies = ['forever', '1_month', '3_months', '6_months', '1_year', '3_years', '5_years', '10_years']
|
||||||
|
if (!validPolicies.includes(retentionPolicy)) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Invalid retention policy'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the retention policy setting
|
||||||
|
setSetting('sermon_retention_policy', retentionPolicy)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
retentionPolicy
|
||||||
|
}
|
||||||
|
})
|
||||||
56
server/plugins/sermon-cleanup.ts
Normal file
56
server/plugins/sermon-cleanup.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { getSetting, deleteOldSermons } from '../utils/database'
|
||||||
|
|
||||||
|
// Map retention policy to days
|
||||||
|
const retentionDaysMap: Record<string, number> = {
|
||||||
|
'forever': 0,
|
||||||
|
'1_month': 30,
|
||||||
|
'3_months': 90,
|
||||||
|
'6_months': 180,
|
||||||
|
'1_year': 365,
|
||||||
|
'3_years': 1095,
|
||||||
|
'5_years': 1825,
|
||||||
|
'10_years': 3650
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCleanup() {
|
||||||
|
try {
|
||||||
|
// Get the retention policy setting
|
||||||
|
const setting = getSetting('sermon_retention_policy')
|
||||||
|
const retentionPolicy = setting ? setting.value : 'forever'
|
||||||
|
|
||||||
|
const retentionDays = retentionDaysMap[retentionPolicy] || 0
|
||||||
|
|
||||||
|
if (retentionDays === 0) {
|
||||||
|
console.log('[Sermon Cleanup] Retention policy is set to forever, skipping cleanup')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old sermons
|
||||||
|
const result = deleteOldSermons(retentionDays)
|
||||||
|
|
||||||
|
if (result.changes > 0) {
|
||||||
|
console.log(`[Sermon Cleanup] Deleted ${result.changes} sermon(s) older than ${retentionDays} days`)
|
||||||
|
} else {
|
||||||
|
console.log(`[Sermon Cleanup] No sermons to delete (policy: ${retentionPolicy})`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Sermon Cleanup] Error running cleanup:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineNitroPlugin((nitroApp) => {
|
||||||
|
console.log('[Sermon Cleanup] Scheduling daily cleanup task')
|
||||||
|
|
||||||
|
// Run cleanup once at startup (after a short delay)
|
||||||
|
setTimeout(async () => {
|
||||||
|
console.log('[Sermon Cleanup] Running initial cleanup')
|
||||||
|
await runCleanup()
|
||||||
|
}, 10000) // 10 seconds after startup
|
||||||
|
|
||||||
|
// Schedule cleanup to run daily (every 24 hours)
|
||||||
|
const dailyInterval = 24 * 60 * 60 * 1000 // 24 hours in milliseconds
|
||||||
|
setInterval(async () => {
|
||||||
|
console.log('[Sermon Cleanup] Running scheduled cleanup')
|
||||||
|
await runCleanup()
|
||||||
|
}, dailyInterval)
|
||||||
|
})
|
||||||
@@ -61,6 +61,13 @@ export interface RateLimit {
|
|||||||
reset_at: string
|
reset_at: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Setting {
|
||||||
|
id?: number
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
updated_at?: string
|
||||||
|
}
|
||||||
|
|
||||||
export function getDatabase() {
|
export function getDatabase() {
|
||||||
if (!db) {
|
if (!db) {
|
||||||
const dbPath = join(process.cwd(), 'data', 'sermons.db')
|
const dbPath = join(process.cwd(), 'data', 'sermons.db')
|
||||||
@@ -147,6 +154,15 @@ export function getDatabase() {
|
|||||||
UNIQUE(identifier, endpoint)
|
UNIQUE(identifier, endpoint)
|
||||||
)
|
)
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
key TEXT UNIQUE NOT NULL,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
|
||||||
// Insert default admin user from environment variables with hashed password
|
// Insert default admin user from environment variables with hashed password
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
@@ -354,3 +370,32 @@ export function resetRateLimit(identifier: string, endpoint: string) {
|
|||||||
return db.prepare('DELETE FROM rate_limits WHERE identifier = ? AND endpoint = ?')
|
return db.prepare('DELETE FROM rate_limits WHERE identifier = ? AND endpoint = ?')
|
||||||
.run(identifier, endpoint)
|
.run(identifier, endpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings management functions
|
||||||
|
export function getSetting(key: string) {
|
||||||
|
const db = getDatabase()
|
||||||
|
return db.prepare('SELECT * FROM settings WHERE key = ?').get(key) as Setting | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSetting(key: string, value: string) {
|
||||||
|
const db = getDatabase()
|
||||||
|
const existing = getSetting(key)
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return db.prepare('UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?').run(value, key)
|
||||||
|
} else {
|
||||||
|
return db.prepare('INSERT INTO settings (key, value) VALUES (?, ?)').run(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sermon retention policy functions
|
||||||
|
export function deleteOldSermons(retentionDays: number) {
|
||||||
|
const db = getDatabase()
|
||||||
|
// Calculate the cutoff date based on retention days
|
||||||
|
const cutoffDate = new Date()
|
||||||
|
cutoffDate.setDate(cutoffDate.getDate() - retentionDays)
|
||||||
|
const cutoffDateStr = cutoffDate.toISOString().split('T')[0] // Format as YYYY-MM-DD
|
||||||
|
|
||||||
|
// Delete sermons older than the cutoff date
|
||||||
|
return db.prepare('DELETE FROM sermons WHERE date < ?').run(cutoffDateStr)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user