LET'S FUCKING GOOOOO #18
@@ -1,5 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import Toast from './components/ui/toast/Toast.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLayout>
|
<NuxtLayout>
|
||||||
|
<Title>Reddit for scientists</Title>
|
||||||
|
<Toast />
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
const store = useNotificationStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fixed bottom-6 right-6 z-[9999] flex flex-col gap-3 w-80">
|
||||||
|
<TransitionGroup name="toast">
|
||||||
|
<div v-for="toast in store.notifications" :key="toast.id" :class="[
|
||||||
|
'p-4 rounded-lg shadow-xl border text-sm font-medium transition-all duration-300',
|
||||||
|
toast.type === 'error'
|
||||||
|
? 'bg-red-50 border-red-200 text-red-800'
|
||||||
|
: 'bg-green-50 border-green-200 text-green-800'
|
||||||
|
]">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>{{ toast.message }}</span>
|
||||||
|
<button class="ml-4 opacity-50 hover:opacity-100" @click="store.remove(toast.id)">×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.toast-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -40,22 +40,23 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<button @click="toggleDark" class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700">
|
<button class="p-2 rounded-lg bg-gray-200 dark:bg-gray-700" @click="toggleDark">
|
||||||
<span v-if="isDark">☀️</span>
|
<span v-if="isDark">☀️</span>
|
||||||
<span v-else>🌙</span>
|
<span v-else>🌙</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<template v-if="authStore?.token">
|
<template v-if="authStore?.token">
|
||||||
<label class="py-2 text-sm font-semibold">{{ authStore?.user.name }}</label>
|
<label class="py-2 text-sm font-semibold">{{ authStore?.user.name }}</label>
|
||||||
<button @click.prevent="logout"
|
<button
|
||||||
class="px-4 py-2 text-sm font-semibold text-red-800 bg-red-100 hover:bg-red-200 rounded-lg transition-all duration-200">
|
class="px-4 py-2 text-sm font-semibold text-red-800 bg-red-100 hover:bg-red-200 rounded-lg transition-all duration-200"
|
||||||
|
@click.prevent="logout">
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<nuxt-link to="/auth/login"
|
<nuxt-link to="/auth/login"
|
||||||
class="px-5 py-2 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm transition-all">
|
class="px-5 py-2 text-sm font-semibold text-white! bg-blue-600 hover:bg-blue-700 rounded-lg shadow-sm transition-all">
|
||||||
Login
|
Login
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default defineNuxtRouteMiddleware((to, from) => {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const isLoggedIn = !!(authStore.user || authStore.token);
|
||||||
|
const mustChangePassword = authStore.user?.mustChangePassword === true;
|
||||||
|
|
||||||
|
const isAuthPage = to.path.startsWith('/auth');
|
||||||
|
const isUpdatePasswordPage = to.path === '/profile/update-password';
|
||||||
|
|
||||||
|
if (!isLoggedIn && !isAuthPage) {
|
||||||
|
return navigateTo('/auth/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn && mustChangePassword && !isUpdatePasswordPage) {
|
||||||
|
return navigateTo('/profile/update-password');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoggedIn && isAuthPage) {
|
||||||
|
return navigateTo('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<h1>Login Form</h1>
|
|
||||||
<div>Username: <input v-model="loginFormData.username" /></div>
|
|
||||||
<div>Password: <input v-model="loginFormData.password" /></div>
|
|
||||||
<button @click="login">LOGIN</button>
|
|
||||||
<button @click="reset">RESET</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="token">
|
|
||||||
<h2>API Request Form</h2>
|
|
||||||
<div>
|
|
||||||
Request: <code>GET {{ api }}</code>/ <input v-model="loginFormData.password" />
|
|
||||||
</div>
|
|
||||||
<div>Token: {{ token }}</div>
|
|
||||||
<button @click="sendRequest">SEND REQUEST</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="messages.length > 0">
|
|
||||||
<h2>Messages</h2>
|
|
||||||
<div v-for="message in messages">
|
|
||||||
<pre>{{ message }}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
const config = useRuntimeConfig();
|
|
||||||
const api = config.public.apiBase;
|
|
||||||
const loginFormData = reactive({
|
|
||||||
username: null,
|
|
||||||
password: null,
|
|
||||||
});
|
|
||||||
const token = ref(null);
|
|
||||||
const messages = ref([]);
|
|
||||||
async function login() {
|
|
||||||
reset();
|
|
||||||
try {
|
|
||||||
await $fetch(`${api}/auth/login`, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
Accept: "application/json",
|
|
||||||
},
|
|
||||||
body: loginFormData,
|
|
||||||
onResponse({ request, response, options }) {
|
|
||||||
messages.value.push({
|
|
||||||
method: options.method,
|
|
||||||
request: request,
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
payload: response._data,
|
|
||||||
});
|
|
||||||
if (response.status == 200) token.value = response._data;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("login request failed: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function reset() {
|
|
||||||
token.value = null;
|
|
||||||
messages.value = [];
|
|
||||||
}
|
|
||||||
async function sendRequest() {
|
|
||||||
try {
|
|
||||||
await $fetch(`${api}/`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
Authorization: `Bearer ${token.value}`,
|
|
||||||
},
|
|
||||||
onResponse({ request, response, options }) {
|
|
||||||
messages.value.push({
|
|
||||||
method: options.method,
|
|
||||||
request: request,
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
payload: response._data,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("api request failed: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<script setup>
|
||||||
|
const email = ref("");
|
||||||
|
const error = ref(null);
|
||||||
|
const message = ref("");
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const api = config.public.apiBase;
|
||||||
|
const router = useRouter();
|
||||||
|
const notifications = useNotificationStore();
|
||||||
|
|
||||||
|
async function handleResetRequest() {
|
||||||
|
error.value = null;
|
||||||
|
message.value = "";
|
||||||
|
|
||||||
|
if (!email.value) {
|
||||||
|
error.value = "Email is required.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\S+@\S+\.\S+$/.test(email.value)) {
|
||||||
|
error.value = "Please enter a valid email address.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch(`${api}/auth/recover-password`, {
|
||||||
|
method: "POST",
|
||||||
|
body: { email: email.value },
|
||||||
|
onResponse({ response }) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
notifications.success("Password reset email sent.");
|
||||||
|
router.push("/auth/login")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onResponseError() {
|
||||||
|
error.value = "Email not found in our records.";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
if (!error.value) error.value = "Network error: Connection to server failed.";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-gray-100 dark:bg-gray-950 flex flex-col items-center justify-center p-6 transition-colors duration-300">
|
||||||
|
<Transition appear enter-active-class="transition duration-700 ease-out"
|
||||||
|
enter-from-class="translate-y-8 opacity-0" enter-to-class="translate-y-0 opacity-100">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border dark:border-gray-800 transition-colors">
|
||||||
|
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100! mb-8 text-center">Reset Password</h1>
|
||||||
|
|
||||||
|
<form class="space-y-6" @submit.prevent="handleResetRequest">
|
||||||
|
<div>
|
||||||
|
<label for="email"
|
||||||
|
class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input id="email" v-model="email" type="email" :class="[
|
||||||
|
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
||||||
|
error ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||||
|
]" placeholder="[email protected]">
|
||||||
|
<p v-if="error" class="mt-1.5 text-xs text-red-500! ml-1 font-medium">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-4 pt-2">
|
||||||
|
<button type="submit" :disabled="loading"
|
||||||
|
class="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-bold py-3 rounded-lg shadow-md hover:shadow-lg transform active:scale-95 transition-all duration-200">
|
||||||
|
{{ loading ? 'SENDING...' : 'SEND RESET LINK' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<nuxt-link to="/auth/login"
|
||||||
|
class="text-sm text-gray-500 hover:text-blue-600 dark:text-gray-400 dark:hover:text-blue-400 transition-colors">
|
||||||
|
Back to Login
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
+41
-47
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border dark:border-gray-800 transition-colors">
|
class="w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border dark:border-gray-800 transition-colors">
|
||||||
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-8 text-center capitalize">Login</h1>
|
<h1 class="text-3xl font-bold text-gray-800 dark:text-white! mb-8 text-center capitalize">Login</h1>
|
||||||
|
|
||||||
<div v-if="globalError"
|
<div v-if="globalError"
|
||||||
class="mb-6 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm text-center">
|
class="mb-6 p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-sm text-center">
|
||||||
@@ -19,24 +19,29 @@
|
|||||||
<input v-model="loginFormData.email" type="email" :class="[
|
<input v-model="loginFormData.email" type="email" :class="[
|
||||||
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
||||||
errors.email ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
errors.email ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||||
]" placeholder="[email protected]" />
|
]" placeholder="[email protected]">
|
||||||
<p v-if="errors.email" class="mt-1 text-xs text-red-500 ml-1">{{ errors.email }}</p>
|
<p v-if="errors.email" class="mt-1 text-xs text-red-500! ml-1">{{ errors.email }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Password</label>
|
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Password</label>
|
||||||
<input v-model="loginFormData.password" type="password" @keyup.enter="handleLogin" :class="[
|
<input v-model="loginFormData.password" type="password" :class="[
|
||||||
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
'w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white',
|
||||||
errors.password ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
errors.password ? 'border-red-500 ring-1 ring-red-500' : 'border-gray-200 dark:border-gray-700'
|
||||||
]" placeholder="••••••••" />
|
]" placeholder="••••••••" @keyup.enter="handleLogin">
|
||||||
<p v-if="errors.password" class="mt-1 text-xs text-red-500 ml-1">{{ errors.password }}</p>
|
<p v-if="errors.password" class="mt-1 text-xs text-red-500! ml-1">{{ errors.password }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-center pt-4">
|
<div class="flex flex-col items-center gap-4 pt-2">
|
||||||
<button @click="handleLogin" :disabled="loading"
|
<button :disabled="loading"
|
||||||
class="w-1/2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-bold py-3 rounded-lg shadow-md hover:shadow-lg transform active:scale-95 transition-all duration-200">
|
class="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-bold py-3 rounded-lg shadow-md hover:shadow-lg transform active:scale-95 transition-all duration-200"
|
||||||
|
@click="handleLogin">
|
||||||
{{ loading ? 'AUTHENTICATING...' : 'LOGIN' }}
|
{{ loading ? 'AUTHENTICATING...' : 'LOGIN' }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<nuxt-link to="/auth/forgot-password" class="text-xs text-blue-600 hover:underline dark:text-blue-400">
|
||||||
|
Forgot Password?
|
||||||
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,28 +49,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
@keyframes fadeInUp {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-fade-in-up {
|
|
||||||
animation: fadeInUp 0.6s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
.delay-200 {
|
|
||||||
animation-delay: 0.2s;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useAuthStore } from "~/stores/auth-store.js";
|
import { useAuthStore } from "~/stores/auth-store.js";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
@@ -111,8 +94,11 @@ async function handleLogin() {
|
|||||||
console.log(response)
|
console.log(response)
|
||||||
token.value = response._data.token;
|
token.value = response._data.token;
|
||||||
user.value = response._data.user;
|
user.value = response._data.user;
|
||||||
router.push("/")
|
if (response._data.user.mustChangePassword === true) {
|
||||||
//getUserInfo();
|
router.push("/profile/update-password");
|
||||||
|
} else {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onResponseError({ response }) {
|
onResponseError({ response }) {
|
||||||
@@ -130,18 +116,26 @@ async function handleLogin() {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getUserInfo() {
|
|
||||||
try {
|
|
||||||
const data = await $fetch(`${api}/auth/user`, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token.value}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
user.value = data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("User info fetch failed:", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in-up {
|
||||||
|
animation: fadeInUp 0.6s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delay-200 {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
+7
-7
@@ -4,7 +4,7 @@
|
|||||||
<div class="w-full max-w-2xl animate-[fadeInUp_0.6s_ease-out]">
|
<div class="w-full max-w-2xl animate-[fadeInUp_0.6s_ease-out]">
|
||||||
|
|
||||||
<header class="text-center mb-12">
|
<header class="text-center mb-12">
|
||||||
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 dark:text-white mb-4 tracking-tight">
|
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 dark:text-white! mb-4 tracking-tight">
|
||||||
Academic Management System
|
Academic Management System
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
<p class="text-lg text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
||||||
@@ -15,17 +15,17 @@
|
|||||||
<nav class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<nav class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
|
||||||
<nuxt-link to="/auth/login"
|
<nuxt-link to="/auth/login"
|
||||||
class="group p-8 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
class="no-underline text-current block group p-8 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||||
<div class="text-3xl mb-4 group-hover:scale-110 transition-transform">🔐</div>
|
<div class="text-3xl mb-4 group-hover:scale-110 transition-transform">🔐</div>
|
||||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-2">Access Portal</h2>
|
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100! mb-2">Access Portal</h2>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Sign in to manage your personalized dashboard and academic
|
<p class="text-sm text-gray-500 dark:text-gray-400">Sign in to manage your personalized dashboard and academic
|
||||||
data.</p>
|
data.</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/auth-test"
|
<nuxt-link to="/auth-test"
|
||||||
class="group p-8 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
class="no-underline text-current block group p-8 bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl shadow-sm hover:shadow-xl hover:-translate-y-1 transition-all duration-300">
|
||||||
<div class="text-3xl mb-4 group-hover:scale-110 transition-transform">🛠️</div>
|
<div class="text-3xl mb-4 group-hover:scale-110 transition-transform">🛠️</div>
|
||||||
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-2">System Test</h2>
|
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-100! mb-2">System Test</h2>
|
||||||
<p class="text-sm text-gray-500 dark:text-gray-400">Verify API connections and authentication state
|
<p class="text-sm text-gray-500 dark:text-gray-400">Verify API connections and authentication state
|
||||||
persistence.</p>
|
persistence.</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
<div
|
<div
|
||||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-gray-200 dark:bg-gray-800 text-xs font-medium text-gray-600 dark:text-gray-400">
|
class="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-gray-200 dark:bg-gray-800 text-xs font-medium text-gray-600 dark:text-gray-400">
|
||||||
<span class="relative flex h-2 w-2">
|
<span class="relative flex h-2 w-2">
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
|
||||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
|
||||||
</span>
|
</span>
|
||||||
System Operational
|
System Operational
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,11 +13,10 @@
|
|||||||
<div
|
<div
|
||||||
class="h-32 w-32 rounded-full border-4 border-white bg-slate-100 overflow-hidden shadow-md ring-1 ring-slate-200">
|
class="h-32 w-32 rounded-full border-4 border-white bg-slate-100 overflow-hidden shadow-md ring-1 ring-slate-200">
|
||||||
<img :src="user.avatarUrl" :alt="user.name"
|
<img :src="user.avatarUrl" :alt="user.name"
|
||||||
class="h-full w-full object-cover transition-transform hover:scale-105" />
|
class="h-full w-full object-cover transition-transform hover:scale-105">
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute bottom-2 right-2 h-5 w-5 rounded-full border-[3px] border-white shadow-sm"
|
<div class="absolute bottom-2 right-2 h-5 w-5 rounded-full border-[3px] border-white shadow-sm"
|
||||||
:class="user.isActive ? 'bg-emerald-500' : 'bg-gray-400'" title="Online Status">
|
:class="user.isActive ? 'bg-emerald-500' : 'bg-gray-400'" title="Online Status" />
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">{{ user.name }}</h1>
|
<h1 class="text-2xl font-bold text-slate-900 tracking-tight">{{ user.name }}</h1>
|
||||||
@@ -139,7 +138,7 @@
|
|||||||
class="mt-4 pt-4 border-t border-slate-50 flex flex-wrap items-center gap-4 text-xs font-medium z-10 relative">
|
class="mt-4 pt-4 border-t border-slate-50 flex flex-wrap items-center gap-4 text-xs font-medium z-10 relative">
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-blue-50 text-blue-700 border border-blue-100">
|
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-blue-50 text-blue-700 border border-blue-100">
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
|
<span class="w-1.5 h-1.5 rounded-full bg-blue-500" />
|
||||||
{{ pub.field }}
|
{{ pub.field }}
|
||||||
</span>
|
</span>
|
||||||
<span class="inline-flex items-center gap-1.5 text-slate-500">
|
<span class="inline-flex items-center gap-1.5 text-slate-500">
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<script setup>
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
const api = config.public.apiBase;
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const notifications = useNotificationStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const passwordData = reactive({
|
||||||
|
oldPassword: "",
|
||||||
|
newPassword: "",
|
||||||
|
confirmNewPassword: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
const error = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
async function handleUpdatePassword() {
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
if (passwordData.newPassword !== passwordData.confirmNewPassword) {
|
||||||
|
error.value = "Passwords do not match.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch(`${api}/users/me/password`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${authStore.token}`
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
oldPassword: passwordData.oldPassword,
|
||||||
|
newPassword: passwordData.newPassword,
|
||||||
|
confirmNewPassword: passwordData.confirmNewPassword
|
||||||
|
},
|
||||||
|
onResponse({ response }) {
|
||||||
|
if (response.status === 200) {
|
||||||
|
notifications.success("Password updated successfully.");
|
||||||
|
authStore.user.mustChangePassword = false;
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
error.value = "Failed to update password. Please check your current password";
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="min-h-screen bg-gray-100 dark:bg-gray-950 flex flex-col items-center justify-center p-6 transition-colors duration-300">
|
||||||
|
<Transition appear enter-active-class="transition duration-700 ease-out"
|
||||||
|
enter-from-class="translate-y-8 opacity-0" enter-to-class="translate-y-0 opacity-100">
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="w-full max-w-md bg-white dark:bg-gray-900 rounded-xl shadow-lg p-8 border dark:border-gray-800 transition-colors">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800 dark:text-white! mb-2 text-center">Password Update</h1>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400 mb-8 text-center">Please update your password to
|
||||||
|
secure your account.</p>
|
||||||
|
|
||||||
|
<form class="space-y-5" @submit.prevent="handleUpdatePassword">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Current
|
||||||
|
Password</label>
|
||||||
|
<input v-model="passwordData.oldPassword" type="password" required
|
||||||
|
class="w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white"
|
||||||
|
placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="border-gray-100 dark:border-gray-800 my-2">
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">New
|
||||||
|
Password</label>
|
||||||
|
<input v-model="passwordData.newPassword" type="password" required
|
||||||
|
class="w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white"
|
||||||
|
placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-600 dark:text-gray-400 mb-1.5 ml-1">Confirm
|
||||||
|
New Password</label>
|
||||||
|
<input v-model="passwordData.confirmNewPassword" type="password" required
|
||||||
|
class="w-full px-4 py-2.5 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-all text-gray-900 dark:text-white"
|
||||||
|
placeholder="••••••••">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="error"
|
||||||
|
class="p-3 bg-red-100 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg text-red-700 dark:text-red-400 text-xs text-center">
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center gap-4 pt-4">
|
||||||
|
<button type="submit" :disabled="loading"
|
||||||
|
class="w-full bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 text-white font-bold py-3 rounded-lg shadow-md hover:shadow-lg transform active:scale-95 transition-all duration-200 capitalize">
|
||||||
|
{{ loading ? 'Updating...' : 'Update Password' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useNotificationStore = defineStore('notifications', {
|
||||||
|
state: () => ({
|
||||||
|
notifications: []
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
add(message, type = 'success', duration = 3000) {
|
||||||
|
const id = Date.now()
|
||||||
|
this.notifications.push({ id, message, type })
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.remove(id)
|
||||||
|
}, duration)
|
||||||
|
},
|
||||||
|
remove(id) {
|
||||||
|
this.notifications = this.notifications.filter(n => n.id !== id)
|
||||||
|
},
|
||||||
|
error(msg) { this.add(msg, 'error') },
|
||||||
|
success(msg) { this.add(msg, 'success') }
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user