Compare commits

...

3 Commits

Author SHA1 Message Date
baf529e838 🐛 Bug fixes and optimization 2024-08-13 18:26:36 +08:00
223f97038c 🌐 Localize more pages 2024-08-13 17:54:13 +08:00
4e42b44958 🌐 Localize sign in & sign up page 2024-08-13 17:43:44 +08:00
19 changed files with 198 additions and 87 deletions

View File

@ -1,12 +1,14 @@
<template>
<div class="text-xs text-grey" :class="(props.centered ?? true) ? 'text-center' : 'text-left'">
<p>Copyright © {{ new Date().getFullYear() }} Solsynth LLC</p>
<div class="text-xs text-grey" :class="props.noCentered ? 'text-left' : 'text-center'">
<p>{{ t("copyright") }} © {{ new Date().getFullYear() }} {{ t("brandNameFormal") }}</p>
<p>Powered by <a class="underline" :href="projects[props.service][1]">{{ projects[props.service][0] }}</a></p>
</div>
</template>
<script setup lang="ts">
const props = defineProps<{ service: string, centered?: boolean }>()
const props = defineProps<{ service: string, noCentered?: boolean }>()
const { t } = useI18n()
const projects: { [id: string]: [string, string] } = {
"passport": ["Hydrogen.Passport", "https://git.solsynth.dev/Hydrogen/Passport"],

View File

@ -12,20 +12,21 @@
<v-divider class="border-opacity-50 my-2" />
<v-list-item title="Dashboard" prepend-icon="mdi-account-supervisor" exact to="/users/me" />
<v-list-item title="Sign out" prepend-icon="mdi-logout" @click="signOut"></v-list-item>
<v-list-item :title="t('userMenuDashboard')" prepend-icon="mdi-account-supervisor" exact to="/users/me" />
<v-list-item :title="t('userMenuSignOut')" prepend-icon="mdi-logout" @click="signOut"></v-list-item>
</v-list>
<v-list density="compact" v-else>
<v-list-item title="Sign in" prepend-icon="mdi-login-variant" to="/auth/sign-in" />
<v-list-item title="Create account" prepend-icon="mdi-account-plus" to="/auth/sign-up" />
<v-list-item :title="t('userMenuSignIn')" prepend-icon="mdi-login-variant" to="/auth/sign-in" />
<v-list-item :title="t('userMenuSignUp')" prepend-icon="mdi-account-plus" to="/auth/sign-up" />
</v-list>
</v-menu>
</template>
<script setup lang="ts">
import { defaultUserinfo, useUserinfo } from "@/stores/userinfo"
import { useUserinfo } from "@/stores/userinfo"
import { computed } from "vue"
const {t} = useI18n()
const config = useRuntimeConfig()
const id = useUserinfo()
@ -51,7 +52,7 @@ const avatar = computed(() => {
function signOut() {
useCookie("__hydrogen_atk", { watch: "shallow" }).value = null
useCookie("__hydrogen_rtk", { watch: "shallow" }).value = null
id.userinfo = defaultUserinfo
id.userinfo = null
reloadNuxtApp()
}
</script>

View File

@ -1,19 +1,20 @@
<template>
<div class="flex items-center">
<v-form class="flex-grow-1" @submit.prevent="submit">
<v-text-field label="Username" variant="solo" density="comfortable" class="mb-3" :hide-details="true"
<v-text-field :label="t('username')" variant="solo" density="comfortable" class="mb-3" :hide-details="true"
:disabled="props.loading" v-model="probe" />
<v-text-field label="Password" variant="solo" density="comfortable" type="password" :disabled="props.loading"
<v-text-field :label="t('password')" variant="solo" density="comfortable" type="password"
:disabled="props.loading"
v-model="password" />
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
Something went wrong... {{ error }}
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="flex justify-between">
<v-btn type="button" variant="plain" color="grey-darken-3" to="/auth/sign-up">Sign up</v-btn>
<v-btn type="button" variant="plain" color="grey-darken-3" to="/auth/sign-up">{{ t("userMenuSignUp") }}</v-btn>
<v-btn
type="submit"
@ -23,7 +24,7 @@
append-icon="mdi-arrow-right"
:disabled="props.loading"
>
Next
{{ t("next") }}
</v-btn>
</div>
</v-form>
@ -31,8 +32,7 @@
</template>
<script setup lang="ts">
import { ref } from "vue"
const { t } = useI18n()
const config = useRuntimeConfig()
const probe = ref("")

View File

@ -2,21 +2,19 @@
<div>
<v-icon icon="mdi-lan-check" size="32" color="grey-darken-3" class="mb-3" />
<h1 class="font-bold text-xl">All Done!</h1>
<p>Welcome back! You just signed in right now! We're going to direct you to dashboard...</p>
<h1 class="font-bold text-xl">{{ t("signInCompleted") }}</h1>
<p>{{ t("signInCompletedCaption") }}</p>
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
Something went wrong... {{ error }}
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue"
import { useRoute, useRouter } from "vue-router"
const { t } = useI18n()
const config = useRuntimeConfig()
const route = useRoute()

View File

@ -2,7 +2,7 @@
<div class="w-full max-w-[720px]">
<v-expand-transition>
<v-alert v-show="route.query['redirect_uri']" variant="tonal" type="info" class="text-xs">
You need to sign in before access that page. After you signed in, we will redirect you to: <br />
{{ t("callbackHint") }} <br />
<span class="font-mono">{{ route.query["redirect_uri"] }}</span>
</v-alert>
</v-expand-transition>
@ -10,5 +10,6 @@
</template>
<script setup lang="ts">
const { t } = useI18n()
const route = useRoute()
</script>

View File

@ -2,7 +2,7 @@
<div class="flex items-center">
<v-form class="flex-grow-1" @submit.prevent="submit">
<div v-if="inputType === 'one-time-password'" class="text-center">
<p class="text-xs opacity-90">Check your inbox!</p>
<p class="text-xs opacity-90">{{ t("multiFactorHint") }}</p>
<v-otp-input
class="pt-0"
variant="solo"
@ -15,7 +15,7 @@
</div>
<v-text-field
v-else
label="Password"
:label="t('password')"
type="password"
variant="solo"
density="comfortable"
@ -25,7 +25,7 @@
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
Something went wrong... {{ error }}
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
@ -38,7 +38,7 @@
append-icon="mdi-arrow-right"
:disabled="loading"
>
Next
{{ t("next") }}
</v-btn>
</div>
</v-form>
@ -46,8 +46,7 @@
</template>
<script setup lang="ts">
import { computed, ref } from "vue"
const { t } = useI18n()
const config = useRuntimeConfig()
const password = ref("")

View File

@ -17,13 +17,13 @@
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
Something went wrong... {{ error }}
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="flex justify-end">
<v-btn variant="text" color="primary" class="justify-self-end" append-icon="mdi-arrow-right" @click="submit">
Next
{{ t("next") }}
</v-btn>
</div>
</div>
@ -31,8 +31,7 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue"
const { t } = useI18n()
const config = useRuntimeConfig()
const focus = ref<number | null>(null)
@ -78,7 +77,7 @@ async function submit() {
function getFactorType(item: any) {
switch (item.type) {
case 1:
return { icon: "mdi-email-fast", label: "Email Validation" }
return { icon: "mdi-email-fast", label: t('multiFactorTypeEmail') }
}
}

View File

@ -10,5 +10,36 @@
"indexProductListHint": "See some of our products just there",
"indexActivities": "Activities",
"indexActivitiesCaption": "Keep in touch,\nand learn what we doing recently.",
"indexActivitiesHint": "See some posts in our realm just here"
"indexActivitiesHint": "See some posts in our realm just here",
"userMenuDashboard": "Dashboard",
"userMenuSignOut": "Sign Out",
"userMenuSignIn": "Sign In",
"userMenuSignUp": "Create Account",
"next": "Next",
"errorOccurred": " Something went wrong... {0}",
"username": "Name",
"nickname": "Nick",
"email": "Email Address",
"password": "Password",
"copyright": "Copyright",
"signUpTitle": "Create an account",
"signUpCaption": "Create an account on Solar Network. Then enjoy all our services.",
"signUpCompleted": "You successfully created an account on Solar Network. Now sign in to your account and start exploring!",
"signUpCompletedAction": "Let's go!",
"signInTitle": "Sign In",
"signInCaption": "Sign in via your account to access the entire Solar Network.",
"multiFactorCaption": "We need to verify that the person trying to access your account is you.",
"multiFactorHint": "Check your inbox",
"multiFactorTypeEmail": "Email One-time-password",
"signInCompleted": "All Done",
"signInCompletedCaption": "Welcome back! You just signed in right now! We're going to direct you to dashboard...",
"transferredToSolianHint": "This part of the functionality has been transferred to our application Solian, please download it or open it in your browser. To learn more, please visit the project description page.",
"personalize": "Personalize",
"personalizeCaption": "Bring your own color to the Solar Network.",
"security": "Security",
"securityCaption": "Guard your Solar Network account.",
"userActivity": "Activity",
"userActivityCaption": "Recent posts of this user.",
"productArchived": "Archived",
"callbackHint": "You need to sign in before access that page. After you signed in, you will be redirected to:"
}

View File

@ -10,5 +10,36 @@
"indexProductListHint": "在这里看看我们的一些产品",
"indexActivities": "动态",
"indexActivitiesCaption": "开发软件,闭门造车是大忌,了解我们最近在做什么。",
"indexActivitiesHint": "看看我们领域中的一些帖子"
"indexActivitiesHint": "看看我们领域中的一些帖子",
"userMenuDashboard": "仪表盘",
"userMenuSignOut": "登出",
"userMenuSignIn": "登陆",
"userMenuSignUp": "注册帐号",
"next": "下一步",
"errorOccurred": "发生错误了… {0}",
"username": "用户名",
"nickname": "显示名",
"email": "邮件地址",
"password": "密码",
"copyright": "版权所有",
"signUpTitle": "创建账号",
"signUpCaption": "在 Solar Network 上创建一个帐号,以享受我们所有的服务。",
"signUpCompleted": "您已成功创建 Solar Network 账户。现在登录您的账户,开始探索吧!",
"signUpCompletedAction": "出发",
"signInTitle": "登陆",
"signInCaption": "通过您的账户登录以访问整个 Solar Network。",
"multiFactorCaption": "我们需要验证试图访问您账户的人是您本人。",
"multiFactorHint": "检查您的收件箱",
"multiFactorTypeEmail": "电子邮件一次性密码",
"signInCompleted": "完成",
"signInCompletedCaption": "欢迎回来!您刚刚登录成功!我们将引导您进入仪表板...",
"transferredToSolianHint": "此部分功能已转移到我们的应用程序 Solian请下载或在浏览器中打开。如需了解更多信息请访问项目描述页面。",
"personalize": "个性化",
"personalizeCaption": "为 Solar Network 染上你的色彩。",
"security": "安全",
"securityCaption": "保护您的 Solar Network 账户。",
"userActivity": "活动",
"userActivityCaption": "此用户的最新帖子。",
"productArchived": "已归档",
"callbackHint": "访问该页面前,您需要先登录。登录后,我们会将把您重定向到:"
}

View File

@ -1,5 +1,5 @@
export default defineNuxtRouteMiddleware((to, from) => {
const state = useLoggedInState();
const state = useLoggedInState()
if (!state.value) {
return navigateTo(`/auth/sign-in?redirect_uri=${to.fullPath}`)

View File

@ -19,7 +19,7 @@ export default defineNuxtConfig({
useCookie: true,
cookieCrossOrigin: true,
cookieKey: "__capital_i18n",
redirectOn: "root",
redirectOn: "no prefix",
},
locales: [
{ code: "en", name: "English", file: "en-US.json" },

View File

@ -6,9 +6,9 @@
<v-card-text class="card-grid pa-9">
<div>
<v-avatar color="accent" icon="mdi-login-variant" size="large" class="card-rounded mb-2" />
<h1 class="text-2xl">Sign in</h1>
<p v-if="ticket">We need to verify that the person trying to access your account is you.</p>
<p v-else>Sign in via your Solar ID to access the entire Solar Network.</p>
<h1 class="text-2xl">{{ t("signInTitle") }}</h1>
<p v-if="ticket" class="max-w-5/6">{{ t("multiFactorCaption") }}</p>
<p v-else class="max-w-5/6">{{ t("signInCaption") }}</p>
</div>
<v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]">
@ -26,12 +26,13 @@
<script setup lang="ts">
import { type Component, onMounted, ref } from "vue"
import { useRoute } from "vue-router"
import FactorPicker from "~/components/auth/FactorPicker.vue"
import FactorApplicator from "~/components/auth/FactorApplicator.vue"
import AccountAuthenticate from "~/components/auth/Authenticate.vue"
import AuthenticateCompleted from "~/components/auth/AuthenticateCompleted.vue"
const { t } = useI18n()
definePageMeta({
alias: ["/auth/mfa"],
})
@ -58,6 +59,21 @@ async function pickUpTicket() {
onMounted(() => pickUpTicket())
const id = useUserinfo()
const router = useRouter()
watch(id, (value) => {
if (value.isLoggedIn) {
if (route.query["close"]) {
window.close()
} else if (route.query["redirect_uri"]) {
window.open((route.query["redirect_uri"] as string) ?? "/", "_self")
} else {
router.push("/users/me")
}
}
}, { deep: true, immediate: true })
const panel = ref("authenticate")
const panels: { [id: string]: Component } = {

View File

@ -6,8 +6,8 @@
<v-card-text class="card-grid pa-9">
<div>
<v-avatar color="accent" icon="mdi-login-variant" size="large" class="card-rounded mb-2" />
<h1 class="text-2xl">Create an account</h1>
<p>Create an account on Solar Network. Then enjoy all our services.</p>
<h1 class="text-2xl">{{ t("signUpTitle") }}</h1>
<p class="max-w-5/6">{{ t("signUpCaption") }}</p>
</div>
<div class="flex items-center">
@ -16,7 +16,7 @@
<v-col :cols="6">
<v-text-field
hide-details
label="Name"
:label="t('username')"
autocomplete="username"
variant="solo"
density="comfortable"
@ -26,7 +26,7 @@
<v-col :cols="6">
<v-text-field
hide-details
label="Nick"
:label="t('nickname')"
autocomplete="nickname"
variant="solo"
density="comfortable"
@ -36,7 +36,7 @@
<v-col :cols="12">
<v-text-field
hide-details
label="Email Address"
:label="t('email')"
type="email"
variant="solo"
density="comfortable"
@ -46,7 +46,7 @@
<v-col :cols="12">
<v-text-field
hide-details
label="Password"
:label="t('password')"
type="password"
autocomplete="new-password"
variant="solo"
@ -58,17 +58,17 @@
<v-expand-transition>
<v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3">
Something went wrong... {{ error }}
{{ t("errorOccurred", [error]) }}
</v-alert>
</v-expand-transition>
<div class="flex justify-between">
<v-btn type="button" variant="plain" color="grey-darken-3" to="/auth/sign-in">
Sign in
{{ t("userMenuSignIn") }}
</v-btn>
<v-btn type="submit" variant="text" color="primary" append-icon="mdi-arrow-right" :disabled="loading">
Next
{{ t("next") }}
</v-btn>
</div>
</v-form>
@ -79,11 +79,11 @@
<v-dialog v-model="done" class="max-w-[560px]">
<v-card title="Congratulations">
<template #text>
You successfully created an account on Solar Network. Now sign in to your account and start exploring!
{{ t("signUpCompleted") }}
</template>
<template #actions>
<div class="flex flex-grow-1 justify-end">
<v-btn @click="callback">Let's go</v-btn>
<v-btn @click="callback">{{ t("signUpCompletedAction") }}</v-btn>
</div>
</template>
</v-card>
@ -99,6 +99,7 @@ import { useRoute, useRouter } from "vue-router"
const error = ref<string | null>(null)
const { t } = useI18n()
const config = useRuntimeConfig()
const route = useRoute()
@ -137,7 +138,7 @@ function callback() {
if (route.params["closable"]) {
window.close()
} else {
router.push({ name: "auth.sign-in" })
router.push("/auth/sign-in")
}
}
</script>

View File

@ -18,7 +18,7 @@
size="small"
class="mx-[-6px]"
>
Archived
{{ t("productArchived") }}
</v-chip>
</v-col>
</v-row>
@ -41,10 +41,10 @@
</template>
<template #not-found>
<v-empty-state
icon="mdi-flask-empty-remove-outline"
text="We haven't this product, yet."
title="Not Found"
class="no-content-placeholder"
icon="mdi-flask-empty-remove-outline"
text="We haven't this product, yet."
title="Not Found"
class="no-content-placeholder"
>
<template #actions>
<v-btn prepend-icon="mdi-list-box" variant="plain" text="Back to index" to="/products" exact />
@ -64,8 +64,10 @@
</style>
<script setup lang="ts">
const route = useRoute();
const { data: page } = await useAsyncData('page', queryContent(route.path).findOne)
const route = useRoute()
const { t } = useI18n()
const { data: page } = await useAsyncData("page", queryContent(route.path).findOne)
</script>
<style scoped>

View File

@ -18,7 +18,7 @@
size="small"
class="mx-[-6px]"
>
Archived
{{ t("productArchived") }}
</v-chip>
</v-col>
</v-row>
@ -29,9 +29,11 @@
<script setup lang="ts">
useHead({
title: 'Products',
title: "Products",
})
const { t } = useI18n()
const { data: products } = await useAsyncData("products", () => queryContent("/products").find())
</script>

View File

@ -13,12 +13,14 @@
<div class="mb-5 text-xs text-grey flex flex-col">
<span>Solar Network User Web Preview</span>
<span>To get full view of this user's profile, open it on <a class="underline" :href="externalOpenLink">Solian</a></span>
<span>
To get full view of this user's profile, open it on <a class="underline" :href="externalOpenLink">Solian</a>
</span>
</div>
<div>
<h1 class="text-xl">Activity</h1>
<span>Recent posts of this user.</span>
<h1 class="text-xl">{{ t("userActivity") }}</h1>
<span>{{ t("userActivityCaption") }}</span>
</div>
</div>
@ -32,9 +34,10 @@
<script setup lang="ts">
definePageMeta({
alias: ["/@:name(.*)*"]
alias: ["/@:name(.*)*"],
})
const { t } = useI18n()
const route = useRoute()
const config = useRuntimeConfig()

View File

@ -13,8 +13,8 @@
<div class="mb-5">
<div class="mx-[2.5ch]">
<h2 class="text-xl">Personalize</h2>
<span class="text-sm">Bring your own color to the Solar Network.</span>
<h2 class="text-xl">{{ t("personalize") }}</h2>
<span class="text-sm">{{ t("personalizeCaption") }}</span>
</div>
<v-alert
@ -22,21 +22,21 @@
type="info"
variant="tonal"
density="comfortable"
text="This part of the functionality has been transferred to our application Solian, please download it or open it in your browser. To learn more, please visit the project description page."
:text="t('transferredToSolianHint')"
/>
</div>
<div class="mb-5">
<div class="mx-[2.5ch]">
<h2 class="text-xl">Security</h2>
<span class="text-sm">Guard your Solar Network account.</span>
<h2 class="text-xl">{{ t("security") }}</h2>
<span class="text-sm">{{ t("securityCaption") }}</span>
</div>
<account-auth-ticket-table class="mt-3" />
</div>
<div class="mb-5 mx-[2.5ch]">
<copyright service="passport" :centered="false" />
<copyright service="passport" no-centered />
</div>
</div>
</v-container>
@ -47,6 +47,7 @@ definePageMeta({
middleware: ["auth"],
})
const { t } = useI18n()
const config = useRuntimeConfig()
const auth = useUserinfo()

View File

@ -2,18 +2,6 @@ import { defineStore } from "pinia"
import { ref } from "vue"
import { solarFetch } from "~/utils/request"
export interface Userinfo {
isLoggedIn: boolean
displayName: string
data: any
}
export const defaultUserinfo: Userinfo = {
isLoggedIn: false,
displayName: "Citizen",
data: null,
}
export function useAtk() {
return useCookie("__hydrogen_atk", { watch: "shallow" })
}
@ -31,6 +19,9 @@ export const useUserinfo = defineStore("userinfo", () => {
const isReady = ref(false)
const isLoggedIn = ref(false)
let fetchCompleter: Completer<boolean> | null = null
let refreshCompleter: Completer<string> | null = null
const lastRefreshedAt = ref<Date | null>(null)
function setTokenSet(atk: string, rtk: string) {
@ -45,6 +36,12 @@ export const useUserinfo = defineStore("userinfo", () => {
return useAtk().value
}
if (refreshCompleter != null) {
return await refreshCompleter.promise
} else {
refreshCompleter = new Completer<string>()
}
const config = useRuntimeConfig()
const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/auth/token`, {
@ -62,18 +59,28 @@ export const useUserinfo = defineStore("userinfo", () => {
const out = await res.json()
console.log("[PASSPORT] Access token has been refreshed now.")
setTokenSet(out["access_token"], out["refresh_token"])
refreshCompleter.complete(out["access_token"])
return out["access_token"]
}
}
async function readProfiles() {
if (fetchCompleter != null) {
await fetchCompleter.promise
return
} else {
fetchCompleter = new Completer<boolean>()
}
if (!useLoggedInState().value) {
fetchCompleter.complete(true)
isReady.value = true
}
const res = await solarFetch("/cgi/auth/users/me")
if (res.status !== 200) {
fetchCompleter.complete(true)
isReady.value = true
return
}
@ -83,7 +90,21 @@ export const useUserinfo = defineStore("userinfo", () => {
isLoggedIn.value = true
isReady.value = true
userinfo.value = data
fetchCompleter.complete(true)
}
return { userinfo, lastRefreshedAt, isLoggedIn, isReady, setTokenSet, getAtk, readProfiles }
return { userinfo, lastRefreshedAt, isLoggedIn, isReady, fetchCompleter, setTokenSet, getAtk, readProfiles }
})
export class Completer<T> {
public readonly promise: Promise<T>
public complete: (value: (PromiseLike<T> | T)) => void
private reject: (reason?: any) => void
public constructor() {
this.promise = new Promise<T>((resolve, reject) => {
this.complete = resolve
this.reject = reject
})
}
}

View File

@ -1,4 +1,7 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
"extends": "./.nuxt/tsconfig.json",
"compilerOptions": {
"strictPropertyInitialization": false
}
}