feat: Implement login, register, and dashboard views
Build complete auth flow with dark-themed CORROSION branding, loading states, error handling, client-side validation, and placeholder dashboard with stat cards and quick actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,72 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// TODO: Implement server overview dashboard with key metrics and status
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
|
||||||
|
const auth = useAuthStore()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-6 space-y-8">
|
||||||
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Dashboard</h1>
|
<!-- Welcome header -->
|
||||||
<p class="text-neutral-400">Server overview — players online, performance metrics, and quick actions.</p>
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-neutral-100">
|
||||||
|
Welcome back, {{ auth.user?.username }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-neutral-500 mt-1">Here's what's happening with your server.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stat cards grid -->
|
||||||
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<!-- Server Status -->
|
||||||
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
|
<p class="text-sm text-neutral-400 mb-2">Server Status</p>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="h-2.5 w-2.5 rounded-full bg-red-500"></span>
|
||||||
|
<span class="text-2xl font-bold text-neutral-100">Offline</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Players Online -->
|
||||||
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
|
<p class="text-sm text-neutral-400 mb-2">Players Online</p>
|
||||||
|
<p class="text-2xl font-bold text-neutral-100">0/0</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Next Wipe -->
|
||||||
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
|
<p class="text-sm text-neutral-400 mb-2">Next Wipe</p>
|
||||||
|
<p class="text-2xl font-bold text-neutral-100">Not Scheduled</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Uptime -->
|
||||||
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-5">
|
||||||
|
<p class="text-sm text-neutral-400 mb-2">Uptime</p>
|
||||||
|
<p class="text-2xl font-bold text-neutral-100">—</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-neutral-200 mb-4">Quick Actions</h2>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Start Server
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Stop Server
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
disabled
|
||||||
|
class="px-4 py-2.5 bg-neutral-800 hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed text-neutral-300 rounded-lg text-sm font-medium transition-colors"
|
||||||
|
>
|
||||||
|
Trigger Wipe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,131 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// TODO: Implement sign-in form with email/password and license validation
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useApi } from '@/composables/useApi'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import type { AuthResponse } from '@/types'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const api = useApi()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
error.value = ''
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post<AuthResponse>('/auth/login', {
|
||||||
|
email: email.value,
|
||||||
|
password: password.value,
|
||||||
|
})
|
||||||
|
|
||||||
|
authStore.setAuth(response)
|
||||||
|
router.push('/')
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
error.value = err.message
|
||||||
|
} else {
|
||||||
|
error.value = 'An unexpected error occurred'
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="min-h-screen bg-neutral-950 flex items-center justify-center px-4">
|
||||||
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Sign In</h1>
|
<div class="w-full max-w-md">
|
||||||
<p class="text-neutral-400">Sign in to your Corrosion server panel.</p>
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-8">
|
||||||
|
<!-- Branding -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold tracking-widest text-red-500">CORROSION</h1>
|
||||||
|
<p class="text-sm text-neutral-500 mt-2">Server Management Panel</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Login form -->
|
||||||
|
<form @submit.prevent="handleLogin" class="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-neutral-400 mb-1.5">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
placeholder="admin@example.com"
|
||||||
|
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-neutral-400 mb-1.5">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autocomplete="current-password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading"
|
||||||
|
class="w-full py-2.5 bg-red-600 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="loading"
|
||||||
|
class="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ loading ? 'Signing in...' : 'Sign In' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Register link -->
|
||||||
|
<p class="mt-6 text-center text-sm text-neutral-500">
|
||||||
|
Don't have an account?
|
||||||
|
<router-link to="/register" class="text-red-400 hover:text-red-300 transition-colors">
|
||||||
|
Create one
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,10 +1,208 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// TODO: Implement registration form with license key field
|
import { ref, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useApi } from '@/composables/useApi'
|
||||||
|
import { useAuthStore } from '@/stores/auth'
|
||||||
|
import type { AuthResponse } from '@/types'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const api = useApi()
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
const email = ref('')
|
||||||
|
const username = ref('')
|
||||||
|
const password = ref('')
|
||||||
|
const confirmPassword = ref('')
|
||||||
|
const licenseKey = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
|
||||||
|
const emailValid = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value))
|
||||||
|
const usernameValid = computed(() => username.value.length >= 3)
|
||||||
|
const passwordsMatch = computed(() => password.value === confirmPassword.value)
|
||||||
|
|
||||||
|
const formValid = computed(() =>
|
||||||
|
emailValid.value &&
|
||||||
|
usernameValid.value &&
|
||||||
|
password.value.length >= 8 &&
|
||||||
|
passwordsMatch.value &&
|
||||||
|
licenseKey.value.trim().length > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
function validate(): string | null {
|
||||||
|
if (!emailValid.value) return 'Please enter a valid email address.'
|
||||||
|
if (!usernameValid.value) return 'Username must be at least 3 characters.'
|
||||||
|
if (password.value.length < 8) return 'Password must be at least 8 characters.'
|
||||||
|
if (!passwordsMatch.value) return 'Passwords do not match.'
|
||||||
|
if (!licenseKey.value.trim()) return 'License key is required.'
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRegister() {
|
||||||
|
const validationError = validate()
|
||||||
|
if (validationError) {
|
||||||
|
error.value = validationError
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
error.value = ''
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.post<AuthResponse>('/auth/register', {
|
||||||
|
email: email.value,
|
||||||
|
username: username.value,
|
||||||
|
password: password.value,
|
||||||
|
license_key: licenseKey.value.trim(),
|
||||||
|
})
|
||||||
|
|
||||||
|
authStore.setAuth(response)
|
||||||
|
router.push('/setup')
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
error.value = err.message
|
||||||
|
} else {
|
||||||
|
error.value = 'An unexpected error occurred'
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="min-h-screen bg-neutral-950 flex items-center justify-center px-4">
|
||||||
<h1 class="text-2xl font-bold text-neutral-100 mb-4">Create Account</h1>
|
<div class="w-full max-w-md">
|
||||||
<p class="text-neutral-400">Register a new account with your license key.</p>
|
<div class="bg-neutral-900 border border-neutral-800 rounded-lg p-8">
|
||||||
|
<!-- Branding -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<h1 class="text-3xl font-bold tracking-widest text-red-500">CORROSION</h1>
|
||||||
|
<p class="text-sm text-neutral-500 mt-2">Create your account</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error message -->
|
||||||
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="mb-6 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Register form -->
|
||||||
|
<form @submit.prevent="handleRegister" class="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label for="email" class="block text-sm font-medium text-neutral-400 mb-1.5">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
v-model="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
autocomplete="email"
|
||||||
|
placeholder="you@example.com"
|
||||||
|
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="username" class="block text-sm font-medium text-neutral-400 mb-1.5">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
v-model="username"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
autocomplete="username"
|
||||||
|
placeholder="At least 3 characters"
|
||||||
|
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="password" class="block text-sm font-medium text-neutral-400 mb-1.5">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
v-model="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
placeholder="At least 8 characters"
|
||||||
|
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="confirm-password" class="block text-sm font-medium text-neutral-400 mb-1.5">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirm-password"
|
||||||
|
v-model="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
autocomplete="new-password"
|
||||||
|
placeholder="Re-enter your password"
|
||||||
|
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500 transition-colors"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="license-key" class="block text-sm font-medium text-neutral-400 mb-1.5">
|
||||||
|
License Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="license-key"
|
||||||
|
v-model="licenseKey"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="XXXX-XXXX-XXXX-XXXX"
|
||||||
|
class="w-full px-3 py-2.5 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-red-500/50 focus:border-red-500 transition-colors font-mono tracking-wider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="loading || !formValid"
|
||||||
|
class="w-full py-2.5 bg-red-600 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="loading"
|
||||||
|
class="animate-spin h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{{ loading ? 'Creating account...' : 'Create Account' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Login link -->
|
||||||
|
<p class="mt-6 text-center text-sm text-neutral-500">
|
||||||
|
Already have an account?
|
||||||
|
<router-link to="/login" class="text-red-400 hover:text-red-300 transition-colors">
|
||||||
|
Sign in
|
||||||
|
</router-link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user