370 lines
13 KiB
Plaintext
370 lines
13 KiB
Plaintext
---
|
|
import DashboardLayout from '../../layouts/DashboardLayout.astro';
|
|
import { Icon } from 'astro-icon/components';
|
|
import { db } from '../../db';
|
|
import { apiTokens } from '../../db/schema';
|
|
import { eq, desc } from 'drizzle-orm';
|
|
|
|
const user = Astro.locals.user;
|
|
if (!user) return Astro.redirect('/login');
|
|
|
|
const url = new URL(Astro.request.url);
|
|
const successType = url.searchParams.get('success');
|
|
|
|
const userTokens = await db.select()
|
|
.from(apiTokens)
|
|
.where(eq(apiTokens.userId, user.id))
|
|
.orderBy(desc(apiTokens.createdAt))
|
|
.all();
|
|
---
|
|
|
|
<DashboardLayout title="Account Settings - Chronus">
|
|
<div class="max-w-4xl mx-auto px-4 sm:px-6">
|
|
<h1 class="text-2xl sm:text-3xl font-bold mb-6 sm:mb-8 text-primary">
|
|
Account Settings
|
|
</h1>
|
|
|
|
{/* Success Messages */}
|
|
{successType === 'profile' && (
|
|
<div class="alert alert-success mb-6">
|
|
<Icon name="heroicons:check-circle" class="w-5 h-5 sm:w-6 sm:h-6 shrink-0" />
|
|
<span class="text-sm sm:text-base">Profile updated successfully!</span>
|
|
</div>
|
|
)}
|
|
|
|
{successType === 'password' && (
|
|
<div class="alert alert-success mb-6">
|
|
<Icon name="heroicons:check-circle" class="w-5 h-5 sm:w-6 sm:h-6 shrink-0" />
|
|
<span class="text-sm sm:text-base">Password changed successfully!</span>
|
|
</div>
|
|
)}
|
|
|
|
<!-- Profile Information -->
|
|
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
|
<div class="card-body p-4 sm:p-6">
|
|
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
|
<Icon name="heroicons:user-circle" class="w-5 h-5 sm:w-6 sm:h-6" />
|
|
Profile Information
|
|
</h2>
|
|
|
|
<form action="/api/user/update-profile" method="POST" class="space-y-5">
|
|
<div class="form-control">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-medium text-sm sm:text-base">Full Name</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
value={user.name}
|
|
placeholder="Your full name"
|
|
class="input input-bordered w-full"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-medium text-sm sm:text-base">Email</span>
|
|
</label>
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
value={user.email}
|
|
placeholder="your@email.com"
|
|
class="input input-bordered w-full"
|
|
disabled
|
|
/>
|
|
<div class="label pt-2">
|
|
<span class="label-text-alt text-base-content/60 text-xs sm:text-sm">Email cannot be changed</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-4">
|
|
<button type="submit" class="btn btn-primary w-full sm:w-auto">
|
|
<Icon name="heroicons:check" class="w-5 h-5" />
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Change Password -->
|
|
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
|
<div class="card-body p-4 sm:p-6">
|
|
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
|
<Icon name="heroicons:key" class="w-5 h-5 sm:w-6 sm:h-6" />
|
|
Change Password
|
|
</h2>
|
|
|
|
<form action="/api/user/change-password" method="POST" class="space-y-5">
|
|
<div class="form-control">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-medium text-sm sm:text-base">Current Password</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
name="currentPassword"
|
|
placeholder="Enter current password"
|
|
class="input input-bordered w-full"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-medium text-sm sm:text-base">New Password</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
name="newPassword"
|
|
placeholder="Enter new password"
|
|
class="input input-bordered w-full"
|
|
required
|
|
minlength="8"
|
|
/>
|
|
<div class="label pt-2">
|
|
<span class="label-text-alt text-base-content/60 text-xs sm:text-sm">Minimum 8 characters</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-control">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-medium text-sm sm:text-base">Confirm New Password</span>
|
|
</label>
|
|
<input
|
|
type="password"
|
|
name="confirmPassword"
|
|
placeholder="Confirm new password"
|
|
class="input input-bordered w-full"
|
|
required
|
|
minlength="8"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex justify-end pt-4">
|
|
<button type="submit" class="btn btn-primary w-full sm:w-auto">
|
|
<Icon name="heroicons:lock-closed" class="w-5 h-5" />
|
|
Update Password
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- API Tokens -->
|
|
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
|
|
<div class="card-body p-4 sm:p-6">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="card-title text-lg sm:text-xl">
|
|
<Icon name="heroicons:code-bracket-square" class="w-5 h-5 sm:w-6 sm:h-6" />
|
|
API Tokens
|
|
</h2>
|
|
<button class="btn btn-primary btn-sm" onclick="createTokenModal.showModal()">
|
|
<Icon name="heroicons:plus" class="w-4 h-4" />
|
|
Create Token
|
|
</button>
|
|
</div>
|
|
|
|
<div class="overflow-x-auto">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th>Last Used</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{userTokens.length === 0 ? (
|
|
<tr>
|
|
<td colspan="4" class="text-center text-base-content/60 py-4">
|
|
No API tokens found. Create one to access the API.
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
userTokens.map(token => (
|
|
<tr>
|
|
<td class="font-medium">{token.name}</td>
|
|
<td class="text-sm">
|
|
{token.lastUsedAt ? token.lastUsedAt.toLocaleDateString() : 'Never'}
|
|
</td>
|
|
<td class="text-sm">
|
|
{token.createdAt ? token.createdAt.toLocaleDateString() : 'N/A'}
|
|
</td>
|
|
<td>
|
|
<button
|
|
class="btn btn-ghost btn-xs text-error"
|
|
onclick={`deleteToken('${token.id}')`}
|
|
>
|
|
<Icon name="heroicons:trash" class="w-4 h-4" />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Info -->
|
|
<div class="card bg-base-100 shadow-xl border border-base-200">
|
|
<div class="card-body p-4 sm:p-6">
|
|
<h2 class="card-title mb-6 text-lg sm:text-xl">
|
|
<Icon name="heroicons:information-circle" class="w-5 h-5 sm:w-6 sm:h-6" />
|
|
Account Information
|
|
</h2>
|
|
|
|
<div class="space-y-3">
|
|
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-300 gap-2 sm:gap-0">
|
|
<span class="text-base-content/70 text-sm sm:text-base">Account ID</span>
|
|
<span class="font-mono text-xs sm:text-sm break-all">{user.id}</span>
|
|
</div>
|
|
<div class="flex flex-col sm:flex-row sm:justify-between py-3 border-b border-base-300 gap-2 sm:gap-0">
|
|
<span class="text-base-content/70 text-sm sm:text-base">Email</span>
|
|
<span class="text-sm sm:text-base break-all">{user.email}</span>
|
|
</div>
|
|
<div class="flex flex-col sm:flex-row sm:justify-between py-3 gap-2 sm:gap-0">
|
|
<span class="text-base-content/70 text-sm sm:text-base">Site Administrator</span>
|
|
<span class={user.isSiteAdmin ? "badge badge-primary" : "badge badge-ghost"}>
|
|
{user.isSiteAdmin ? "Yes" : "No"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create Token Modal -->
|
|
<dialog id="createTokenModal" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg">Create API Token</h3>
|
|
<p class="py-4 text-sm text-base-content/70">
|
|
API tokens allow you to authenticate with the API programmatically.
|
|
Give your token a descriptive name.
|
|
</p>
|
|
|
|
<form id="createTokenForm" method="dialog" class="space-y-4">
|
|
<div class="form-control">
|
|
<label class="label pb-2">
|
|
<span class="label-text font-medium">Token Name</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
id="tokenName"
|
|
placeholder="e.g. CI/CD Pipeline"
|
|
class="input input-bordered w-full"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button type="button" class="btn" onclick="createTokenModal.close()">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Generate Token</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<form method="dialog" class="modal-backdrop">
|
|
<button>close</button>
|
|
</form>
|
|
</dialog>
|
|
|
|
<!-- Show Token Modal -->
|
|
<dialog id="showTokenModal" class="modal">
|
|
<div class="modal-box">
|
|
<h3 class="font-bold text-lg text-success flex items-center gap-2">
|
|
<Icon name="heroicons:check-circle" class="w-6 h-6" />
|
|
Token Created
|
|
</h3>
|
|
<p class="py-4">
|
|
Make sure to copy your personal access token now. You won't be able to see it again!
|
|
</p>
|
|
|
|
<div class="bg-base-200 p-4 rounded-lg break-all font-mono text-sm relative group">
|
|
<span id="newTokenDisplay"></span>
|
|
<button
|
|
class="absolute top-2 right-2 btn btn-xs btn-ghost opacity-0 group-hover:opacity-100 transition-opacity"
|
|
onclick="copyToken()"
|
|
title="Copy to clipboard"
|
|
>
|
|
<Icon name="heroicons:clipboard" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button class="btn btn-primary" onclick="closeShowTokenModal()">Done</button>
|
|
</div>
|
|
</div>
|
|
</dialog>
|
|
|
|
<script is:inline>
|
|
// Handle Token Creation
|
|
const createTokenForm = document.getElementById('createTokenForm');
|
|
|
|
createTokenForm.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const name = document.getElementById('tokenName').value;
|
|
const formData = new FormData();
|
|
formData.append('name', name);
|
|
|
|
try {
|
|
const response = await fetch('/api/user/tokens', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
document.getElementById('createTokenModal').close();
|
|
document.getElementById('newTokenDisplay').innerText = data.token;
|
|
document.getElementById('showTokenModal').showModal();
|
|
document.getElementById('tokenName').value = ''; // Reset form
|
|
} else {
|
|
alert('Failed to create token');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating token:', error);
|
|
alert('An error occurred');
|
|
}
|
|
});
|
|
|
|
// Handle Token Copy
|
|
function copyToken() {
|
|
const token = document.getElementById('newTokenDisplay').innerText;
|
|
navigator.clipboard.writeText(token);
|
|
}
|
|
|
|
// Handle Closing Show Token Modal (refresh page to show new token in list)
|
|
function closeShowTokenModal() {
|
|
document.getElementById('showTokenModal').close();
|
|
window.location.reload();
|
|
}
|
|
|
|
// Handle Token Deletion
|
|
async function deleteToken(id) {
|
|
if (!confirm('Are you sure you want to revoke this token? Any applications using it will stop working.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/user/tokens/${id}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
alert('Failed to delete token');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting token:', error);
|
|
alert('An error occurred');
|
|
}
|
|
}
|
|
</script>
|
|
</DashboardLayout>
|