init auth #1
@@ -7,7 +7,7 @@ const route = useRoute()
|
||||
|
||||
<template>
|
||||
<!-- Login Layout (Full Screen) -->
|
||||
<div v-if="route.name === 'login'" class="h-screen w-full">
|
||||
<div v-if="['login', 'register'].includes(route.name)" class="h-screen w-full">
|
||||
<RouterView />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('auth_code')
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
@@ -18,14 +20,22 @@ const isActive = (path) => {
|
||||
return route.path.startsWith(path)
|
||||
}
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', icon: '🏠', tooltip: 'Home' },
|
||||
{ path: '/assets', icon: '📂', tooltip: 'Assets' },
|
||||
{ path: '/generation', icon: '🎨', tooltip: 'Image Generation' },
|
||||
{ path: '/flexible-generation', icon: '🖌️', tooltip: 'Flexible Generation' },
|
||||
{ path: '/characters', icon: '👥', tooltip: 'Characters' },
|
||||
{ path: '/image-to-prompt', icon: '✨', tooltip: 'Image to Prompt' }
|
||||
]
|
||||
const navItems = computed(() => {
|
||||
const items = [
|
||||
{ path: '/', icon: '🏠', tooltip: 'Home' },
|
||||
{ path: '/assets', icon: '📂', tooltip: 'Assets' },
|
||||
// { path: '/generation', icon: '🎨', tooltip: 'Image Generation' },
|
||||
{ path: '/flexible', icon: '🖌️', tooltip: 'Flexible Generation' },
|
||||
{ path: '/characters', icon: '👥', tooltip: 'Characters' },
|
||||
{ path: '/image-to-prompt', icon: '✨', tooltip: 'Image to Prompt' }
|
||||
]
|
||||
|
||||
if (authStore.isAdmin()) {
|
||||
items.push({ path: '/admin/approvals', icon: '🛡️', tooltip: 'Approvals' })
|
||||
}
|
||||
|
||||
return items
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import RegisterView from '../views/RegisterView.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@@ -9,10 +11,21 @@ const router = createRouter({
|
||||
name: 'login',
|
||||
component: LoginView
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'register',
|
||||
component: RegisterView
|
||||
},
|
||||
{
|
||||
path: '/admin/approvals',
|
||||
name: 'approvals',
|
||||
component: () => import('../views/UserApprovalView.vue'),
|
||||
meta: { requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'characters-home',
|
||||
component: () => import('../views/CharactersView.vue')
|
||||
name: 'home',
|
||||
component: () => import('../views/FlexibleGenerationView.vue')
|
||||
},
|
||||
{
|
||||
path: '/characters',
|
||||
@@ -40,23 +53,32 @@ const router = createRouter({
|
||||
component: () => import('../views/ImageGenerationView.vue')
|
||||
},
|
||||
{
|
||||
path: '/flexible-generation',
|
||||
name: 'flexible-generation',
|
||||
path: '/flexible',
|
||||
name: 'flexible',
|
||||
component: () => import('../views/FlexibleGenerationView.vue')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuth = localStorage.getItem('auth_code')
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const authStore = useAuthStore()
|
||||
const publicPages = ['/login', '/register']
|
||||
const authRequired = !publicPages.includes(to.path)
|
||||
|
||||
if (to.name !== 'login' && !isAuth) {
|
||||
next({ name: 'login' })
|
||||
} else if (to.name === 'login' && isAuth) {
|
||||
next({ name: 'dashboard' })
|
||||
} else {
|
||||
next()
|
||||
if (authRequired) {
|
||||
if (!authStore.isAuthenticated) {
|
||||
return next('/login')
|
||||
}
|
||||
|
||||
// Check if route requires admin
|
||||
if (to.meta.requiresAdmin && !authStore.isAdmin()) {
|
||||
return next('/') // Redirect non-admins to home
|
||||
}
|
||||
} else if ((to.name === 'login' || to.name === 'register') && authStore.isAuthenticated) {
|
||||
return next('/')
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -10,6 +10,19 @@ const api = axios.create({
|
||||
})
|
||||
|
||||
// Request interceptor handling can be added here if needed
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
if (user && user.token) {
|
||||
config.headers['Authorization'] = `${user.tokenType} ${user.token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
api.interceptors.response.use(
|
||||
response => response,
|
||||
error => {
|
||||
|
||||
46
src/services/authService.js
Normal file
46
src/services/authService.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import api from "./api";
|
||||
|
||||
export const authService = {
|
||||
async login(username, password) {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append("username", username);
|
||||
formData.append("password", password);
|
||||
|
||||
const response = await api.post("/auth/token", formData, {
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async register(username, password, fullName) {
|
||||
const payload = {
|
||||
username,
|
||||
password,
|
||||
full_name: fullName,
|
||||
};
|
||||
const response = await api.post("/auth/register", payload);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getMe() {
|
||||
const response = await api.get("/auth/me");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async getApprovals() {
|
||||
const response = await api.get("/admin/approvals");
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async approveUser(username) {
|
||||
const response = await api.post(`/admin/approve/${username}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async denyUser(username) {
|
||||
const response = await api.post(`/admin/deny/${username}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -48,6 +48,11 @@ export const dataService = {
|
||||
return response.data
|
||||
},
|
||||
|
||||
deleteGeneration: async (id) => {
|
||||
const response = await api.delete(`/generations/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
generatePromptFromImage: async (files, prompt) => {
|
||||
const formData = new FormData()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import axios from "axios";
|
||||
import { authService } from "../services/authService";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
const user = ref(JSON.parse(localStorage.getItem("user")) || null);
|
||||
@@ -10,44 +10,61 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
async function login({ username, password }) {
|
||||
error.value = null;
|
||||
try {
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('username', username);
|
||||
formData.append('password', password);
|
||||
|
||||
const response = await axios.post(
|
||||
`${import.meta.env.VITE_API_URL}/login/access-token`,
|
||||
formData,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const { access_token, token_type } = response.data;
|
||||
const data = await authService.login(username, password);
|
||||
|
||||
const userData = {
|
||||
username,
|
||||
token: access_token,
|
||||
tokenType: token_type
|
||||
token: data.access_token,
|
||||
tokenType: data.token_type
|
||||
};
|
||||
|
||||
user.value = userData;
|
||||
isAuthenticated.value = true;
|
||||
localStorage.setItem("user", JSON.stringify(userData));
|
||||
|
||||
// Fetch full profile to get roles/admin status
|
||||
await fetchCurrentUser();
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Login failed:', err);
|
||||
// Backend error format: { detail: "message" } usually
|
||||
error.value = err.response?.data?.detail || 'Login failed. Please check your credentials.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function register({ username, password, fullName }) {
|
||||
error.value = null;
|
||||
try {
|
||||
await authService.register(username, password, fullName);
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error('Registration failed:', err);
|
||||
error.value = err.response?.data?.detail || 'Registration failed. Please try again.';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchCurrentUser() {
|
||||
try {
|
||||
const userData = await authService.getMe();
|
||||
// Merge with existing session data if needed, or just update user object
|
||||
// We keep the token from login, but update other fields
|
||||
user.value = { ...user.value, ...userData };
|
||||
localStorage.setItem("user", JSON.stringify(user.value));
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch user profile:', err);
|
||||
}
|
||||
}
|
||||
|
||||
const isAdmin = () => user.value?.is_admin === true;
|
||||
|
||||
function logout() {
|
||||
user.value = null;
|
||||
isAuthenticated.value = false;
|
||||
localStorage.removeItem("user");
|
||||
}
|
||||
|
||||
return { user, isAuthenticated, error, login, logout };
|
||||
return { user, isAuthenticated, error, login, register, logout, fetchCurrentUser, isAdmin };
|
||||
});
|
||||
|
||||
@@ -365,9 +365,9 @@ const deleteGeneration = async (gen) => {
|
||||
// or we need a way to delete the generation record.
|
||||
// The user said "delete asset", and these are likely linked.
|
||||
// If gen has a result list, we delete the first asset.
|
||||
if (gen.result_list && gen.result_list.length > 0) {
|
||||
await dataService.deleteAsset(gen.result_list[0])
|
||||
}
|
||||
|
||||
await dataService.deleteGeneration(gen.id)
|
||||
|
||||
} catch (e) {
|
||||
console.error('Failed to delete generation', e)
|
||||
// Reload to restore state if failed
|
||||
|
||||
@@ -1,27 +1,32 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
|
||||
const router = useRouter()
|
||||
const code = ref('')
|
||||
const error = ref('')
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!code.value.trim()) {
|
||||
error.value = 'Please enter your access code'
|
||||
const handleLogin = async () => {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
authStore.error = 'Please enter both username and password'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
// Simulate a brief delay for feel
|
||||
if (code.value == 'NE_LEZ_SUDA_SUK') {
|
||||
localStorage.setItem('auth_code', code.value.trim())
|
||||
const success = await authStore.login({
|
||||
username: username.value,
|
||||
password: password.value
|
||||
})
|
||||
|
||||
if (success) {
|
||||
router.push('/')
|
||||
} else {
|
||||
error.value = 'Pshel nah'
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
@@ -41,33 +46,44 @@ const handleLogin = () => {
|
||||
class="w-16 h-16 bg-gradient-to-tr from-violet-600 to-cyan-500 rounded-2xl flex items-center justify-center text-3xl mb-6 mx-auto shadow-lg shadow-violet-500/20">
|
||||
✨
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 tracking-tight">Access Gate</h1>
|
||||
<p class="text-slate-400 text-sm">Please enter your authorized access code</p>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 tracking-tight">Welcome Back</h1>
|
||||
<p class="text-slate-400 text-sm">Sign in to access your workspace</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Secure
|
||||
Code</label>
|
||||
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Username</label>
|
||||
<div class="relative">
|
||||
<InputText v-model="code" type="password" placeholder="••••••••"
|
||||
<InputText v-model="username" placeholder="Enter username"
|
||||
class="w-full !bg-slate-900/50 !border-white/10 !text-white !p-4 !rounded-xl focus:!border-violet-500 !transition-all"
|
||||
@keyup.enter="handleLogin" />
|
||||
<i class="pi pi-lock absolute right-4 top-1/2 -translate-y-1/2 text-slate-500"></i>
|
||||
<i class="pi pi-user absolute right-4 top-1/2 -translate-y-1/2 text-slate-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="text-red-400 text-xs ml-1 flex items-center gap-1.5 animate-bounce">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
{{ error }}
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Password</label>
|
||||
<div class="relative">
|
||||
<Password v-model="password" :feedback="false" placeholder="••••••••"
|
||||
inputClass="w-full !bg-slate-900/50 !border-white/10 !text-white !p-4 !rounded-xl focus:!border-violet-500 !transition-all"
|
||||
toggleMask @keyup.enter="handleLogin" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button label="Initialize Session" icon="pi pi-bolt" :loading="loading" @click="handleLogin"
|
||||
class="w-full !py-4 !rounded-xl !bg-gradient-to-r !from-violet-600 !to-cyan-500 !border-none !font-bold !text-lg !shadow-xl !shadow-violet-900/20 hover:scale-[1.02] active:scale-[0.98] transition-all" />
|
||||
</div>
|
||||
<div v-if="authStore.error" class="text-red-400 text-xs ml-1 flex items-center gap-1.5 animate-bounce">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
{{ authStore.error }}
|
||||
</div>
|
||||
|
||||
<div class="pt-4 border-t border-white/5 w-full text-center">
|
||||
<p class="text-slate-600 text-[10px] uppercase font-bold tracking-widest">Authorized Personnel Only</p>
|
||||
<Button label="Sign In" icon="pi pi-sign-in" :loading="loading" @click="handleLogin"
|
||||
class="w-full !py-4 !rounded-xl !bg-gradient-to-r !from-violet-600 !to-cyan-500 !border-none !font-bold !text-lg !shadow-xl !shadow-violet-900/20 hover:scale-[1.02] active:scale-[0.98] transition-all" />
|
||||
|
||||
<div class="text-center mt-2">
|
||||
<span class="text-slate-500 text-sm">Don't have an account? </span>
|
||||
<router-link to="/register" class="text-violet-400 hover:text-violet-300 transition-colors text-sm font-semibold">
|
||||
Create one
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,4 +111,8 @@ const handleLogin = () => {
|
||||
.animate-pulse {
|
||||
animation: pulse 8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
:deep(.p-password-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
138
src/views/RegisterView.vue
Normal file
138
src/views/RegisterView.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import Button from 'primevue/button'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const fullName = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
const handleRegister = async () => {
|
||||
if (!username.value.trim() || !password.value.trim()) {
|
||||
authStore.error = 'Please fill in all required fields'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
const success = await authStore.register({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
fullName: fullName.value
|
||||
})
|
||||
|
||||
if (success) {
|
||||
// Auto-login after successful registration
|
||||
const loginSuccess = await authStore.login({
|
||||
username: username.value,
|
||||
password: password.value
|
||||
})
|
||||
|
||||
if (loginSuccess) {
|
||||
router.push('/')
|
||||
} else {
|
||||
router.push('/login')
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen w-full flex items-center justify-center bg-slate-950 overflow-hidden relative">
|
||||
<!-- Animated Background Blur -->
|
||||
<div class="absolute top-1/4 left-1/4 w-96 h-96 bg-violet-600/20 blur-[120px] rounded-full animate-pulse"></div>
|
||||
<div class="absolute bottom-1/4 right-1/4 w-96 h-96 bg-cyan-600/20 blur-[120px] rounded-full animate-pulse"
|
||||
style="animation-delay: 1s;"></div>
|
||||
|
||||
<div
|
||||
class="glass-panel p-10 rounded-3xl border border-white/10 bg-white/5 backdrop-blur-xl w-full max-w-md z-10 flex flex-col items-center gap-8 shadow-2xl">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="w-16 h-16 bg-gradient-to-tr from-violet-600 to-cyan-500 rounded-2xl flex items-center justify-center text-3xl mb-6 mx-auto shadow-lg shadow-violet-500/20">
|
||||
🚀
|
||||
</div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2 tracking-tight">Create Account</h1>
|
||||
<p class="text-slate-400 text-sm">Join the workspace</p>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Username</label>
|
||||
<div class="relative">
|
||||
<InputText v-model="username" placeholder="Choose a username"
|
||||
class="w-full !bg-slate-900/50 !border-white/10 !text-white !p-4 !rounded-xl focus:!border-violet-500 !transition-all" />
|
||||
<i class="pi pi-user absolute right-4 top-1/2 -translate-y-1/2 text-slate-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Full Name (Optional)</label>
|
||||
<div class="relative">
|
||||
<InputText v-model="fullName" placeholder="John Doe"
|
||||
class="w-full !bg-slate-900/50 !border-white/10 !text-white !p-4 !rounded-xl focus:!border-violet-500 !transition-all" />
|
||||
<i class="pi pi-id-card absolute right-4 top-1/2 -translate-y-1/2 text-slate-500"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Password</label>
|
||||
<div class="relative">
|
||||
<Password v-model="password" placeholder="••••••••"
|
||||
inputClass="w-full !bg-slate-900/50 !border-white/10 !text-white !p-4 !rounded-xl focus:!border-violet-500 !transition-all"
|
||||
toggleMask @keyup.enter="handleRegister" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="authStore.error" class="text-red-400 text-xs ml-1 flex items-center gap-1.5 animate-bounce">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
{{ authStore.error }}
|
||||
</div>
|
||||
|
||||
<Button label="Create Account" icon="pi pi-user-plus" :loading="loading" @click="handleRegister"
|
||||
class="w-full !py-4 !rounded-xl !bg-gradient-to-r !from-violet-600 !to-cyan-500 !border-none !font-bold !text-lg !shadow-xl !shadow-violet-900/20 hover:scale-[1.02] active:scale-[0.98] transition-all" />
|
||||
|
||||
<div class="text-center mt-2">
|
||||
<span class="text-slate-500 text-sm">Already have an account? </span>
|
||||
<router-link to="/login" class="text-violet-400 hover:text-violet-300 transition-colors text-sm font-semibold">
|
||||
Sign in
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.glass-panel {
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
:deep(.p-password-input) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
134
src/views/UserApprovalView.vue
Normal file
134
src/views/UserApprovalView.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { authService } from '@/services/authService'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import Button from 'primevue/button'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Tag from 'primevue/tag'
|
||||
|
||||
const users = ref([])
|
||||
const loading = ref(true)
|
||||
const toast = useToast()
|
||||
const processing = ref({})
|
||||
|
||||
const loadApprovals = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
users.value = await authService.getApprovals()
|
||||
} catch (e) {
|
||||
console.error('Failed to load approvals', e)
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to load pending approvals', life: 3000 })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleApprove = async (username) => {
|
||||
processing.value[username] = true
|
||||
try {
|
||||
await authService.approveUser(username)
|
||||
toast.add({ severity: 'success', summary: 'Approved', detail: `User ${username} approved`, life: 3000 })
|
||||
await loadApprovals()
|
||||
} catch (e) {
|
||||
console.error('Failed to approve user', e)
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to approve user', life: 3000 })
|
||||
} finally {
|
||||
processing.value[username] = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeny = async (username) => {
|
||||
processing.value[username] = true
|
||||
try {
|
||||
await authService.denyUser(username)
|
||||
toast.add({ severity: 'info', summary: 'Denied', detail: `User ${username} denied`, life: 3000 })
|
||||
await loadApprovals()
|
||||
} catch (e) {
|
||||
console.error('Failed to deny user', e)
|
||||
toast.add({ severity: 'error', summary: 'Error', detail: 'Failed to deny user', life: 3000 })
|
||||
} finally {
|
||||
processing.value[username] = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadApprovals()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 md:p-8 min-h-screen">
|
||||
<div class="max-w-7xl mx-auto space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">User Approvals</h1>
|
||||
<p class="text-slate-400">Manage pending user registration requests</p>
|
||||
</div>
|
||||
<Button icon="pi pi-refresh" rounded outlined @click="loadApprovals" :loading="loading" />
|
||||
</div>
|
||||
|
||||
<div class="glass-panel rounded-2xl p-1 overflow-hidden">
|
||||
<DataTable :value="users" :loading="loading" class="w-full"
|
||||
pt:headerRow:class="bg-white/5 text-slate-300"
|
||||
pt:bodyRow:class="hover:bg-white/5 transition-colors border-b border-white/5">
|
||||
|
||||
<template #empty>
|
||||
<div class="p-8 text-center text-slate-500">
|
||||
No pending approvals found.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="username" header="Username" class="text-white font-medium"></Column>
|
||||
<Column field="full_name" header="Full Name" class="text-slate-300"></Column>
|
||||
<Column field="email" header="Email" class="text-slate-400">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.email || '-' }}
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="created_at" header="Registered" class="text-slate-400">
|
||||
<template #body="slotProps">
|
||||
{{ new Date(slotProps.data.created_at).toLocaleDateString() }}
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column header="Actions" alignFrozen="right" frozen class="w-[200px]">
|
||||
<template #body="slotProps">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<Button icon="pi pi-check" severity="success" text rounded
|
||||
@click="handleApprove(slotProps.data.username)"
|
||||
:loading="processing[slotProps.data.username]" tooltip="Approve" />
|
||||
<Button icon="pi pi-times" severity="danger" text rounded
|
||||
@click="handleDeny(slotProps.data.username)"
|
||||
:loading="processing[slotProps.data.username]" tooltip="Deny" />
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.glass-panel {
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
:deep(.p-datatable-header-cell) {
|
||||
background: transparent !important;
|
||||
color: inherit;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
:deep(.p-datatable-tbody > tr) {
|
||||
background: transparent !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:deep(.p-datatable-tbody > tr:hover) {
|
||||
background: rgba(255, 255, 255, 0.03) !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user