✨ DysonNetwork.Pass service frontend
This commit is contained in:
55
DysonNetwork.Pass/Client/src/views/accounts/me.vue
Normal file
55
DysonNetwork.Pass/Client/src/views/accounts/me.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="max-w-3xl mx-auto p-8">
|
||||
<div class="flex items-center gap-6 mb-8">
|
||||
<n-avatar round :size="100" :alt="userStore.user.name">
|
||||
<n-icon size="48">
|
||||
<person-round />
|
||||
</n-icon>
|
||||
</n-avatar>
|
||||
<div>
|
||||
<n-text strong class="text-2xl">
|
||||
{{ userStore.user.nick || userStore.user.name }}
|
||||
</n-text>
|
||||
<n-text depth="3" class="block">@{{ userStore.user.name }}</n-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between mb-2">
|
||||
<n-text>Level {{ userStore.user.profile.level }}</n-text>
|
||||
<n-text>{{ userStore.user.profile.experience }} XP</n-text>
|
||||
</div>
|
||||
<n-progress
|
||||
type="line"
|
||||
:percentage="userStore.user.profile.leveling_progress"
|
||||
:height="8"
|
||||
status="success"
|
||||
:show-indicator="false"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="userStore.user.profile.bio" class="mt-8">
|
||||
<n-h3>About</n-h3>
|
||||
<n-p>{{ userStore.user.profile.bio }}</n-p>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<n-button type="primary" icon-placement="right" tag="a" href="https://solian.app/#/account">
|
||||
Open in the Solian
|
||||
<template #icon>
|
||||
<n-icon>
|
||||
<launch-outlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NAvatar, NText, NProgress, NH3, NP, NButton, NIcon } from 'naive-ui'
|
||||
import { PersonRound, LaunchOutlined } from '@vicons/material'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
</script>
|
@@ -1,38 +1,34 @@
|
||||
<template>
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<n-card class="max-w-lg text-center" title="Captcha">
|
||||
<div class="flex justify-center mb-4 mt-2">
|
||||
<div v-if="provider === 'cloudflare'" class="cf-turnstile" :data-sitekey="apiKey" data-callback="onTurnstileSuccess"></div>
|
||||
<div v-else-if="provider === 'recaptcha'" class="g-recaptcha" :data-sitekey="apiKey" data-callback="onRecaptchaSuccess"></div>
|
||||
<div v-else-if="provider === 'hcaptcha'" class="h-captcha" :data-sitekey="apiKey" data-callback="onHcaptchaSuccess"></div>
|
||||
<div v-else class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<span>Captcha provider not configured correctly.</span>
|
||||
</div>
|
||||
<n-card class="max-w-lg text-center" title="Captcha Verification">
|
||||
<div class="mb-4 mt-2">
|
||||
<Captcha :provider="provider" :api-key="apiKey" @verified="onCaptchaVerified" />
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<div class="font-semibold mb-1">Solar Network Anti-Robot</div>
|
||||
<div class="text-base-content/70">
|
||||
Powered by
|
||||
<template v-if="provider === 'cloudflare'">
|
||||
<a href="https://www.cloudflare.com/turnstile/" class="link link-hover">
|
||||
<a href="https://www.cloudflare.com/turnstile/" class="link link-hover" target="_blank" rel="noopener noreferrer">
|
||||
Cloudflare Turnstile
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="provider === 'recaptcha'">
|
||||
<a href="https://www.google.com/recaptcha/" class="link link-hover">
|
||||
<a href="https://www.google.com/recaptcha/" class="link link-hover" target="_blank" rel="noopener noreferrer">
|
||||
Google reCaptcha
|
||||
</a>
|
||||
</template>
|
||||
<template v-else-if="provider === 'hcaptcha'">
|
||||
<a href="https://www.hcaptcha.com/" class="link link-hover" target="_blank" rel="noopener noreferrer">
|
||||
hCaptcha
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>Nothing</span>
|
||||
</template>
|
||||
<br/>
|
||||
Hosted by
|
||||
<a href="https://github.com/Solsynth/DysonNetwork" class="link link-hover">
|
||||
<a href="https://github.com/Solsynth/DysonNetwork" class="link link-hover" target="_blank" rel="noopener noreferrer">
|
||||
DysonNetwork.Sphere
|
||||
</a>
|
||||
</div>
|
||||
@@ -42,43 +38,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { NCard } from 'naive-ui';
|
||||
import Captcha from '@/components/Captcha.vue';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
// Get provider and API key from app data
|
||||
// @ts-ignore
|
||||
const { Provider: provider, ApiKey: apiKey } = window.__APP_DATA__ || {};
|
||||
const provider = ref((window as any).__APP_DATA__?.Provider || '');
|
||||
const apiKey = ref((window as any).__APP_DATA__?.ApiKey || '');
|
||||
|
||||
// Load the appropriate CAPTCHA script based on provider
|
||||
const loadCaptchaScript = () => {
|
||||
if (!provider) return;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.async = true;
|
||||
script.defer = true;
|
||||
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'recaptcha':
|
||||
script.src = 'https://www.recaptcha.net/recaptcha/api.js';
|
||||
break;
|
||||
case 'cloudflare':
|
||||
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js';
|
||||
break;
|
||||
case 'hcaptcha':
|
||||
script.src = 'https://js.hcaptcha.com/1/api.js';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
document.head.appendChild(script);
|
||||
};
|
||||
|
||||
// Handle successful CAPTCHA verification
|
||||
(window as any).onTurnstileSuccess = (token: string) => {
|
||||
const onCaptchaVerified = (token: string) => {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(`captcha_tk=${token}`, '*');
|
||||
}
|
||||
@@ -88,31 +59,4 @@ const loadCaptchaScript = () => {
|
||||
window.location.href = `${redirectUri}?captcha_tk=${encodeURIComponent(token)}`;
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).onRecaptchaSuccess = (token: string) => {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(`captcha_tk=${token}`, '*');
|
||||
}
|
||||
|
||||
const redirectUri = route.query.redirect_uri as string;
|
||||
if (redirectUri) {
|
||||
window.location.href = `${redirectUri}?captcha_tk=${encodeURIComponent(token)}`;
|
||||
}
|
||||
};
|
||||
|
||||
(window as any).onHcaptchaSuccess = (token: string) => {
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage(`captcha_tk=${token}`, '*');
|
||||
}
|
||||
|
||||
const redirectUri = route.query.redirect_uri as string;
|
||||
if (redirectUri) {
|
||||
window.location.href = `${redirectUri}?captcha_tk=${encodeURIComponent(token)}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Load CAPTCHA script when component mounts
|
||||
onMounted(() => {
|
||||
loadCaptchaScript();
|
||||
});
|
||||
</script>
|
||||
</script>
|
174
DysonNetwork.Pass/Client/src/views/create-account.vue
Normal file
174
DysonNetwork.Pass/Client/src/views/create-account.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<n-card class="w-full max-w-md" title="Create a new Solar Network ID">
|
||||
<n-spin :show="isLoading">
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
@submit.prevent="handleCreateAccount"
|
||||
>
|
||||
<n-form-item path="name" label="Username">
|
||||
<n-input v-model:value="formModel.name" size="large" />
|
||||
</n-form-item>
|
||||
<n-form-item path="nick" label="Nickname">
|
||||
<n-input v-model:value="formModel.nick" size="large" />
|
||||
</n-form-item>
|
||||
<n-form-item path="email" label="Email">
|
||||
<n-input v-model:value="formModel.email" placeholder="your@email.com" size="large" />
|
||||
</n-form-item>
|
||||
<n-form-item path="password" label="Password">
|
||||
<n-input
|
||||
v-model:value="formModel.password"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
placeholder="Enter your password"
|
||||
size="large"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="captchaToken">
|
||||
<div class="flex justify-center w-full">
|
||||
<captcha
|
||||
:provider="captchaProvider"
|
||||
:api-key="captchaApiKey"
|
||||
@verified="onCaptchaVerified"
|
||||
/>
|
||||
</div>
|
||||
</n-form-item>
|
||||
|
||||
<n-button type="primary" attr-type="submit" block size="large" :disabled="isLoading">
|
||||
Create Account
|
||||
</n-button>
|
||||
|
||||
<div class="mt-3 text-sm text-center opacity-75">
|
||||
<n-button text block @click="router.push('/login')" size="tiny">
|
||||
Already have an account? Login
|
||||
</n-button>
|
||||
</div>
|
||||
</n-form>
|
||||
<n-alert
|
||||
v-if="error"
|
||||
title="Error"
|
||||
type="error"
|
||||
closable
|
||||
@close="error = null"
|
||||
class="mt-4"
|
||||
>
|
||||
{{ error }}
|
||||
</n-alert>
|
||||
</n-spin>
|
||||
</n-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
NCard,
|
||||
NInput,
|
||||
NButton,
|
||||
NSpin,
|
||||
NAlert,
|
||||
NForm,
|
||||
NFormItem,
|
||||
type FormInst,
|
||||
type FormRules,
|
||||
useMessage,
|
||||
} from 'naive-ui'
|
||||
import Captcha from '@/components/Captcha.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
const formModel = reactive({
|
||||
name: '',
|
||||
nick: '',
|
||||
email: '',
|
||||
password: '',
|
||||
language: 'en-us',
|
||||
captchaToken: '',
|
||||
})
|
||||
|
||||
const rules: FormRules = {
|
||||
name: [
|
||||
{ required: true, message: 'Please enter a username', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[A-Za-z0-9_-]+$/,
|
||||
message: 'Username can only contain letters, numbers, underscores, and hyphens.',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
nick: [{ required: true, message: 'Please enter a nickname', trigger: 'blur' }],
|
||||
email: [
|
||||
{ required: true, message: 'Please enter your email', trigger: 'blur' },
|
||||
{ type: 'email', message: 'Please enter a valid email address', trigger: ['input', 'blur'] },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: 'Please enter a password', trigger: 'blur' },
|
||||
{ min: 4, message: 'Password must be at least 4 characters long', trigger: 'blur' },
|
||||
],
|
||||
captchaToken: [{ required: true, message: 'Please complete the captcha verification.' }],
|
||||
}
|
||||
|
||||
// Get captcha provider and API key from global data
|
||||
const captchaProvider = ref((window as any).__APP_DATA__?.Provider || '')
|
||||
const captchaApiKey = ref((window as any).__APP_DATA__?.ApiKey || '')
|
||||
|
||||
const onCaptchaVerified = (token: string) => {
|
||||
formModel.captchaToken = token
|
||||
}
|
||||
|
||||
const messageDisplay = useMessage()
|
||||
|
||||
function handleCreateAccount(e: Event) {
|
||||
e.preventDefault()
|
||||
formRef.value?.validate(async (errors) => {
|
||||
if (errors) {
|
||||
return
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/accounts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
name: formModel.name,
|
||||
nick: formModel.nick,
|
||||
email: formModel.email,
|
||||
password: formModel.password,
|
||||
language: formModel.language,
|
||||
captcha_token: formModel.captchaToken,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.text()
|
||||
throw new Error(message || 'Failed to create account.')
|
||||
}
|
||||
|
||||
// On success, redirect to login page
|
||||
const messageReactive = messageDisplay.success(
|
||||
'Welcome to Solar Network! Your account has been created successfully.',
|
||||
{ duration: 8000 },
|
||||
)
|
||||
setTimeout(() => {
|
||||
messageReactive.type = 'info'
|
||||
messageReactive.content = "Don't forget to check your email for activation instructions."
|
||||
}, 3000)
|
||||
router.push('/login')
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { NCard, NSpace, NInput, NButton, NSpin, NAlert, NProgress } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
|
||||
import FingerprintJS from '@fingerprintjs/fingerprintjs'
|
||||
|
||||
// State management
|
||||
@@ -190,6 +192,9 @@ async function handleVerifyFactor() {
|
||||
}
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
|
||||
async function exchangeToken() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
@@ -210,7 +215,14 @@ async function exchangeToken() {
|
||||
|
||||
const { token } = await response.json()
|
||||
localStorage.setItem('authToken', token)
|
||||
await router.push('/')
|
||||
await userStore.fetchUser()
|
||||
|
||||
const redirectUri = route.query.redirect_uri as string
|
||||
if (redirectUri) {
|
||||
window.location.href = redirectUri
|
||||
} else {
|
||||
await router.push('/')
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e.message
|
||||
stage.value = 'select-factor' // Go back if token exchange fails
|
||||
@@ -253,6 +265,11 @@ function getFactorName(factorType: number) {
|
||||
<n-button type="primary" block class="mt-4" size="large" @click="handleFindAccount">
|
||||
Continue
|
||||
</n-button>
|
||||
<div class="mt-3 text-sm text-center opacity-75">
|
||||
<n-button text block @click="router.push('/create-account')" size="tiny">
|
||||
Don't have an account? Create one!
|
||||
</n-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stage 2: Select Factor -->
|
||||
|
Reference in New Issue
Block a user