♻️ Moved sign in from Passport to here
This commit is contained in:
		
							
								
								
									
										5
									
								
								app.vue
									
									
									
									
									
								
							
							
						
						
									
										5
									
								
								app.vue
									
									
									
									
									
								
							| @@ -11,6 +11,7 @@ import { useTheme } from "vuetify" | |||||||
| import "@unocss/reset/tailwind.css" | import "@unocss/reset/tailwind.css" | ||||||
|  |  | ||||||
| const theme = useTheme() | const theme = useTheme() | ||||||
|  | const userinfo = useUserinfo() | ||||||
|  |  | ||||||
| onMounted(() => { | onMounted(() => { | ||||||
|   theme.global.name.value = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" |   theme.global.name.value = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" | ||||||
| @@ -18,5 +19,9 @@ onMounted(() => { | |||||||
|   window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", event => { |   window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", event => { | ||||||
|     theme.global.name.value = event.matches ? "dark" : "light" |     theme.global.name.value = event.matches ? "dark" : "light" | ||||||
|   }) |   }) | ||||||
|  |  | ||||||
|  |   if (checkLoggedIn()) { | ||||||
|  |     userinfo.readProfiles() | ||||||
|  |   } | ||||||
| }) | }) | ||||||
| </script> | </script> | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								components/Copyright.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								components/Copyright.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="text-xs text-grey" :class="(props.centered ?? true) ? 'text-center' : 'text-left'"> | ||||||
|  |     <p>Copyright © {{ new Date().getFullYear() }} Solsynth LLC</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 projects: { [id: string]: [string, string] } = { | ||||||
|  |   "passport": ["Hydrogen.Passport", "https://git.solsynth.dev/Hydrogen/Passport"], | ||||||
|  |   "paperclip": ["Hydrogen.Paperclip", "https://git.solsynth.dev/Hydrogen/Paperclip"], | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										58
									
								
								components/UserMenu.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										58
									
								
								components/UserMenu.vue
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,58 @@ | |||||||
|  | <template> | ||||||
|  |   <v-menu> | ||||||
|  |     <template #activator="{ props }"> | ||||||
|  |       <v-btn flat exact v-bind="props" icon> | ||||||
|  |         <v-avatar color="transparent" icon="mdi-account-circle" :image="avatar" /> | ||||||
|  |       </v-btn> | ||||||
|  |     </template> | ||||||
|  |  | ||||||
|  |     <v-list density="compact" v-if="!id.userinfo.isLoggedIn"> | ||||||
|  |       <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> | ||||||
|  |     <v-list density="compact" v-else> | ||||||
|  |       <v-list-item :title="nickname" :subtitle="username" /> | ||||||
|  |  | ||||||
|  |       <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> | ||||||
|  |   </v-menu> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { defaultUserinfo, useUserinfo } from "@/stores/userinfo" | ||||||
|  | import { computed } from "vue" | ||||||
|  | import Cookie from "universal-cookie" | ||||||
|  |  | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const id = useUserinfo() | ||||||
|  |  | ||||||
|  | const username = computed(() => { | ||||||
|  |   if (id.userinfo.isLoggedIn) { | ||||||
|  |     return "@" + id.userinfo.data?.name | ||||||
|  |   } else { | ||||||
|  |     return "@visitor" | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | const nickname = computed(() => { | ||||||
|  |   if (id.userinfo.isLoggedIn) { | ||||||
|  |     return id.userinfo.data?.nick | ||||||
|  |   } else { | ||||||
|  |     return "Anonymous" | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | const avatar = computed(() => { | ||||||
|  |   return id.userinfo.data?.avatar ? `${config.public.solarNetworkApi}/cgi/files/attachments/${id.userinfo.data?.avatar}` : void 0 | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | function signout() { | ||||||
|  |   const ck = new Cookie() | ||||||
|  |   ck.remove("__hydrogen_atk") | ||||||
|  |   ck.remove("__hydrogen_rtk") | ||||||
|  |   id.userinfo = defaultUserinfo | ||||||
|  |   window.location.reload() | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										187
									
								
								components/account/AuthTicketTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								components/account/AuthTicketTable.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,187 @@ | |||||||
|  | <template> | ||||||
|  |   <v-expansion-panels> | ||||||
|  |     <v-expansion-panel eager title="Tickets"> | ||||||
|  |       <template #text> | ||||||
|  |         <v-card :loading="reverting.tickets" variant="outlined"> | ||||||
|  |           <v-data-table-server | ||||||
|  |             density="compact" | ||||||
|  |             :headers="dataDefinitions.tickets" | ||||||
|  |             :items="tickets" | ||||||
|  |             :items-length="pagination.tickets.total" | ||||||
|  |             :loading="reverting.tickets" | ||||||
|  |             v-model:items-per-page="pagination.tickets.pageSize" | ||||||
|  |             @update:options="readTickets" | ||||||
|  |             item-value="id" | ||||||
|  |           > | ||||||
|  |             <template v-slot:item="{ item }: { item: any }"> | ||||||
|  |               <tr> | ||||||
|  |                 <td>{{ item.id }}</td> | ||||||
|  |                 <td>{{ item.ip_address }}</td> | ||||||
|  |                 <td> | ||||||
|  |                   <v-tooltip :text="item.user_agent" location="top"> | ||||||
|  |                     <template #activator="{ props }"> | ||||||
|  |                       <div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[280px]"> | ||||||
|  |                         {{ item.user_agent }} | ||||||
|  |                       </div> | ||||||
|  |                     </template> | ||||||
|  |                   </v-tooltip> | ||||||
|  |                 </td> | ||||||
|  |                 <td>{{ new Date(item.created_at).toLocaleString() }}</td> | ||||||
|  |                 <td> | ||||||
|  |                   <v-tooltip text="Sign Out"> | ||||||
|  |                     <template #activator="{ props }"> | ||||||
|  |                       <v-btn | ||||||
|  |                         v-bind="props" | ||||||
|  |                         variant="text" | ||||||
|  |                         size="x-small" | ||||||
|  |                         color="error" | ||||||
|  |                         icon="mdi-logout-variant" | ||||||
|  |                         @click="killTicket(item)" | ||||||
|  |                       /> | ||||||
|  |                     </template> | ||||||
|  |                   </v-tooltip> | ||||||
|  |                 </td> | ||||||
|  |               </tr> | ||||||
|  |             </template> | ||||||
|  |           </v-data-table-server> | ||||||
|  |         </v-card> | ||||||
|  |       </template> | ||||||
|  |     </v-expansion-panel> | ||||||
|  |  | ||||||
|  |     <v-expansion-panel eager title="Events"> | ||||||
|  |       <template #text> | ||||||
|  |         <v-card :loading="reverting.events" variant="outlined"> | ||||||
|  |           <v-data-table-server | ||||||
|  |             density="compact" | ||||||
|  |             :headers="dataDefinitions.events" | ||||||
|  |             :items="events" | ||||||
|  |             :items-length="pagination.events.total" | ||||||
|  |             :loading="reverting.events" | ||||||
|  |             v-model:items-per-page="pagination.events.pageSize" | ||||||
|  |             @update:options="readEvents" | ||||||
|  |             item-value="id" | ||||||
|  |           > | ||||||
|  |             <template v-slot:item="{ item }: { item: any }"> | ||||||
|  |               <tr> | ||||||
|  |                 <td>{{ item.id }}</td> | ||||||
|  |                 <td>{{ item.type }}</td> | ||||||
|  |                 <td>{{ item.target }}</td> | ||||||
|  |                 <td>{{ item.ip_address }}</td> | ||||||
|  |                 <td> | ||||||
|  |                   <v-tooltip :text="item.user_agent" location="top"> | ||||||
|  |                     <template #activator="{ props }"> | ||||||
|  |                       <div v-bind="props" class="text-ellipsis whitespace-nowrap overflow-hidden max-w-[180px]"> | ||||||
|  |                         {{ item.user_agent }} | ||||||
|  |                       </div> | ||||||
|  |                     </template> | ||||||
|  |                   </v-tooltip> | ||||||
|  |                 </td> | ||||||
|  |                 <td>{{ new Date(item.created_at).toLocaleString() }}</td> | ||||||
|  |               </tr> | ||||||
|  |             </template> | ||||||
|  |           </v-data-table-server> | ||||||
|  |         </v-card> | ||||||
|  |       </template> | ||||||
|  |     </v-expansion-panel> | ||||||
|  |   </v-expansion-panels> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  |  | ||||||
|  | const dataDefinitions: { [id: string]: any[] } = { | ||||||
|  |   tickets: [ | ||||||
|  |     { align: "start", key: "id", title: "ID" }, | ||||||
|  |     { align: "start", key: "ip_address", title: "IP Address" }, | ||||||
|  |     { align: "start", key: "user_agent", title: "User Agent" }, | ||||||
|  |     { align: "start", key: "created_at", title: "Issued At" }, | ||||||
|  |     { align: "start", key: "actions", title: "Actions", sortable: false }, | ||||||
|  |   ], | ||||||
|  |   events: [ | ||||||
|  |     { align: "start", key: "id", title: "ID" }, | ||||||
|  |     { align: "start", key: "type", title: "Type" }, | ||||||
|  |     { align: "start", key: "target", title: "Affected Object" }, | ||||||
|  |     { align: "start", key: "ip_address", title: "IP Address" }, | ||||||
|  |     { align: "start", key: "user_agent", title: "User Agent" }, | ||||||
|  |     { align: "start", key: "created_at", title: "Performed At" }, | ||||||
|  |   ], | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const tickets = ref<any>([]) | ||||||
|  | const events = ref<any>([]) | ||||||
|  |  | ||||||
|  | const reverting = reactive({ tickets: false, sessions: false, events: false }) | ||||||
|  | const pagination = reactive({ | ||||||
|  |   tickets: { page: 1, pageSize: 5, total: 0 }, | ||||||
|  |   events: { page: 1, pageSize: 5, total: 0 }, | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | async function readTickets({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) { | ||||||
|  |   if (itemsPerPage) pagination.tickets.pageSize = itemsPerPage | ||||||
|  |   if (page) pagination.tickets.page = page | ||||||
|  |  | ||||||
|  |   reverting.sessions = true | ||||||
|  |   const res = await fetch( | ||||||
|  |     `${config.public.solarNetworkApi}/cgi/auth/users/me/tickets?` + | ||||||
|  |     new URLSearchParams({ | ||||||
|  |       take: pagination.tickets.pageSize.toString(), | ||||||
|  |       offset: ((pagination.tickets.page - 1) * pagination.tickets.pageSize).toString(), | ||||||
|  |     }), | ||||||
|  |     { | ||||||
|  |       headers: { Authorization: `Bearer ${getAtk()}` }, | ||||||
|  |     }, | ||||||
|  |   ) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     const data = await res.json() | ||||||
|  |     tickets.value = data["data"] | ||||||
|  |     pagination.tickets.total = data["count"] | ||||||
|  |   } | ||||||
|  |   reverting.sessions = false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function readEvents({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) { | ||||||
|  |   if (itemsPerPage) pagination.events.pageSize = itemsPerPage | ||||||
|  |   if (page) pagination.events.page = page | ||||||
|  |  | ||||||
|  |   reverting.events = true | ||||||
|  |   const res = await fetch( | ||||||
|  |     `${config.public.solarNetworkApi}/cgi/auth/users/me/events?` + | ||||||
|  |     new URLSearchParams({ | ||||||
|  |       take: pagination.events.pageSize.toString(), | ||||||
|  |       offset: ((pagination.events.page - 1) * pagination.events.pageSize).toString(), | ||||||
|  |     }), | ||||||
|  |     { | ||||||
|  |       headers: { Authorization: `Bearer ${getAtk()}` }, | ||||||
|  |     }, | ||||||
|  |   ) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     const data = await res.json() | ||||||
|  |     events.value = data["data"] | ||||||
|  |     pagination.events.total = data["count"] | ||||||
|  |   } | ||||||
|  |   reverting.events = false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | Promise.all([readTickets({}), readEvents({})]) | ||||||
|  |  | ||||||
|  | async function killTicket(item: any) { | ||||||
|  |   reverting.sessions = true | ||||||
|  |   const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/users/me/tickets/${item.id}`, { | ||||||
|  |     method: "DELETE", | ||||||
|  |     headers: { Authorization: `Bearer ${getAtk()}` }, | ||||||
|  |   }) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     await readTickets({}) | ||||||
|  |     error.value = null | ||||||
|  |   } | ||||||
|  |   reverting.sessions = false | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @@ -6,7 +6,7 @@ | |||||||
|       </v-sheet> |       </v-sheet> | ||||||
|     </v-carousel-item> |     </v-carousel-item> | ||||||
|   </v-carousel> |   </v-carousel> | ||||||
|   <div v-else class="w-full flex items-center justify-center"> |   <div v-else class="w-full h-full flex items-center justify-center"> | ||||||
|     <v-progress-circular indeterminate /> |     <v-progress-circular indeterminate /> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|   | |||||||
							
								
								
									
										66
									
								
								components/auth/Authenticate.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										66
									
								
								components/auth/Authenticate.vue
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | <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" | ||||||
|  |                     :disabled="props.loading" v-model="probe" /> | ||||||
|  |       <v-text-field label="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 }} | ||||||
|  |         </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="submit" | ||||||
|  |           variant="text" | ||||||
|  |           color="primary" | ||||||
|  |           class="justify-self-end" | ||||||
|  |           append-icon="mdi-arrow-right" | ||||||
|  |           :disabled="props.loading" | ||||||
|  |         > | ||||||
|  |           Next | ||||||
|  |         </v-btn> | ||||||
|  |       </div> | ||||||
|  |     </v-form> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { ref } from "vue" | ||||||
|  |  | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const probe = ref("") | ||||||
|  | const password = ref("") | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  |  | ||||||
|  | const props = defineProps<{ loading?: boolean }>() | ||||||
|  | const emits = defineEmits(["swap", "update:loading", "update:ticket"]) | ||||||
|  |  | ||||||
|  | async function submit() { | ||||||
|  |   if (!probe.value || !password.value) return | ||||||
|  |  | ||||||
|  |   emits("update:loading", true) | ||||||
|  |   const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/auth`, { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ username: probe.value, password: password.value }), | ||||||
|  |   }) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     const data = await res.json() | ||||||
|  |     emits("update:ticket", data["ticket"]) | ||||||
|  |     if (data.is_finished) emits("swap", "completed") | ||||||
|  |     else emits("swap", "mfa") | ||||||
|  |     error.value = null | ||||||
|  |   } | ||||||
|  |   emits("update:loading", false) | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										69
									
								
								components/auth/AuthenticateCompleted.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								components/auth/AuthenticateCompleted.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | <template> | ||||||
|  |   <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> | ||||||
|  |  | ||||||
|  |     <v-expand-transition> | ||||||
|  |       <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3"> | ||||||
|  |         Something went wrong... {{ error }} | ||||||
|  |       </v-alert> | ||||||
|  |     </v-expand-transition> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { onMounted, ref } from "vue" | ||||||
|  | import { useRoute, useRouter } from "vue-router" | ||||||
|  |  | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const route = useRoute() | ||||||
|  | const router = useRouter() | ||||||
|  | const userinfo = useUserinfo() | ||||||
|  |  | ||||||
|  | const props = defineProps<{ loading?: boolean; currentFactor?: any; ticket?: any }>() | ||||||
|  | const emits = defineEmits(["update:loading"]) | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  |  | ||||||
|  | async function load() { | ||||||
|  |   emits("update:loading", true) | ||||||
|  |   await getToken(props.ticket.grant_token) | ||||||
|  |   await userinfo.readProfiles() | ||||||
|  |   setTimeout(() => callback(), 1850) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | onMounted(() => load()) | ||||||
|  |  | ||||||
|  | async function getToken(tk: string) { | ||||||
|  |   const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/auth/token`, { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       code: tk, | ||||||
|  |       grant_type: "grant_token", | ||||||
|  |     }), | ||||||
|  |   }) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     const err = await res.text() | ||||||
|  |     error.value = err | ||||||
|  |     throw new Error(err) | ||||||
|  |   } else { | ||||||
|  |     const out = await res.json() | ||||||
|  |     setTokenSet(out["access_token"], out["refresh_token"]) | ||||||
|  |     error.value = null | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function callback() { | ||||||
|  |   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") | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
							
								
								
									
										14
									
								
								components/auth/CallbackHint.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								components/auth/CallbackHint.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <template> | ||||||
|  |   <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 /> | ||||||
|  |         <span class="font-mono">{{ route.query["redirect_uri"] }}</span> | ||||||
|  |       </v-alert> | ||||||
|  |     </v-expand-transition> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | const route = useRoute() | ||||||
|  | </script> | ||||||
							
								
								
									
										94
									
								
								components/auth/FactorApplicator.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										94
									
								
								components/auth/FactorApplicator.vue
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,94 @@ | |||||||
|  | <template> | ||||||
|  |   <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> | ||||||
|  |         <v-otp-input | ||||||
|  |           class="pt-0" | ||||||
|  |           variant="solo" | ||||||
|  |           density="compact" | ||||||
|  |           type="text" | ||||||
|  |           :length="6" | ||||||
|  |           v-model="password" | ||||||
|  |           :loading="loading" | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |       <v-text-field | ||||||
|  |         v-else | ||||||
|  |         label="Password" | ||||||
|  |         type="password" | ||||||
|  |         variant="solo" | ||||||
|  |         density="comfortable" | ||||||
|  |         :disabled="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 }} | ||||||
|  |         </v-alert> | ||||||
|  |       </v-expand-transition> | ||||||
|  |  | ||||||
|  |       <div class="flex justify-end"> | ||||||
|  |         <v-btn | ||||||
|  |           type="submit" | ||||||
|  |           variant="text" | ||||||
|  |           color="primary" | ||||||
|  |           class="justify-self-end" | ||||||
|  |           append-icon="mdi-arrow-right" | ||||||
|  |           :disabled="loading" | ||||||
|  |         > | ||||||
|  |           Next | ||||||
|  |         </v-btn> | ||||||
|  |       </div> | ||||||
|  |     </v-form> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { computed, ref } from "vue" | ||||||
|  |  | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const password = ref("") | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  |  | ||||||
|  | const props = defineProps<{ loading?: boolean; currentFactor?: any; ticket?: any }>() | ||||||
|  | const emits = defineEmits(["swap", "update:ticket", "update:loading"]) | ||||||
|  |  | ||||||
|  | async function submit() { | ||||||
|  |   emits("update:loading", true) | ||||||
|  |   const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/auth/mfa`, { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify({ | ||||||
|  |       ticket_id: props.ticket?.id, | ||||||
|  |       factor_id: props.currentFactor?.id, | ||||||
|  |       code: password.value, | ||||||
|  |     }), | ||||||
|  |   }) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     const data = await res.json() | ||||||
|  |     error.value = null | ||||||
|  |     password.value = "" | ||||||
|  |     emits("update:ticket", data["ticket"]) | ||||||
|  |     if (data["is_finished"]) emits("swap", "completed") | ||||||
|  |     else emits("swap", "mfa") | ||||||
|  |   } | ||||||
|  |   emits("update:loading", false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const inputType = computed(() => { | ||||||
|  |   switch (props.currentFactor?.type) { | ||||||
|  |     case 0: | ||||||
|  |       return "text" | ||||||
|  |     case 1: | ||||||
|  |       return "one-time-password" | ||||||
|  |     default: | ||||||
|  |       return "unknown" | ||||||
|  |   } | ||||||
|  | }) | ||||||
|  | </script> | ||||||
							
								
								
									
										89
									
								
								components/auth/FactorPicker.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										89
									
								
								components/auth/FactorPicker.vue
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="flex items-center"> | ||||||
|  |     <div class="flex-grow-1"> | ||||||
|  |       <v-card class="mb-3"> | ||||||
|  |         <v-list density="compact" color="primary"> | ||||||
|  |           <v-list-item | ||||||
|  |             v-for="(item, idx) in factors ?? []" | ||||||
|  |             :key="idx" | ||||||
|  |             :prepend-icon="getFactorType(item)?.icon" | ||||||
|  |             :title="getFactorType(item)?.label" | ||||||
|  |             :active="focus === item.id" | ||||||
|  |             :disabled="getFactorAvailable(item)" | ||||||
|  |             @click="focus = item.id" | ||||||
|  |           /> | ||||||
|  |         </v-list> | ||||||
|  |       </v-card> | ||||||
|  |  | ||||||
|  |       <v-expand-transition> | ||||||
|  |         <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3"> | ||||||
|  |           Something went wrong... {{ 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 | ||||||
|  |         </v-btn> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { onMounted, ref } from "vue" | ||||||
|  |  | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const focus = ref<number | null>(null) | ||||||
|  | const factors = ref<any[]>([]) | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  |  | ||||||
|  | const props = defineProps<{ ticket?: any }>() | ||||||
|  | const emits = defineEmits(["swap", "update:loading", "update:currentFactor"]) | ||||||
|  |  | ||||||
|  | async function load() { | ||||||
|  |   emits("update:loading", true) | ||||||
|  |   const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/auth/factors?ticketId=${props.ticket.id}`) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     factors.value = (await res.json()).filter((e: any) => e.type != 0) | ||||||
|  |   } | ||||||
|  |   emits("update:loading", false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | onMounted(() => load()) | ||||||
|  |  | ||||||
|  | async function submit() { | ||||||
|  |   if (!focus.value) return | ||||||
|  |  | ||||||
|  |   emits("update:loading", true) | ||||||
|  |   const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/auth/factors/${focus.value}`, { | ||||||
|  |     method: "POST", | ||||||
|  |   }) | ||||||
|  |   if (res.status !== 200 && res.status !== 204) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     const item = factors.value.find((item: any) => item.id === focus.value) | ||||||
|  |     emits("update:currentFactor", item) | ||||||
|  |     emits("swap", "applicator") | ||||||
|  |     error.value = null | ||||||
|  |     focus.value = null | ||||||
|  |   } | ||||||
|  |   emits("update:loading", false) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getFactorType(item: any) { | ||||||
|  |   switch (item.type) { | ||||||
|  |     case 1: | ||||||
|  |       return { icon: "mdi-email-fast", label: "Email Validation" } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getFactorAvailable(factor: any) { | ||||||
|  |   const blacklist: number[] = props.ticket?.blacklist_factors ?? [] | ||||||
|  |   return blacklist.includes(factor.id) | ||||||
|  | } | ||||||
|  | </script> | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| --- | --- | ||||||
| thumbnail: /thumbnails/products/acefield.webp | thumbnail: /thumbnails/products/acefield.webp | ||||||
| title: AceField | title: AceField | ||||||
| description: AceField is an experimental multiplayer top-down view shooting game that created by Solsynth LLC affiliation Highland Entertainment. | description: An experimental multiplayer top-down view shooting game that created by Solsynth LLC affiliation Highland Entertainment. | ||||||
| url: https://files.solsynth.dev/production01/acefield | url: https://files.solsynth.dev/production01/acefield | ||||||
| --- | --- | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,9 +13,7 @@ | |||||||
|  |  | ||||||
|       <v-spacer></v-spacer> |       <v-spacer></v-spacer> | ||||||
|  |  | ||||||
|       <v-btn href="https://id.solsynth.dev" target="_blank" icon slim> |       <user-menu /> | ||||||
|         <v-avatar size="small" color="transparent" icon="mdi-account-circle" /> |  | ||||||
|       </v-btn> |  | ||||||
|     </v-container> |     </v-container> | ||||||
|   </v-app-bar> |   </v-app-bar> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import vuetify, { transformAssetUrls } from "vite-plugin-vuetify" | import vuetify, { transformAssetUrls } from "vite-plugin-vuetify" | ||||||
|  |  | ||||||
| // https://nuxt.com/docs/api/configuration/nuxt-config |  | ||||||
| export default defineNuxtConfig({ | export default defineNuxtConfig({ | ||||||
|   devtools: { enabled: true }, |   devtools: { enabled: true }, | ||||||
|  |  | ||||||
| @@ -50,6 +49,10 @@ export default defineNuxtConfig({ | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   pinia: { | ||||||
|  |     storesDirs: ['./stores/**'], | ||||||
|  |   }, | ||||||
|  |  | ||||||
|   build: { |   build: { | ||||||
|     transpile: ["vuetify"], |     transpile: ["vuetify"], | ||||||
|   }, |   }, | ||||||
| @@ -59,6 +62,7 @@ export default defineNuxtConfig({ | |||||||
|     "@nuxt/content", |     "@nuxt/content", | ||||||
|     "@nuxt/image", |     "@nuxt/image", | ||||||
|     "@nuxtjs/sitemap", |     "@nuxtjs/sitemap", | ||||||
|  |     "@pinia/nuxt", | ||||||
|     (_options, nuxt) => { |     (_options, nuxt) => { | ||||||
|       nuxt.hooks.hook("vite:extendConfig", (config) => { |       nuxt.hooks.hook("vite:extendConfig", (config) => { | ||||||
|         // @ts-expect-error |         // @ts-expect-error | ||||||
|   | |||||||
| @@ -14,7 +14,10 @@ | |||||||
|     "@nuxt/content": "^2.13.2", |     "@nuxt/content": "^2.13.2", | ||||||
|     "@nuxt/image": "^1.7.0", |     "@nuxt/image": "^1.7.0", | ||||||
|     "@nuxtjs/sitemap": "^6.0.0-beta.1", |     "@nuxtjs/sitemap": "^6.0.0-beta.1", | ||||||
|  |     "@pinia/nuxt": "^0.5.3", | ||||||
|     "nuxt": "^3.12.4", |     "nuxt": "^3.12.4", | ||||||
|  |     "pinia": "^2.2.1", | ||||||
|  |     "universal-cookie": "^7.2.0", | ||||||
|     "vue": "latest" |     "vue": "latest" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								pages/auth/sign-in.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								pages/auth/sign-in.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container class="h-[calc(100vh-80px)] flex flex-col gap-3 items-center justify-center"> | ||||||
|  |     <auth-callback-hint /> | ||||||
|  |  | ||||||
|  |     <v-card class="w-full max-w-[720px] overflow-auto" :loading="loading"> | ||||||
|  |       <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> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <v-window :touch="false" :model-value="panel" class="pa-2 mx-[-0.5rem]"> | ||||||
|  |           <v-window-item v-for="(k, idx) in Object.keys(panels)" :key="idx" :value="k"> | ||||||
|  |             <component :is="panels[k]" @swap="(val: string) => (panel = val)" v-model:loading="loading" | ||||||
|  |                        v-model:currentFactor="currentFactor" v-model:ticket="ticket" /> | ||||||
|  |           </v-window-item> | ||||||
|  |         </v-window> | ||||||
|  |       </v-card-text> | ||||||
|  |     </v-card> | ||||||
|  |  | ||||||
|  |     <copyright service="passport" /> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <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 route = useRoute() | ||||||
|  |  | ||||||
|  | const loading = ref(false) | ||||||
|  |  | ||||||
|  | const currentFactor = ref<any>(null) | ||||||
|  | const ticket = ref<any>(null) | ||||||
|  |  | ||||||
|  | async function pickUpTicket() { | ||||||
|  |   if (route.query["ticketId"]) { | ||||||
|  |     loading.value = true | ||||||
|  |     const res = await fetch(`/api/auth/tickets/${route.query["ticketId"]}`) | ||||||
|  |     if (res.status == 200) { | ||||||
|  |       ticket.value = await res.json() | ||||||
|  |       if (ticket.value["available_at"] != null) panel.value = "completed" | ||||||
|  |       else panel.value = "mfa" | ||||||
|  |     } | ||||||
|  |     loading.value = false | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | onMounted(() => pickUpTicket()) | ||||||
|  |  | ||||||
|  | const panel = ref("authenticate") | ||||||
|  |  | ||||||
|  | const panels: { [id: string]: Component } = { | ||||||
|  |   authenticate: AccountAuthenticate, | ||||||
|  |   mfa: FactorPicker, | ||||||
|  |   applicator: FactorApplicator, | ||||||
|  |   completed: AuthenticateCompleted, | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .card-grid { | ||||||
|  |   display: grid; | ||||||
|  |   grid-template-columns: 1fr 1fr; | ||||||
|  |   gap: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .card-grid { | ||||||
|  |     grid-template-columns: 1fr; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .card-rounded { | ||||||
|  |   border-radius: 8px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
							
								
								
									
										161
									
								
								pages/auth/sign-up.vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										161
									
								
								pages/auth/sign-up.vue
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,161 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container class="h-[calc(100vh-80px)] flex flex-col gap-3 items-center justify-center"> | ||||||
|  |     <auth-callback-hint /> | ||||||
|  |  | ||||||
|  |     <v-card class="w-full max-w-[720px] overflow-auto" :loading="loading"> | ||||||
|  |       <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> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div class="flex items-center"> | ||||||
|  |           <v-form class="flex-grow-1" @submit.prevent="submit"> | ||||||
|  |             <v-row dense class="mb-3"> | ||||||
|  |               <v-col :cols="6"> | ||||||
|  |                 <v-text-field | ||||||
|  |                   hide-details | ||||||
|  |                   label="Name" | ||||||
|  |                   autocomplete="username" | ||||||
|  |                   variant="solo" | ||||||
|  |                   density="comfortable" | ||||||
|  |                   v-model="data.name" | ||||||
|  |                 /> | ||||||
|  |               </v-col> | ||||||
|  |               <v-col :cols="6"> | ||||||
|  |                 <v-text-field | ||||||
|  |                   hide-details | ||||||
|  |                   label="Nick" | ||||||
|  |                   autocomplete="nickname" | ||||||
|  |                   variant="solo" | ||||||
|  |                   density="comfortable" | ||||||
|  |                   v-model="data.nick" | ||||||
|  |                 /> | ||||||
|  |               </v-col> | ||||||
|  |               <v-col :cols="12"> | ||||||
|  |                 <v-text-field | ||||||
|  |                   hide-details | ||||||
|  |                   label="Email Address" | ||||||
|  |                   type="email" | ||||||
|  |                   variant="solo" | ||||||
|  |                   density="comfortable" | ||||||
|  |                   v-model="data.email" | ||||||
|  |                 /> | ||||||
|  |               </v-col> | ||||||
|  |               <v-col :cols="12"> | ||||||
|  |                 <v-text-field | ||||||
|  |                   hide-details | ||||||
|  |                   label="Password" | ||||||
|  |                   type="password" | ||||||
|  |                   autocomplete="new-password" | ||||||
|  |                   variant="solo" | ||||||
|  |                   density="comfortable" | ||||||
|  |                   v-model="data.password" | ||||||
|  |                 /> | ||||||
|  |               </v-col> | ||||||
|  |             </v-row> | ||||||
|  |  | ||||||
|  |             <v-expand-transition> | ||||||
|  |               <v-alert v-show="error" variant="tonal" type="error" class="text-xs mb-3"> | ||||||
|  |                 Something went wrong... {{ 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 | ||||||
|  |               </v-btn> | ||||||
|  |  | ||||||
|  |               <v-btn type="submit" variant="text" color="primary" append-icon="mdi-arrow-right" :disabled="loading"> | ||||||
|  |                 Next | ||||||
|  |               </v-btn> | ||||||
|  |             </div> | ||||||
|  |           </v-form> | ||||||
|  |         </div> | ||||||
|  |       </v-card-text> | ||||||
|  |     </v-card> | ||||||
|  |  | ||||||
|  |     <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! | ||||||
|  |         </template> | ||||||
|  |         <template #actions> | ||||||
|  |           <div class="flex flex-grow-1 justify-end"> | ||||||
|  |             <v-btn @click="callback">Let's go</v-btn> | ||||||
|  |           </div> | ||||||
|  |         </template> | ||||||
|  |       </v-card> | ||||||
|  |     </v-dialog> | ||||||
|  |  | ||||||
|  |     <copyright service="passport" /> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import { ref } from "vue" | ||||||
|  | import { useRoute, useRouter } from "vue-router" | ||||||
|  |  | ||||||
|  | const error = ref<string | null>(null) | ||||||
|  |  | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const route = useRoute() | ||||||
|  | const router = useRouter() | ||||||
|  |  | ||||||
|  | const done = ref(false) | ||||||
|  | const loading = ref(false) | ||||||
|  |  | ||||||
|  | const data = ref({ | ||||||
|  |   name: "", | ||||||
|  |   nick: "", | ||||||
|  |   email: "", | ||||||
|  |   password: "", | ||||||
|  | }) | ||||||
|  |  | ||||||
|  | async function submit() { | ||||||
|  |   const payload = data.value | ||||||
|  |   if (!payload.name || !payload.nick || !payload.email || !payload.password) return | ||||||
|  |  | ||||||
|  |   loading.value = true | ||||||
|  |   const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/users`, { | ||||||
|  |     method: "POST", | ||||||
|  |     headers: { "Content-Type": "application/json" }, | ||||||
|  |     body: JSON.stringify(payload), | ||||||
|  |   }) | ||||||
|  |   if (res.status !== 200) { | ||||||
|  |     error.value = await res.text() | ||||||
|  |   } else { | ||||||
|  |     done.value = true | ||||||
|  |     error.value = null | ||||||
|  |   } | ||||||
|  |   loading.value = false | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function callback() { | ||||||
|  |   if (route.params["closable"]) { | ||||||
|  |     window.close() | ||||||
|  |   } else { | ||||||
|  |     router.push({ name: "auth.sign-in" }) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style scoped> | ||||||
|  | .card-grid { | ||||||
|  |   display: grid; | ||||||
|  |   grid-template-columns: 1fr 1fr; | ||||||
|  |   gap: 1rem; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (max-width: 768px) { | ||||||
|  |   .card-grid { | ||||||
|  |     grid-template-columns: 1fr; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .card-rounded { | ||||||
|  |   border-radius: 8px; | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <template> | <template> | ||||||
|   <v-container class="flex flex-col gap-[4rem] my-[2rem]"> |   <v-container class="flex flex-col gap-[4rem] my-[2rem]"> | ||||||
|     <v-row class="content-section"> |     <v-row class="content-section"> | ||||||
|       <v-col cols="12" md="4" class="d-flex align-center"> |       <v-col cols="12" md="4" class="flex justify-start"> | ||||||
|         <div> |         <div> | ||||||
|           <h1 class="text-4xl font-bold">Solsynth</h1> |           <h1 class="text-4xl font-bold">Solsynth</h1> | ||||||
|           <p class="text-lg mt-3"> |           <p class="text-lg mt-3"> | ||||||
| @@ -21,11 +21,11 @@ | |||||||
|     </v-row> |     </v-row> | ||||||
|     <v-row class="content-section"> |     <v-row class="content-section"> | ||||||
|       <v-col cols="12" md="8"> |       <v-col cols="12" md="8"> | ||||||
|         <v-card> |         <v-card class="max-h-[500px]"> | ||||||
|           <activity-carousel class="carousel-section" /> |           <activity-carousel class="carousel-section" /> | ||||||
|         </v-card> |         </v-card> | ||||||
|       </v-col> |       </v-col> | ||||||
|       <v-col cols="12" md="4" class="d-flex align-center" order="first" order-md="last"> |       <v-col cols="12" md="4" class="flex justify-end" order="first" order-md="last"> | ||||||
|         <div class="text-right"> |         <div class="text-right"> | ||||||
|           <h1 class="text-4xl font-bold">Activities</h1> |           <h1 class="text-4xl font-bold">Activities</h1> | ||||||
|           <p class="text-lg mt-3"> |           <p class="text-lg mt-3"> | ||||||
| @@ -34,7 +34,7 @@ | |||||||
|           </p> |           </p> | ||||||
|           <p class="text-grey mt-2"> |           <p class="text-grey mt-2"> | ||||||
|             <v-icon icon="mdi-arrow-left" size="16" class="mb-0.5" /> |             <v-icon icon="mdi-arrow-left" size="16" class="mb-0.5" /> | ||||||
|             See some posts of our realm just here |             See some posts in our realm just here | ||||||
|           </p> |           </p> | ||||||
|         </div> |         </div> | ||||||
|       </v-col> |       </v-col> | ||||||
|   | |||||||
							
								
								
									
										52
									
								
								pages/users/me.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								pages/users/me.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | <template> | ||||||
|  |   <v-container class="content-container mx-auto"> | ||||||
|  |     <v-img v-if="urlOfBanner" :src="urlOfBanner" :aspect-ratio="16 / 5" class="rounded-md mb-3" cover /> | ||||||
|  |  | ||||||
|  |     <div class="mx-[2.5ch]"> | ||||||
|  |       <div class="my-5 flex flex-row gap-4"> | ||||||
|  |         <v-avatar :image="urlOfAvatar" /> | ||||||
|  |         <div class="flex flex-col"> | ||||||
|  |           <span>{{ auth.userinfo.data?.nick }} <span class="text-xs">@{{ auth.userinfo.data?.name }}</span></span> | ||||||
|  |           <span class="text-sm">{{ auth.userinfo.data?.description }}</span> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <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> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <v-alert | ||||||
|  |           class="mt-3" | ||||||
|  |           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." | ||||||
|  |         /> | ||||||
|  |       </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> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <account-auth-ticket-table class="mt-3" /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="mb-5 mx-[2.5ch]"> | ||||||
|  |         <copyright service="passport" :centered="false" /> | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |   </v-container> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script setup lang="ts"> | ||||||
|  | const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  | const auth = useUserinfo() | ||||||
|  |  | ||||||
|  | const urlOfAvatar = computed(() => auth.userinfo.data?.avatar ? `${config.public.solarNetworkApi}/cgi/files/attachments/${auth.userinfo.data.avatar}` : void 0) | ||||||
|  | const urlOfBanner = computed(() => auth.userinfo.data?.banner ? `${config.public.solarNetworkApi}/cgi/files/attachments/${auth.userinfo.data.banner}` : void 0) | ||||||
|  | </script> | ||||||
							
								
								
									
										60
									
								
								stores/userinfo.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								stores/userinfo.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | |||||||
|  | import Cookie from "universal-cookie" | ||||||
|  | import { defineStore } from "pinia" | ||||||
|  | import { ref } from "vue" | ||||||
|  |  | ||||||
|  | export interface Userinfo { | ||||||
|  |   isLoggedIn: boolean | ||||||
|  |   displayName: string | ||||||
|  |   data: any | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const defaultUserinfo: Userinfo = { | ||||||
|  |   isLoggedIn: false, | ||||||
|  |   displayName: "Citizen", | ||||||
|  |   data: null, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function getAtk(): string { | ||||||
|  |   return new Cookie().get("__hydrogen_atk") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function checkLoggedIn(): boolean { | ||||||
|  |   return new Cookie().get("__hydrogen_rtk") | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export function setTokenSet(atk: string, rtk: string) { | ||||||
|  |   new Cookie().set("__hydrogen_atk", atk, { path: "/" }) | ||||||
|  |   new Cookie().set("__hydrogen_rtk", rtk, { path: "/" }) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const useUserinfo = defineStore("userinfo", () => { | ||||||
|  |   const userinfo = ref(defaultUserinfo) | ||||||
|  |   const isReady = ref(false) | ||||||
|  |  | ||||||
|  |   async function readProfiles() { | ||||||
|  |     if (!checkLoggedIn()) { | ||||||
|  |       isReady.value = true | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const config = useRuntimeConfig() | ||||||
|  |  | ||||||
|  |     const res = await fetch(`${config.public.solarNetworkApi}/cgi/auth/users/me`, { | ||||||
|  |       headers: { Authorization: `Bearer ${getAtk()}` }, | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |     if (res.status !== 200) { | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const data = await res.json() | ||||||
|  |  | ||||||
|  |     isReady.value = true | ||||||
|  |     userinfo.value = { | ||||||
|  |       isLoggedIn: true, | ||||||
|  |       displayName: data["nick"], | ||||||
|  |       data: data, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return { userinfo, isReady, readProfiles } | ||||||
|  | }) | ||||||
		Reference in New Issue
	
	Block a user