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.
569 lines
15 KiB
HTML
569 lines
15 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<meta name="description" content="Third-party Account Binding">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ $t('auth.bind_account') || 'Bind Account' }}</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);
|
|
margin: 0;
|
|
}
|
|
|
|
.background {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 100%;
|
|
top: 0;
|
|
left: 0;
|
|
overflow: hidden;
|
|
opacity: 0.3;
|
|
z-index: 0;
|
|
}
|
|
|
|
.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;
|
|
cursor: pointer;
|
|
}
|
|
|
|
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;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
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 for button */
|
|
.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;
|
|
display: none;
|
|
/* Hidden by default, shown when loaded */
|
|
}
|
|
|
|
.container.visible {
|
|
display: block;
|
|
animation: fadeIn 0.5s ease-out;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.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%);
|
|
}
|
|
|
|
/* Initial Loading Screen */
|
|
.initial-loading {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10;
|
|
color: white;
|
|
}
|
|
|
|
.initial-loading .loading-spinner {
|
|
position: static;
|
|
transform: none;
|
|
width: 40px;
|
|
height: 40px;
|
|
margin-bottom: 20px;
|
|
border-width: 3px;
|
|
}
|
|
|
|
.error-message {
|
|
color: #ef4444;
|
|
background: rgba(255, 255, 255, 0.9);
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
text-align: center;
|
|
max-width: 400px;
|
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="background" id="background">
|
|
<!-- Bubbles will be injected here -->
|
|
</div>
|
|
|
|
<!-- Initial Loading State -->
|
|
<div v-if="loading && !error" class="initial-loading">
|
|
<div class="loading-spinner"></div>
|
|
<p style="color: white; margin: 0;">{{ $t('common.processing') || 'Processing...' }}</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-if="error" class="error-message">
|
|
<h3 style="margin-top: 0;">Error</h3>
|
|
<p style="color: #ef4444;">{{ error }}</p>
|
|
<a href="/login" class="button"
|
|
style="display: inline-block; padding: 10px 20px; background: var(--color-primary, #4f46e5); color: white; border-radius: 6px;">
|
|
{{ $t('auth.back_to_login') || 'Back to Login' }}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Main Container (Hidden until loaded and needs bind) -->
|
|
<div class="container" :class="{ 'right-panel-active': isRegister, 'visible': !loading && !error && needBind }">
|
|
|
|
<!-- Sign Up (Create New) Container -->
|
|
<div class="form-container sign-up-container">
|
|
<form @submit.prevent="handleRegister">
|
|
<h1>{{ $t('auth.create_account') || 'Create Account' }}</h1>
|
|
<span style="margin-bottom: 20px;">{{ $t('auth.bind_new_desc', {provider: provider}) || `Create a new account to
|
|
bind with ${provider}` }}</span>
|
|
|
|
<input type="text" v:value="regForm.username" :placeholder="$t('auth.username') || 'Username'" required />
|
|
<input type="email" v:value="regForm.email" :placeholder="$t('auth.email') || 'Email'" />
|
|
|
|
<button :disabled="submitting" :class="{ 'button-loading': submitting }">
|
|
{{ $t('auth.sign_up_bind') || 'Sign Up & Bind' }}
|
|
<div v-if="submitting" class="loading-spinner"></div>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Sign In (Bind Existing) Container -->
|
|
<div class="form-container sign-in-container">
|
|
<form @submit.prevent="handleBind">
|
|
<h1>{{ $t('auth.bind_existing') || 'Bind Existing' }}</h1>
|
|
<span style="margin-bottom: 20px;">{{ $t('auth.bind_exist_desc', {provider: provider}) || `Bind your ${provider}
|
|
to an existing account` }}</span>
|
|
|
|
<input type="text" v:value="bindForm.username" :placeholder="$t('auth.account') || 'Username/Email'" required />
|
|
<input type="password" v:value="bindForm.password" :placeholder="$t('auth.password') || 'Password'" required />
|
|
|
|
<a href="/forget" style="font-size: 12px;">{{ $t('auth.forgot_password') || 'Forgot your password?' }}</a>
|
|
|
|
<button :disabled="submitting" :class="{ 'button-loading': submitting }">
|
|
{{ $t('auth.login_bind') || 'Login & Bind' }}
|
|
<div v-if="submitting" class="loading-spinner"></div>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Overlay Container -->
|
|
<div class="overlay-container">
|
|
<div class="overlay">
|
|
<div class="overlay-panel overlay-left">
|
|
<h1>{{ $t('auth.welcome_back') || 'Welcome Back!' }}</h1>
|
|
<p>{{ $t('auth.bind_login_tip') || 'To keep connected with us please login with your personal info' }}</p>
|
|
<button class="ghost" @click="isRegister = false">
|
|
{{ $t('auth.bind_existing') || 'Bind Existing' }}
|
|
</button>
|
|
</div>
|
|
<div class="overlay-panel overlay-right">
|
|
<h1>{{ $t('auth.hello_friend') || 'Hello, Friend!' }}</h1>
|
|
<p>{{ $t('auth.bind_register_tip') || 'Enter your personal details and start journey with us' }}</p>
|
|
<button class="ghost" @click="isRegister = true">
|
|
{{ $t('auth.create_new') || 'Create New' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
|
|
<script setup>
|
|
loading = true
|
|
submitting = false
|
|
error = ''
|
|
needBind = false
|
|
isRegister = false // Default to Bind Existing (Sign In) view
|
|
|
|
provider = ''
|
|
providerId = ''
|
|
tempToken = ''
|
|
|
|
bindForm = {
|
|
username: '',
|
|
password: ''
|
|
}
|
|
|
|
regForm = {
|
|
username: '',
|
|
email: ''
|
|
}
|
|
|
|
// Helper to generate bubbles
|
|
createBubbles = () => {
|
|
const bg = document.getElementById('background');
|
|
if (!bg) return;
|
|
|
|
const bubbleCount = 10;
|
|
for (let i = 0; i < bubbleCount; i++) {
|
|
const bubble = document.createElement('div');
|
|
bubble.classList.add('bubble');
|
|
|
|
const size = Math.random() * 60 + 20 + 'px';
|
|
bubble.style.width = size;
|
|
bubble.style.height = size;
|
|
bubble.style.left = Math.random() * 100 + '%';
|
|
bubble.style.animationDuration = Math.random() * 10 + 5 + 's';
|
|
bubble.style.animationDelay = Math.random() * 5 + 's';
|
|
|
|
bg.appendChild(bubble);
|
|
}
|
|
}
|
|
|
|
handleCallback = async () => {
|
|
try {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const code = urlParams.get('code');
|
|
const state = urlParams.get('state');
|
|
|
|
if (!code || !state) {
|
|
error = 'Invalid callback parameters';
|
|
loading = false;
|
|
return;
|
|
}
|
|
|
|
// Get provider from router params
|
|
provider = $router.params.provider;
|
|
if (!provider) {
|
|
// Fallback: try to get from path if router params not ready or using query
|
|
const pathParts = window.location.pathname.split('/');
|
|
if (pathParts.length > 2 && pathParts[pathParts.length - 1] !== 'callback') {
|
|
provider = pathParts[pathParts.length - 1];
|
|
}
|
|
}
|
|
|
|
if (!provider) {
|
|
error = 'Missing provider in URL';
|
|
loading = false;
|
|
return;
|
|
}
|
|
|
|
const data = await $env.$vbase.oauthCallback(provider, code, state);
|
|
|
|
if (data.access_token || data.token) {
|
|
// Already bound, login success (token set by vbase)
|
|
$router.push('/');
|
|
} else if (data.need_bind) {
|
|
// Need binding
|
|
needBind = true;
|
|
provider = data.provider;
|
|
providerId = data.provider_id;
|
|
tempToken = data.temp_token;
|
|
|
|
// Pre-fill username if available from provider (optional, depends on API)
|
|
// if (data.provider_username) regForm.username = data.provider_username;
|
|
|
|
loading = false;
|
|
} else {
|
|
error = 'Unknown response state';
|
|
loading = false;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
error = e.message || 'Network error';
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
handleBind = async () => {
|
|
if (!bindForm.username || !bindForm.password) {
|
|
$message.error($t('auth.fill_required') || 'Please fill in all required fields');
|
|
return;
|
|
}
|
|
|
|
submitting = true;
|
|
try {
|
|
const data = await $env.$vbase.bindAccount(tempToken, bindForm.username, bindForm.password);
|
|
|
|
$message.success($t('auth.bind_success') || 'Binding successful');
|
|
setTimeout(() => $router.push('/'), 1000);
|
|
} catch (e) {
|
|
$message.error(e.message || 'Binding error');
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
}
|
|
|
|
handleRegister = async () => {
|
|
if (!regForm.username) {
|
|
$message.error($t('auth.username_required') || 'Username is required');
|
|
return;
|
|
}
|
|
|
|
submitting = true;
|
|
try {
|
|
const data = await $env.$vbase.bindRegister(tempToken, regForm.username, regForm.email || undefined);
|
|
|
|
$message.success($t('auth.register_success') || 'Registration successful');
|
|
setTimeout(() => $router.push('/'), 1000);
|
|
} catch (e) {
|
|
$message.error(e.message || 'Registration error');
|
|
} finally {
|
|
submitting = false;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
$watch(() => {
|
|
createBubbles();
|
|
handleCallback();
|
|
});
|
|
</script>
|
|
|
|
</html>
|