Compare commits
2 Commits
7732856f90
...
0b9746bce8
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b9746bce8 | |||
| 6856449075 |
@@ -7,7 +7,7 @@ const route = useRoute()
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Login Layout (Full Screen) -->
|
<!-- 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 />
|
<RouterView />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useRouter, useRoute } from 'vue-router'
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('auth_code')
|
authStore.logout()
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,14 +20,22 @@ const isActive = (path) => {
|
|||||||
return route.path.startsWith(path)
|
return route.path.startsWith(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
const navItems = [
|
const navItems = computed(() => {
|
||||||
|
const items = [
|
||||||
{ path: '/', icon: '🏠', tooltip: 'Home' },
|
{ path: '/', icon: '🏠', tooltip: 'Home' },
|
||||||
{ path: '/assets', icon: '📂', tooltip: 'Assets' },
|
{ path: '/assets', icon: '📂', tooltip: 'Assets' },
|
||||||
{ path: '/generation', icon: '🎨', tooltip: 'Image Generation' },
|
// { path: '/generation', icon: '🎨', tooltip: 'Image Generation' },
|
||||||
{ path: '/flexible-generation', icon: '🖌️', tooltip: 'Flexible Generation' },
|
{ path: '/flexible', icon: '🖌️', tooltip: 'Flexible Generation' },
|
||||||
{ path: '/characters', icon: '👥', tooltip: 'Characters' },
|
{ path: '/characters', icon: '👥', tooltip: 'Characters' },
|
||||||
{ path: '/image-to-prompt', icon: '✨', tooltip: 'Image to Prompt' }
|
{ path: '/image-to-prompt', icon: '✨', tooltip: 'Image to Prompt' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if (authStore.isAdmin()) {
|
||||||
|
items.push({ path: '/admin/approvals', icon: '🛡️', tooltip: 'Approvals' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import LoginView from '../views/LoginView.vue'
|
import LoginView from '../views/LoginView.vue'
|
||||||
|
import RegisterView from '../views/RegisterView.vue'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
@@ -9,10 +11,21 @@ const router = createRouter({
|
|||||||
name: 'login',
|
name: 'login',
|
||||||
component: LoginView
|
component: LoginView
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/register',
|
||||||
|
name: 'register',
|
||||||
|
component: RegisterView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/approvals',
|
||||||
|
name: 'approvals',
|
||||||
|
component: () => import('../views/UserApprovalView.vue'),
|
||||||
|
meta: { requiresAdmin: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'characters-home',
|
name: 'home',
|
||||||
component: () => import('../views/CharactersView.vue')
|
component: () => import('../views/FlexibleGenerationView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/characters',
|
path: '/characters',
|
||||||
@@ -40,23 +53,32 @@ const router = createRouter({
|
|||||||
component: () => import('../views/ImageGenerationView.vue')
|
component: () => import('../views/ImageGenerationView.vue')
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/flexible-generation',
|
path: '/flexible',
|
||||||
name: 'flexible-generation',
|
name: 'flexible',
|
||||||
component: () => import('../views/FlexibleGenerationView.vue')
|
component: () => import('../views/FlexibleGenerationView.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
const isAuth = localStorage.getItem('auth_code')
|
const authStore = useAuthStore()
|
||||||
|
const publicPages = ['/login', '/register']
|
||||||
|
const authRequired = !publicPages.includes(to.path)
|
||||||
|
|
||||||
if (to.name !== 'login' && !isAuth) {
|
if (authRequired) {
|
||||||
next({ name: 'login' })
|
if (!authStore.isAuthenticated) {
|
||||||
} else if (to.name === 'login' && isAuth) {
|
return next('/login')
|
||||||
next({ name: 'dashboard' })
|
|
||||||
} else {
|
|
||||||
next()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
export default router
|
||||||
|
|||||||
@@ -10,6 +10,19 @@ const api = axios.create({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Request interceptor handling can be added here if needed
|
// 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(
|
api.interceptors.response.use(
|
||||||
response => response,
|
response => response,
|
||||||
error => {
|
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
|
return response.data
|
||||||
},
|
},
|
||||||
|
|
||||||
|
deleteGeneration: async (id) => {
|
||||||
|
const response = await api.delete(`/generations/${id}`)
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
|
||||||
generatePromptFromImage: async (files, prompt) => {
|
generatePromptFromImage: async (files, prompt) => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import axios from "axios";
|
import { authService } from "../services/authService";
|
||||||
|
|
||||||
export const useAuthStore = defineStore("auth", () => {
|
export const useAuthStore = defineStore("auth", () => {
|
||||||
const user = ref(JSON.parse(localStorage.getItem("user")) || null);
|
const user = ref(JSON.parse(localStorage.getItem("user")) || null);
|
||||||
@@ -10,44 +10,61 @@ export const useAuthStore = defineStore("auth", () => {
|
|||||||
async function login({ username, password }) {
|
async function login({ username, password }) {
|
||||||
error.value = null;
|
error.value = null;
|
||||||
try {
|
try {
|
||||||
const formData = new URLSearchParams();
|
const data = await authService.login(username, password);
|
||||||
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 userData = {
|
const userData = {
|
||||||
username,
|
username,
|
||||||
token: access_token,
|
token: data.access_token,
|
||||||
tokenType: token_type
|
tokenType: data.token_type
|
||||||
};
|
};
|
||||||
|
|
||||||
user.value = userData;
|
user.value = userData;
|
||||||
isAuthenticated.value = true;
|
isAuthenticated.value = true;
|
||||||
localStorage.setItem("user", JSON.stringify(userData));
|
localStorage.setItem("user", JSON.stringify(userData));
|
||||||
|
|
||||||
|
// Fetch full profile to get roles/admin status
|
||||||
|
await fetchCurrentUser();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Login failed:', err);
|
console.error('Login failed:', err);
|
||||||
|
// Backend error format: { detail: "message" } usually
|
||||||
error.value = err.response?.data?.detail || 'Login failed. Please check your credentials.';
|
error.value = err.response?.data?.detail || 'Login failed. Please check your credentials.';
|
||||||
return false;
|
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() {
|
function logout() {
|
||||||
user.value = null;
|
user.value = null;
|
||||||
isAuthenticated.value = false;
|
isAuthenticated.value = false;
|
||||||
localStorage.removeItem("user");
|
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.
|
// or we need a way to delete the generation record.
|
||||||
// The user said "delete asset", and these are likely linked.
|
// The user said "delete asset", and these are likely linked.
|
||||||
// If gen has a result list, we delete the first asset.
|
// 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) {
|
} catch (e) {
|
||||||
console.error('Failed to delete generation', e)
|
console.error('Failed to delete generation', e)
|
||||||
// Reload to restore state if failed
|
// Reload to restore state if failed
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import Button from 'primevue/button'
|
import Button from 'primevue/button'
|
||||||
import InputText from 'primevue/inputtext'
|
import InputText from 'primevue/inputtext'
|
||||||
|
import Password from 'primevue/password'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const code = ref('')
|
const authStore = useAuthStore()
|
||||||
const error = ref('')
|
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = async () => {
|
||||||
if (!code.value.trim()) {
|
if (!username.value.trim() || !password.value.trim()) {
|
||||||
error.value = 'Please enter your access code'
|
authStore.error = 'Please enter both username and password'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
// Simulate a brief delay for feel
|
const success = await authStore.login({
|
||||||
if (code.value == 'NE_LEZ_SUDA_SUK') {
|
username: username.value,
|
||||||
localStorage.setItem('auth_code', code.value.trim())
|
password: password.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (success) {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} else {
|
|
||||||
error.value = 'Pshel nah'
|
|
||||||
}
|
}
|
||||||
loading.value = false
|
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">
|
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>
|
</div>
|
||||||
<h1 class="text-3xl font-bold text-white mb-2 tracking-tight">Access Gate</h1>
|
<h1 class="text-3xl font-bold text-white mb-2 tracking-tight">Welcome Back</h1>
|
||||||
<p class="text-slate-400 text-sm">Please enter your authorized access code</p>
|
<p class="text-slate-400 text-sm">Sign in to access your workspace</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full flex flex-col gap-4">
|
<div class="w-full flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Secure
|
<label class="text-slate-400 text-[10px] font-bold uppercase tracking-widest ml-1">Username</label>
|
||||||
Code</label>
|
|
||||||
<div class="relative">
|
<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"
|
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" />
|
@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>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="text-red-400 text-xs ml-1 flex items-center gap-1.5 animate-bounce">
|
<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>
|
||||||
|
|
||||||
|
<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>
|
<i class="pi pi-exclamation-circle"></i>
|
||||||
{{ error }}
|
{{ authStore.error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button label="Initialize Session" icon="pi pi-bolt" :loading="loading" @click="handleLogin"
|
<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" />
|
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 class="pt-4 border-t border-white/5 w-full text-center">
|
<div class="text-center mt-2">
|
||||||
<p class="text-slate-600 text-[10px] uppercase font-bold tracking-widest">Authorized Personnel Only</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,4 +111,8 @@ const handleLogin = () => {
|
|||||||
.animate-pulse {
|
.animate-pulse {
|
||||||
animation: pulse 8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
animation: pulse 8s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.p-password-input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
</style>
|
</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