mirror of https://github.com/veypi/OneAuth.git
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
842 lines
21 KiB
HTML
842 lines
21 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<meta name="description" content="Login and Register Page">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ isSignUp ? $t('auth.register') : $t('auth.login') }}</title>
|
|
<style>
|
|
body {
|
|
height: 100%;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
position: relative;
|
|
background: linear-gradient(135deg, var(--color-primary-light, #e0e7ff) 0%, var(--color-primary-dark, #4338ca) 100%);
|
|
font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
}
|
|
|
|
.background {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 0;
|
|
overflow: hidden;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.bubble {
|
|
position: absolute;
|
|
bottom: -150px;
|
|
background: rgba(255, 255, 255, 0.2);
|
|
border-radius: 50%;
|
|
animation: rise linear infinite;
|
|
}
|
|
|
|
@keyframes rise {
|
|
0% {
|
|
transform: translateY(0) rotate(0);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
100% {
|
|
transform: translateY(-120vh) rotate(360deg);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
h1 {
|
|
font-weight: bold;
|
|
margin: 0;
|
|
margin-bottom: 15px;
|
|
color: var(--color-text, #1f2937);
|
|
}
|
|
|
|
p {
|
|
font-size: 14px;
|
|
font-weight: 100;
|
|
line-height: 20px;
|
|
letter-spacing: 0.5px;
|
|
margin: 20px 0 30px;
|
|
color: var(--color-text, #1f2937);
|
|
}
|
|
|
|
span {
|
|
font-size: 12px;
|
|
margin: 15px 0;
|
|
color: var(--color-text-light, #6b7280);
|
|
}
|
|
|
|
a {
|
|
color: var(--color-primary, #4f46e5);
|
|
font-size: 14px;
|
|
text-decoration: none;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
button {
|
|
border-radius: var(--border-radius, 8px);
|
|
border: 1px solid var(--color-primary, #4f46e5);
|
|
background-color: var(--color-primary, #4f46e5);
|
|
color: #ffffff;
|
|
font-size: 12px;
|
|
font-weight: bold;
|
|
padding: 12px 45px;
|
|
letter-spacing: 1px;
|
|
text-transform: uppercase;
|
|
transition: transform 80ms ease-in, background-color 0.2s;
|
|
cursor: pointer;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
button:active {
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
button:focus {
|
|
outline: none;
|
|
}
|
|
|
|
button.ghost {
|
|
background-color: transparent;
|
|
border-color: #ffffff;
|
|
color: #ffffff;
|
|
}
|
|
|
|
button.ghost:hover {
|
|
background-color: rgba(255, 255, 255, 0.1);
|
|
}
|
|
|
|
button:disabled {
|
|
background-color: var(--color-border, #d1d5db);
|
|
border-color: var(--color-border, #d1d5db);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
/* Loading spinner */
|
|
.loading-spinner {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
border-top: 2px solid #ffffff;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% {
|
|
transform: translate(-50%, -50%) rotate(0deg);
|
|
}
|
|
|
|
100% {
|
|
transform: translate(-50%, -50%) rotate(360deg);
|
|
}
|
|
}
|
|
|
|
.button-loading {
|
|
color: transparent !important;
|
|
}
|
|
|
|
form {
|
|
background-color: var(--bg-color-secondary, #ffffff);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
padding: 0 50px;
|
|
height: 100%;
|
|
text-align: center;
|
|
}
|
|
|
|
input {
|
|
background-color: var(--bg-color-secondary, #ffffff);
|
|
border: 1px solid var(--color-border, #d1d5db);
|
|
width: 100%;
|
|
padding: 10px 12px;
|
|
margin: 8px 0;
|
|
border-radius: var(--border-radius, 8px);
|
|
color: var(--color-text, #1f2937);
|
|
transition: border-color 0.2s, box-shadow 0.2s;
|
|
font-size: 14px;
|
|
}
|
|
|
|
input:focus {
|
|
outline: none;
|
|
border-color: var(--color-primary, #4f46e5);
|
|
box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
|
|
}
|
|
|
|
input::placeholder {
|
|
color: var(--color-text-light, #9ca3af);
|
|
}
|
|
|
|
.container {
|
|
background-color: var(--bg-color-secondary, #ffffff);
|
|
border-radius: 10px;
|
|
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.15), 0 10px 10px rgba(0, 0, 0, 0.1);
|
|
position: relative;
|
|
overflow: hidden;
|
|
width: 768px;
|
|
max-width: 100%;
|
|
min-height: 550px;
|
|
z-index: 1;
|
|
}
|
|
|
|
.form-container {
|
|
position: absolute;
|
|
top: 0;
|
|
height: 100%;
|
|
transition: all 0.6s ease-in-out;
|
|
}
|
|
|
|
.sign-in-container {
|
|
left: 0;
|
|
width: 50%;
|
|
z-index: 2;
|
|
}
|
|
|
|
.sign-up-container {
|
|
left: 0;
|
|
width: 50%;
|
|
opacity: 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
.container.right-panel-active .sign-in-container {
|
|
transform: translateX(100%);
|
|
}
|
|
|
|
.container.right-panel-active .sign-up-container {
|
|
transform: translateX(100%);
|
|
opacity: 1;
|
|
z-index: 5;
|
|
}
|
|
|
|
.overlay-container {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 50%;
|
|
width: 50%;
|
|
height: 100%;
|
|
overflow: hidden;
|
|
transition: transform 0.6s ease-in-out;
|
|
z-index: 100;
|
|
}
|
|
|
|
.container.right-panel-active .overlay-container {
|
|
transform: translateX(-100%);
|
|
}
|
|
|
|
.overlay {
|
|
background: var(--color-primary, #4f46e5);
|
|
background: linear-gradient(to right, var(--color-secondary, #7c3aed), var(--color-primary, #4f46e5));
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
background-position: 0 0;
|
|
color: #ffffff;
|
|
position: relative;
|
|
left: -100%;
|
|
height: 100%;
|
|
width: 200%;
|
|
transform: translateX(0);
|
|
transition: transform 0.6s ease-in-out;
|
|
}
|
|
|
|
.container.right-panel-active .overlay {
|
|
transform: translateX(50%);
|
|
}
|
|
|
|
.overlay-panel {
|
|
position: absolute;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-direction: column;
|
|
padding: 0 40px;
|
|
text-align: center;
|
|
top: 0;
|
|
height: 100%;
|
|
width: 50%;
|
|
transform: translateX(0);
|
|
transition: transform 0.6s ease-in-out;
|
|
}
|
|
|
|
.overlay-left {
|
|
transform: translateX(-20%);
|
|
}
|
|
|
|
.container.right-panel-active .overlay-left {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.overlay-right {
|
|
right: 0;
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.container.right-panel-active .overlay-right {
|
|
transform: translateX(20%);
|
|
}
|
|
|
|
.social-container {
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.social-container a {
|
|
border: 1px solid #dddddd;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
margin: 0 5px;
|
|
height: 40px;
|
|
width: 40px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
overflow: hidden;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.social-container a:hover {
|
|
background-color: #f3f4f6;
|
|
border-color: var(--color-primary);
|
|
color: var(--color-primary);
|
|
}
|
|
|
|
.social-btn span {
|
|
font-size: 10px;
|
|
margin: 0;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
max-width: 36px;
|
|
}
|
|
|
|
.error-message {
|
|
color: var(--color-danger, #ef4444);
|
|
font-size: 12px;
|
|
margin-top: 8px;
|
|
min-height: 18px;
|
|
}
|
|
|
|
.login-tab {
|
|
display: flex;
|
|
margin-bottom: 15px;
|
|
border-bottom: 1px solid #eee;
|
|
width: 100%;
|
|
}
|
|
|
|
.tab-item {
|
|
padding: 10px 15px;
|
|
cursor: pointer;
|
|
flex: 1;
|
|
text-align: center;
|
|
color: #666;
|
|
font-size: 13px;
|
|
border-bottom: 2px solid transparent;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.tab-item.active {
|
|
color: var(--color-primary, #4f46e5);
|
|
border-bottom-color: var(--color-primary, #4f46e5);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.input-group {
|
|
width: 100%;
|
|
margin: 6px 0;
|
|
}
|
|
|
|
.forgot-link {
|
|
font-size: 12px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.container {
|
|
width: 100%;
|
|
min-height: 100vh;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.form-container {
|
|
width: 100%;
|
|
}
|
|
|
|
.sign-in-container,
|
|
.sign-up-container {
|
|
width: 100%;
|
|
}
|
|
|
|
.container.right-panel-active .sign-in-container {
|
|
transform: translateX(100%);
|
|
}
|
|
|
|
.container.right-panel-active .sign-up-container {
|
|
transform: translateX(100%);
|
|
}
|
|
|
|
.overlay-container {
|
|
display: none;
|
|
}
|
|
|
|
form {
|
|
padding: 0 30px;
|
|
}
|
|
|
|
.mobile-toggle {
|
|
display: block !important;
|
|
margin-top: 20px;
|
|
font-size: 14px;
|
|
color: var(--color-primary, #4f46e5);
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
@media (min-width: 769px) {
|
|
.mobile-toggle {
|
|
display: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="background">
|
|
<div v-for="bubble in bubbles" class="bubble" :style="bubble.style"></div>
|
|
</div>
|
|
|
|
<div class="container" :class="{ 'right-panel-active': isSignUp }">
|
|
<!-- Register Form -->
|
|
<div class="form-container sign-up-container">
|
|
<form @submit="handleSignUp">
|
|
<h1>{{ $t('auth.create_account') }}</h1>
|
|
<div class="social-container">
|
|
<a v-for="p in providers" @click="handleOAuth(p.code)" :title="p.name" class="social-btn">
|
|
<i v-if="p.icon" :class="'fa-brands fa-' + p.icon"></i>
|
|
<span v-else>{{ p.name }}</span>
|
|
</a>
|
|
</div>
|
|
<span>{{ $t('auth.use_info_register') }}</span>
|
|
|
|
<div class="input-group">
|
|
<input type="text" :placeholder="$t('auth.username')" v:value="signUpForm.username" required />
|
|
</div>
|
|
|
|
<div class="input-group" v-if="emailEnabled">
|
|
<input type="email" :placeholder="$t('auth.email')" v:value="signUpForm.email" />
|
|
</div>
|
|
|
|
<div class="input-group" v-if="smsEnabled">
|
|
<input type="text" :placeholder="$t('auth.phone_placeholder')" v:value="signUpForm.phone" />
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<input type="password" :placeholder="$t('auth.password')" v:value="signUpForm.password" required />
|
|
</div>
|
|
|
|
<div class="input-group">
|
|
<input type="password" :placeholder="$t('auth.confirm_password')" v:value="signUpForm.confirmPassword"
|
|
required />
|
|
</div>
|
|
|
|
<div class="error-message">{{ signUpError }}</div>
|
|
|
|
<button type="submit" :class="{ 'button-loading': signUpLoading }" :disabled="signUpLoading">
|
|
<span v-if="!signUpLoading">{{ $t('auth.register') }}</span>
|
|
<div v-if="signUpLoading" class="loading-spinner"></div>
|
|
</button>
|
|
|
|
<div class="mobile-toggle" @click="switchToSignIn">
|
|
{{ $t('auth.have_account') }} <a>{{ $t('auth.login') }}</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Login Form -->
|
|
<div class="form-container sign-in-container">
|
|
<form @submit="handleSignIn">
|
|
<h1>{{ $t('auth.login') }}</h1>
|
|
<div class="social-container">
|
|
<a v-for="p in providers" @click="handleOAuth(p.code)" :title="p.name" class="social-btn">
|
|
<i v-if="p.icon" :class="'fa-brands fa-' + p.icon"></i>
|
|
<span v-else>{{ p.name }}</span>
|
|
</a>
|
|
</div>
|
|
<span>{{ $t('auth.use_account') }}</span>
|
|
|
|
<!-- Login Type Tabs -->
|
|
<div class="login-tab" v-if="smsEnabled || emailEnabled">
|
|
<div class="tab-item" :class="{ active: loginType === 'username' }" @click="switchLoginType('username')">
|
|
{{ $t('auth.username_login') }}
|
|
</div>
|
|
<div class="tab-item" :class="{ active: loginType === 'code' }" @click="switchLoginType('code')">
|
|
{{ $t('auth.code_login') }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Username/Password Login -->
|
|
<div v-if="loginType === 'username'" style="width: 100%;">
|
|
<div class="input-group">
|
|
<input type="text" :placeholder="$t('auth.username_or_email')" v:value="signInForm.username" required />
|
|
</div>
|
|
<div class="input-group">
|
|
<input type="password" :placeholder="$t('auth.password')" v:value="signInForm.password" required />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Code Login -->
|
|
<div v-if="loginType === 'code'" style="width: 100%;">
|
|
<div class="input-group">
|
|
<input type="text" :placeholder="codePlaceholder" v:value="signInForm.target" required />
|
|
</div>
|
|
<div class="input-group">
|
|
<input type="text" :placeholder="$t('auth.verification_code')" v:value="signInForm.code" required />
|
|
</div>
|
|
</div>
|
|
|
|
<a href="#" class="forgot-link">{{ $t('auth.forgot_password') }}</a>
|
|
|
|
<div class="error-message">{{ signInError }}</div>
|
|
|
|
<button type="submit" :class="{ 'button-loading': signInLoading }" :disabled="signInLoading">
|
|
<span v-if="!signInLoading">{{ $t('auth.login') }}</span>
|
|
<div v-if="signInLoading" class="loading-spinner"></div>
|
|
</button>
|
|
|
|
<div class="mobile-toggle" @click="switchToSignUp">
|
|
{{ $t('auth.no_account') }} <a>{{ $t('auth.register') }}</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Overlay -->
|
|
<div class="overlay-container">
|
|
<div class="overlay">
|
|
<div class="overlay-panel overlay-left">
|
|
<h1>{{ $t('auth.welcome_back') }}</h1>
|
|
<p>{{ $t('auth.keep_connected') }}</p>
|
|
<button class="ghost" @click="switchToSignIn">{{ $t('auth.login') }}</button>
|
|
</div>
|
|
<div class="overlay-panel overlay-right">
|
|
<h1>{{ $t('auth.hello_friend') }}</h1>
|
|
<p>{{ $t('auth.start_journey') }}</p>
|
|
<button class="ghost" @click="switchToSignUp">{{ $t('auth.register') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
|
|
<script setup>
|
|
// Reactive data - use = for reactive state (NO let/const/var)
|
|
isSignUp = false;
|
|
loginType = 'username';
|
|
redirect = $router.query.redirect || '/';
|
|
|
|
smsEnabled = false;
|
|
emailEnabled = false;
|
|
regRequireEmail = false;
|
|
regRequirePhone = false;
|
|
codePlaceholder = $t('auth.email_or_phone');
|
|
countDown = 0;
|
|
timer = null;
|
|
|
|
signInForm = {
|
|
username: '',
|
|
password: '',
|
|
target: '',
|
|
code: ''
|
|
};
|
|
|
|
signUpForm = {
|
|
username: '',
|
|
email: '',
|
|
phone: '',
|
|
password: '',
|
|
confirmPassword: ''
|
|
};
|
|
|
|
signInError = '';
|
|
signUpError = '';
|
|
signInLoading = false;
|
|
signUpLoading = false;
|
|
bubbles = [];
|
|
providers = [];
|
|
|
|
// Create background bubbles
|
|
createBubbles = () => {
|
|
const numberOfBubbles = 15;
|
|
for (let i = 0; i < numberOfBubbles; i++) {
|
|
const size = Math.random() * 80 + 40;
|
|
const left = Math.random() * 100;
|
|
const delay = Math.random() * 15;
|
|
const duration = Math.random() * 15 + 10;
|
|
|
|
bubbles.push({
|
|
style: {
|
|
width: size + 'px',
|
|
height: size + 'px',
|
|
left: left + '%',
|
|
animationDelay: delay + 's',
|
|
animationDuration: duration + 's',
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
// Switch to sign up panel
|
|
switchToSignUp = () => {
|
|
if (signInLoading || signUpLoading) return;
|
|
isSignUp = true;
|
|
signUpError = '';
|
|
signInError = '';
|
|
};
|
|
|
|
// Switch to sign in panel
|
|
switchToSignIn = () => {
|
|
if (signInLoading || signUpLoading) return;
|
|
isSignUp = false;
|
|
signUpError = '';
|
|
signInError = '';
|
|
};
|
|
|
|
// Switch login type
|
|
switchLoginType = (type) => {
|
|
if (signInLoading) return;
|
|
loginType = type;
|
|
signInError = '';
|
|
signInForm.username = '';
|
|
signInForm.password = '';
|
|
signInForm.target = '';
|
|
signInForm.code = '';
|
|
};
|
|
|
|
// Handle OAuth login
|
|
handleOAuth = async (provider) => {
|
|
try {
|
|
const res = await $axios.get('/api/auth/authorize/thirdparty', {
|
|
params: {
|
|
provider: provider,
|
|
redirect: redirect
|
|
}
|
|
});
|
|
if (res && res.auth_url) {
|
|
window.location.href = res.auth_url;
|
|
}
|
|
} catch (e) {
|
|
$message.error(e.message || $t('auth.oauth_failed'));
|
|
}
|
|
};
|
|
|
|
// Load configuration
|
|
loadConfig = async () => {
|
|
try {
|
|
const res = await $axios.get('/api/info');
|
|
if (res) {
|
|
if (res.oauth_providers) {
|
|
providers = res.oauth_providers;
|
|
}
|
|
|
|
// Update feature flags
|
|
smsEnabled = res.sms_enabled;
|
|
emailEnabled = res.email_enabled;
|
|
regRequireEmail = res.reg_require_email;
|
|
regRequirePhone = res.reg_require_phone;
|
|
|
|
// Update placeholder
|
|
if (smsEnabled && emailEnabled) {
|
|
codePlaceholder = $t('auth.email_or_phone');
|
|
} else if (smsEnabled) {
|
|
codePlaceholder = $t('auth.phone_placeholder');
|
|
} else if (emailEnabled) {
|
|
codePlaceholder = $t('auth.email');
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to load config:", e);
|
|
}
|
|
};
|
|
// Send verification code
|
|
sendCode = async () => {
|
|
if (countDown > 0) return;
|
|
|
|
if (!signInForm.target) {
|
|
signInError = $t('auth.target_required');
|
|
return;
|
|
}
|
|
|
|
// Determine type
|
|
const type = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(signInForm.target) ? 'email' : 'sms';
|
|
|
|
// Check if enabled
|
|
if (type === 'email' && !emailEnabled) {
|
|
signInError = $t('auth.email_disabled');
|
|
return;
|
|
}
|
|
if (type === 'sms' && !smsEnabled) {
|
|
signInError = $t('auth.sms_disabled');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await $env.$vbase.request('POST', '/api/verification/send', {
|
|
type: type,
|
|
target: signInForm.target,
|
|
purpose: 'login'
|
|
});
|
|
|
|
signInError = '';
|
|
$message.success($t('auth.code_sent'));
|
|
countDown = 60;
|
|
if (timer) clearInterval(timer);
|
|
timer = setInterval(() => {
|
|
countDown--;
|
|
if (countDown <= 0) {
|
|
clearInterval(timer);
|
|
timer = null;
|
|
}
|
|
}, 1000);
|
|
|
|
} catch (e) {
|
|
signInError = e.message || 'Failed to send code';
|
|
}
|
|
};
|
|
|
|
// Handle Sign In
|
|
handleSignIn = async (e) => {
|
|
e.preventDefault();
|
|
if (signInLoading) return;
|
|
|
|
signInError = '';
|
|
signInLoading = true;
|
|
|
|
try {
|
|
// Code login
|
|
if (loginType === 'code') {
|
|
if (!signInForm.target || !signInForm.code) {
|
|
throw new Error($t('auth.target_and_code_required'));
|
|
}
|
|
|
|
const type = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(signInForm.target) ? 'email' : 'phone';
|
|
const data = await $env.$vbase.request('POST', '/api/auth/login/code', {
|
|
type: type,
|
|
target: signInForm.target,
|
|
code: signInForm.code
|
|
});
|
|
|
|
if (data && data.access_token) {
|
|
$env.$vbase.token = data.access_token;
|
|
if (data.refresh_token) $env.$vbase.refreshToken = data.refresh_token;
|
|
if (data.user) $env.$vbase.user = data.user;
|
|
|
|
$message.success($t('auth.login_success'));
|
|
if (redirect === '/' || redirect.startsWith('http')) {
|
|
window.location.href = redirect;
|
|
} else {
|
|
window.location.href = redirect;
|
|
}
|
|
} else {
|
|
throw new Error('Login failed: no token received');
|
|
}
|
|
} else {
|
|
// Username/Password login
|
|
if (!signInForm.username || !signInForm.password) {
|
|
throw new Error($t('auth.fill_all_fields'));
|
|
}
|
|
|
|
const success = await $env.$vbase.login(signInForm.username, signInForm.password);
|
|
|
|
if (success) {
|
|
$message.success($t('auth.login_success'));
|
|
window.location.href = redirect;
|
|
} else {
|
|
throw new Error($t('auth.login_failed'));
|
|
}
|
|
}
|
|
} catch (error) {
|
|
signInError = error.message || $t('auth.login_failed');
|
|
} finally {
|
|
signInLoading = false;
|
|
}
|
|
};
|
|
|
|
// Handle Sign Up
|
|
handleSignUp = async (e) => {
|
|
e.preventDefault();
|
|
if (signUpLoading) return;
|
|
|
|
signUpError = '';
|
|
|
|
// Validation
|
|
if (!signUpForm.username || !signUpForm.password || !signUpForm.confirmPassword) {
|
|
signUpError = $t('auth.fill_all_fields');
|
|
return;
|
|
}
|
|
|
|
if (signUpForm.username.length < 3) {
|
|
signUpError = $t('auth.username_too_short');
|
|
return;
|
|
}
|
|
|
|
if (signUpForm.password.length < 8) {
|
|
signUpError = $t('auth.password_too_short');
|
|
return;
|
|
}
|
|
|
|
if (signUpForm.password !== signUpForm.confirmPassword) {
|
|
signUpError = $t('auth.passwords_not_match');
|
|
return;
|
|
}
|
|
|
|
signUpLoading = true;
|
|
|
|
try {
|
|
// Use vbase.request for consistent API handling
|
|
const data = await $env.$vbase.request('POST', '/api/auth/register', {
|
|
username: signUpForm.username,
|
|
email: signUpForm.email || undefined,
|
|
phone: signUpForm.phone || undefined,
|
|
password: signUpForm.password
|
|
});
|
|
|
|
if (data && data.access_token) {
|
|
// Auto login after register using vbase setters
|
|
$env.$vbase.token = data.access_token;
|
|
if (data.refresh_token) {
|
|
$env.$vbase.refreshToken = data.refresh_token;
|
|
}
|
|
if (data.user) {
|
|
$env.$vbase.user = data.user;
|
|
}
|
|
|
|
$message.success($t('auth.register_success'));
|
|
window.location.href = redirect;
|
|
} else {
|
|
$message.success($t('auth.register_success'));
|
|
switchToSignIn();
|
|
}
|
|
} catch (error) {
|
|
signUpError = error.message || $t('auth.register_failed');
|
|
} finally {
|
|
signUpLoading = false;
|
|
}
|
|
};
|
|
|
|
// Initialize
|
|
createBubbles();
|
|
loadConfig();
|
|
</script>
|
|
|
|
</html>
|