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.
368 lines
10 KiB
HTML
368 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
|
|
<head>
|
|
<meta name="description" content="User Profile">
|
|
<title>{{ $t('user.profile') }}</title>
|
|
<style>
|
|
body {
|
|
background-color: var(--bg-color);
|
|
display: flex;
|
|
justify-content: center;
|
|
padding-top: var(--spacing-xl);
|
|
padding-bottom: var(--spacing-xl);
|
|
box-sizing: border-box;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.profile-container {
|
|
width: 100%;
|
|
max-width: 800px;
|
|
padding: var(--spacing-xl);
|
|
background: var(--bg-color-secondary);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
border: 1px solid var(--border-color);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-xl);
|
|
height: fit-content;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: var(--color-text);
|
|
margin-bottom: var(--spacing-md);
|
|
padding-bottom: var(--spacing-xs);
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.avatar-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-lg);
|
|
}
|
|
|
|
.avatar-wrapper {
|
|
position: relative;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.avatar {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 32px;
|
|
color: var(--color-primary-text);
|
|
font-weight: bold;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.avatar-overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.avatar-wrapper:hover .avatar-overlay {
|
|
opacity: 1;
|
|
}
|
|
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.form-full {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.oauth-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.oauth-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--spacing-sm);
|
|
background: var(--bg-color);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: var(--radius-md);
|
|
}
|
|
|
|
.oauth-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.oauth-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 18px;
|
|
background: var(--bg-color-secondary);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.danger-zone {
|
|
margin-top: var(--spacing-lg);
|
|
padding-top: var(--spacing-lg);
|
|
border-top: 1px solid var(--border-color);
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<div class="profile-container">
|
|
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
<h2 style="margin: 0;">{{ $t('user.profile') }}</h2>
|
|
<v-btn color="danger" variant="outline" size="sm" :click="handleLogout">
|
|
{{ $t('auth.logout') }}
|
|
</v-btn>
|
|
</div>
|
|
|
|
<!-- Basic Info -->
|
|
<div>
|
|
<div class="section-title">{{ $t('user.info') || 'Basic Information' }}</div>
|
|
<div class="avatar-section">
|
|
<div class="avatar-wrapper" @click="triggerAvatarUpload">
|
|
<div class="avatar">
|
|
<img v-if="user.avatar" :src="user.avatar" alt="Avatar">
|
|
<span v-else>{{ user.nickname ? user.nickname.charAt(0).toUpperCase() : (user.username ? user.username.charAt(0).toUpperCase() : 'U') }}</span>
|
|
</div>
|
|
<div class="avatar-overlay">
|
|
<i class="fas fa-edit"></i>
|
|
</div>
|
|
</div>
|
|
<div style="flex: 1;">
|
|
<div style="font-weight: bold; font-size: 1.2rem;">{{ user.nickname || user.username }}</div>
|
|
<div style="color: var(--color-text-light); font-size: 0.9rem;">{{ $t('common.id') }}: {{ user.id }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<form style="margin-top: var(--spacing-lg);" @submit.prevent="updateProfile">
|
|
<div class="form-grid">
|
|
<v-input label="Username" :label="$t('user.username')" v:value="user.username" disabled></v-input>
|
|
<v-input label="Nickname" :label="$t('user.nickname')" v:value="user.nickname"></v-input>
|
|
<v-input label="Email" :label="$t('user.email')" type="email" v:value="user.email"></v-input>
|
|
<v-input label="Phone" :label="$t('user.phone')" type="tel" v:value="user.phone"></v-input>
|
|
<v-input class="form-full" label="Avatar URL" :label="$t('user.avatar_url')" v:value="user.avatar"></v-input>
|
|
</div>
|
|
<div style="margin-top: var(--spacing-md); display: flex; justify-content: flex-end;">
|
|
<v-btn type="submit" color="primary" :loading="loading">
|
|
{{ $t('common.save') }}
|
|
</v-btn>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Security -->
|
|
<div>
|
|
<div class="section-title">{{ $t('auth.security') || 'Security' }}</div>
|
|
<form @submit.prevent="changePassword">
|
|
<div class="form-grid">
|
|
<v-input class="form-full" :label="$t('auth.old_password')" type="password" v:value="passwordForm.old_password"></v-input>
|
|
<v-input class="form-full" :label="$t('auth.new_password')" type="password" v:value="passwordForm.new_password"></v-input>
|
|
</div>
|
|
<div style="margin-top: var(--spacing-md); display: flex; justify-content: flex-end;">
|
|
<v-btn type="submit" variant="outline" :loading="pwdLoading" :disabled="!passwordForm.new_password || !passwordForm.old_password">
|
|
{{ $t('auth.change_password') || 'Update Password' }}
|
|
</v-btn>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Linked Accounts -->
|
|
<div>
|
|
<div class="section-title">{{ $t('auth.linked_accounts') || 'Linked Accounts' }}</div>
|
|
<div class="oauth-list">
|
|
<div class="oauth-item" v-for="provider in providers">
|
|
<div class="oauth-info">
|
|
<div class="oauth-icon">
|
|
<i class="fab" :class="'fa-' + provider.code"></i>
|
|
</div>
|
|
<div>
|
|
<div style="font-weight: 500;">{{ provider.name }}</div>
|
|
<div style="font-size: 0.8rem; color: var(--color-text-light);" v-if="isBound(provider.code)">
|
|
{{ getBindInfo(provider.code) }}
|
|
</div>
|
|
<div style="font-size: 0.8rem; color: var(--color-text-light);" v-else>
|
|
{{ $t('auth.not_linked') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<v-btn v-if="isBound(provider.code)" size="sm" variant="outline" color="danger" :click="() => unbind(provider.code)">
|
|
{{ $t('common.unbind') }}
|
|
</v-btn>
|
|
<v-btn v-else size="sm" variant="outline" :click="() => bind(provider.code)">
|
|
{{ $t('common.bind') }}
|
|
</v-btn>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</body>
|
|
<script setup>
|
|
user = $env.$vbase.user || {};
|
|
loading = false;
|
|
pwdLoading = false;
|
|
passwordForm = { old_password: '', new_password: '' };
|
|
|
|
// Bindings
|
|
providers = [];
|
|
bindings = [];
|
|
|
|
// Fetch fresh data
|
|
loadUser = async () => {
|
|
try {
|
|
user = await $env.$vbase.fetchUser();
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
loadBindings = async () => {
|
|
try {
|
|
// Get enabled providers
|
|
const providersRes = await $axios.get('/api/auth/login-methods');
|
|
providers = providersRes.methods.filter(p => p.type !== 'password' && p.type !== 'code' && p.type !== 'email_code' && p.type !== 'phone_code').map(p => ({
|
|
code: p.type,
|
|
name: p.name
|
|
}));
|
|
|
|
// Get current bindings
|
|
bindings = await $axios.get('/api/auth/me/bindings');
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
updateProfile = async () => {
|
|
loading = true;
|
|
try {
|
|
await $axios.patch('/api/auth/me', {
|
|
nickname: user.nickname,
|
|
email: user.email,
|
|
phone: user.phone,
|
|
avatar: user.avatar
|
|
});
|
|
$message.success($t('common.save_success'));
|
|
await loadUser();
|
|
} catch (e) {
|
|
$message.error(e.message);
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
};
|
|
|
|
changePassword = async () => {
|
|
if (!passwordForm.new_password || !passwordForm.old_password) return;
|
|
pwdLoading = true;
|
|
try {
|
|
await $axios.post('/api/auth/me/change-password', {
|
|
old_password: passwordForm.old_password,
|
|
new_password: passwordForm.new_password
|
|
});
|
|
$message.success($t('auth.password_changed'));
|
|
passwordForm.old_password = '';
|
|
passwordForm.new_password = '';
|
|
} catch (e) {
|
|
$message.error(e.message);
|
|
} finally {
|
|
pwdLoading = false;
|
|
}
|
|
};
|
|
|
|
handleLogout = async () => {
|
|
await $env.$vbase.logout('/login');
|
|
};
|
|
|
|
// OAuth Helpers
|
|
isBound = (code) => {
|
|
return bindings.some(b => b.provider === code);
|
|
};
|
|
|
|
getBindInfo = (code) => {
|
|
const b = bindings.find(b => b.provider === code);
|
|
return b ? (b.provider_name || b.email || $t('auth.linked')) : '';
|
|
};
|
|
|
|
bind = async (provider) => {
|
|
try {
|
|
// Fetch authorization URL
|
|
const res = await $axios.get('/api/auth/authorize/thirdparty', {
|
|
params: {
|
|
provider: provider,
|
|
redirect: window.location.origin + '/callback/' + provider,
|
|
bind_mode: true
|
|
}
|
|
});
|
|
// Redirect to provider auth page
|
|
if (res.auth_url) {
|
|
window.location.href = res.auth_url;
|
|
}
|
|
} catch (e) {
|
|
$message.error(e.message);
|
|
}
|
|
};
|
|
|
|
unbind = async (provider) => {
|
|
try {
|
|
await $message.confirm($t('auth.unbind_confirm'));
|
|
await $axios.delete(`/api/auth/me/bindings/${provider}`);
|
|
$message.success($t('common.success'));
|
|
loadBindings();
|
|
} catch (e) {
|
|
if (e !== 'cancel') $message.error(e.message);
|
|
}
|
|
};
|
|
|
|
triggerAvatarUpload = async () => {
|
|
try {
|
|
const url = await $message.prompt($t('user.avatar_url'), user.avatar || '');
|
|
if (url) {
|
|
user.avatar = url;
|
|
}
|
|
} catch (e) {
|
|
// Cancelled
|
|
}
|
|
};
|
|
|
|
</script>
|
|
<script>
|
|
$data.loadUser();
|
|
$data.loadBindings();
|
|
</script>
|
|
|
|
</html> |