✨ Proper user login
This commit is contained in:
@@ -5,11 +5,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import "~/assets/css/tailwind.css"
|
||||
|
||||
import "@mdi/font/css/materialdesignicons.css"
|
||||
|
||||
import { useUserStore } from "~/stores/user"
|
||||
import "~/assets/css/tailwind.css"
|
||||
|
||||
onMounted(() => {
|
||||
const userStore = useUserStore()
|
||||
|
@@ -1,17 +1,122 @@
|
||||
<template>
|
||||
<img v-if="itemType == 'image'" :src="remoteSource" class="rounded-md">
|
||||
<div v-if="itemType == 'image'" class="relative rounded-md overflow-hidden" :style="containerStyle">
|
||||
<!-- Blurhash placeholder -->
|
||||
<div
|
||||
v-if="blurhash"
|
||||
class="absolute inset-0"
|
||||
:style="blurhashContainerStyle"
|
||||
>
|
||||
<canvas
|
||||
ref="blurCanvas"
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
width="32"
|
||||
height="32"
|
||||
/>
|
||||
</div>
|
||||
<!-- Main image -->
|
||||
<img
|
||||
:src="remoteSource"
|
||||
class="w-full h-auto rounded-md"
|
||||
:class="{ 'opacity-0': !imageLoaded && blurhash }"
|
||||
@load="imageLoaded = true"
|
||||
@error="imageLoaded = true"
|
||||
/>
|
||||
</div>
|
||||
<audio v-else-if="itemType == 'audio'" :src="remoteSource" controls />
|
||||
<video v-else-if="itemType == 'video'" :src="remoteSource" controls />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import type { SnAttachment } from '~/types/api'
|
||||
import { computed, ref, onMounted, watch } from "vue"
|
||||
import { decode } from "blurhash"
|
||||
import type { SnAttachment } from "~/types/api"
|
||||
|
||||
const props = defineProps<{ item: SnAttachment }>()
|
||||
|
||||
const itemType = computed(() => props.item.mimeType.split('/')[0] ?? 'unknown')
|
||||
const itemType = computed(() => props.item.mimeType.split("/")[0] ?? "unknown")
|
||||
const blurhash = computed(() => props.item.fileMeta?.blur)
|
||||
const imageWidth = computed(() => props.item.fileMeta?.width)
|
||||
const imageHeight = computed(() => props.item.fileMeta?.height)
|
||||
const aspectRatio = computed(() => props.item.fileMeta?.ratio ?? (imageWidth.value && imageHeight.value ? imageHeight.value / imageWidth.value : 1))
|
||||
const imageLoaded = ref(false)
|
||||
|
||||
const apiBase = useSolarNetworkUrl();
|
||||
const remoteSource = computed(() => `${apiBase}/drive/files/${props.item.id}?original=true`)
|
||||
const blurCanvas = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
const remoteSource = computed(
|
||||
() => `${apiBase}/drive/files/${props.item.id}?original=true`
|
||||
)
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
if (imageWidth.value && imageHeight.value) {
|
||||
const maxWidth = 640 // Cap maximum width
|
||||
const maxHeight = 800 // Cap maximum height
|
||||
|
||||
let width = imageWidth.value
|
||||
let height = imageHeight.value
|
||||
|
||||
// Scale down if width exceeds max
|
||||
if (width > maxWidth) {
|
||||
const ratio = maxWidth / width
|
||||
width = maxWidth
|
||||
height = height * ratio
|
||||
}
|
||||
|
||||
// Scale down if height exceeds max
|
||||
if (height > maxHeight) {
|
||||
const ratio = maxHeight / height
|
||||
height = maxHeight
|
||||
width = width * ratio
|
||||
}
|
||||
|
||||
return {
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
'max-width': '100%',
|
||||
'max-height': '100%'
|
||||
}
|
||||
}
|
||||
return {
|
||||
'max-width': '800px',
|
||||
'max-height': '600px'
|
||||
}
|
||||
})
|
||||
|
||||
const blurhashContainerStyle = computed(() => {
|
||||
return {
|
||||
'padding-bottom': `${aspectRatio.value * 100}%`
|
||||
}
|
||||
})
|
||||
|
||||
const decodeBlurhash = () => {
|
||||
if (!blurhash.value || !blurCanvas.value) return
|
||||
|
||||
try {
|
||||
const pixels = decode(blurhash.value, 32, 32)
|
||||
const imageData = new ImageData(new Uint8ClampedArray(pixels), 32, 32)
|
||||
const context = blurCanvas.value.getContext("2d")
|
||||
if (context) {
|
||||
context.putImageData(imageData, 0, 0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to decode blurhash:", error)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
decodeBlurhash()
|
||||
|
||||
// Fallback timeout to show image if load event doesn't fire
|
||||
if (blurhash.value) {
|
||||
setTimeout(() => {
|
||||
if (!imageLoaded.value) {
|
||||
imageLoaded.value = true
|
||||
}
|
||||
}, 3000) // 3 second timeout
|
||||
}
|
||||
})
|
||||
|
||||
watch(blurhash, () => {
|
||||
decodeBlurhash()
|
||||
})
|
||||
</script>
|
||||
|
@@ -67,7 +67,7 @@ const submitting = ref(false)
|
||||
async function submit() {
|
||||
submitting.value = true
|
||||
const api = useSolarNetwork()
|
||||
await api(`/posts?pub=${publisher.value}`, {
|
||||
await api(`/sphere/posts?pub=${publisher.value}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
|
@@ -10,21 +10,18 @@
|
||||
<v-list-item v-bind="itemProps">
|
||||
<template #prepend>
|
||||
<v-avatar
|
||||
:image="item.raw.picture ? `${apiBase}/api/drive/files/${item.raw.picture.id}` : undefined"
|
||||
:image="item.raw.picture ? `${apiBase}/drive/files/${item.raw.picture.id}` : undefined"
|
||||
size="small"
|
||||
rounded
|
||||
/>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.raw?.nick }}</v-list-item-title>
|
||||
<v-list-item-subtitle>@{{ item.raw?.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template #selection="{ item }">
|
||||
<div class="d-flex align-center">
|
||||
<v-avatar
|
||||
:image="item.raw.picture ? `${apiBase}/api/drive/files/${item.raw.picture.id}` : undefined"
|
||||
:image="item.raw.picture ? `${apiBase}/drive/files/${item.raw.picture.id}` : undefined"
|
||||
size="24"
|
||||
rounded
|
||||
class="me-2"
|
||||
/>
|
||||
{{ item.raw?.nick }}
|
||||
|
@@ -3,7 +3,7 @@ import { useDark, useToggle } from "@vueuse/core"
|
||||
// composables/useCustomTheme.ts
|
||||
export function useCustomTheme(): {
|
||||
isDark: WritableComputedRef<boolean, boolean>
|
||||
toggle: (value?: boolean | undefined) => boolean
|
||||
toggle: (value?: boolean | undefined) => boolean,
|
||||
} {
|
||||
const { $vuetify } = useNuxtApp()
|
||||
|
||||
|
@@ -1,28 +1,45 @@
|
||||
// Solar Network aka the api client
|
||||
import { keysToCamel, keysToSnake } from '~/utils/transformKeys'
|
||||
import { keysToCamel, keysToSnake } from "~/utils/transformKeys"
|
||||
|
||||
export const useSolarNetwork = () => {
|
||||
const apiBase = useSolarNetworkUrl();
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
|
||||
return $fetch.create({
|
||||
baseURL: apiBase,
|
||||
credentials: 'include',
|
||||
credentials: "include",
|
||||
// Add Authorization header with Bearer token
|
||||
onRequest: ({ options }) => {
|
||||
// Get token from user store
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.token
|
||||
|
||||
if (token) {
|
||||
if (!options.headers) {
|
||||
options.headers = new Headers()
|
||||
}
|
||||
if (options.headers instanceof Headers) {
|
||||
options.headers.set("Authorization", `Bearer ${token}`)
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
;(options.headers as any)["Authorization"] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
// Transform request data from camelCase to snake_case
|
||||
if (options.body && typeof options.body === "object") {
|
||||
options.body = keysToSnake(options.body)
|
||||
}
|
||||
},
|
||||
// Transform response keys from snake_case to camelCase
|
||||
onResponse: ({ response }) => {
|
||||
if (response._data) {
|
||||
response._data = keysToCamel(response._data)
|
||||
}
|
||||
},
|
||||
// Transform request data from camelCase to snake_case
|
||||
onRequest: ({ options }) => {
|
||||
if (options.body && typeof options.body === 'object') {
|
||||
options.body = keysToSnake(options.body)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useSolarNetworkUrl = () => {
|
||||
const config = useRuntimeConfig()
|
||||
return config.public.apiBase
|
||||
return config.public.development ? "/api" : config.public.apiBase
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<v-app :theme="isDark ? 'dark' : 'light'">
|
||||
<v-app-bar flat>
|
||||
<v-app-bar flat class="app-bar-blur">
|
||||
<v-container class="mx-auto d-flex align-center justify-center">
|
||||
<v-btn
|
||||
v-for="link in links"
|
||||
@@ -13,9 +13,13 @@
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-responsive max-width="160">
|
||||
<v-avatar class="me-4" color="grey-darken-1" size="32" />
|
||||
</v-responsive>
|
||||
<v-avatar
|
||||
class="me-4"
|
||||
color="grey-darken-1"
|
||||
size="32"
|
||||
icon="mdi-account"
|
||||
:image="`${apiBase}/drive/files/${user?.profile.picture?.id}`"
|
||||
/>
|
||||
</v-container>
|
||||
</v-app-bar>
|
||||
|
||||
@@ -29,6 +33,9 @@
|
||||
import { useCustomTheme } from "~/composables/useCustomTheme"
|
||||
import type { NavLink } from "~/types/navlink"
|
||||
|
||||
const apiBase = useSolarNetworkUrl()
|
||||
|
||||
const { user } = useUserStore()
|
||||
const { isDark } = useCustomTheme()
|
||||
|
||||
const links: NavLink[] = [
|
||||
@@ -39,3 +46,22 @@ const links: NavLink[] = [
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-bar-blur {
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 40%,
|
||||
rgba(0, 0, 0, 0.5) 65%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 1) 40%,
|
||||
rgba(0, 0, 0, 0.5) 65%,
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100%;
|
||||
}
|
||||
</style>
|
||||
|
@@ -75,7 +75,7 @@ async function getFactors() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const availableFactors = await api(
|
||||
const availableFactors = await api<SnAuthFactor[]>(
|
||||
`/id/auth/challenge/${challenge.value.id}/factors`
|
||||
)
|
||||
factors.value = availableFactors.filter(
|
||||
@@ -94,7 +94,7 @@ async function getFactors() {
|
||||
}
|
||||
}
|
||||
|
||||
async function requestVerificationCode(hint: string | null) {
|
||||
async function requestVerificationCode() {
|
||||
if (!selectedFactorId.value || !challenge.value) return
|
||||
|
||||
const isResend = stage.value === "enter-code"
|
||||
@@ -104,10 +104,7 @@ async function requestVerificationCode(hint: string | null) {
|
||||
try {
|
||||
await api(
|
||||
`/id/auth/challenge/${challenge.value.id}/factors/${selectedFactorId.value}`,
|
||||
{
|
||||
method: "POST",
|
||||
body: hint
|
||||
}
|
||||
{ method: "POST" }
|
||||
)
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : "An error occurred"
|
||||
@@ -134,7 +131,7 @@ async function handleFactorSelected() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await requestVerificationCode(selectedFactor.value.contact ?? null)
|
||||
await requestVerificationCode()
|
||||
stage.value = "enter-code"
|
||||
} catch {
|
||||
// Error is already set by requestVerificationCode
|
||||
@@ -163,7 +160,7 @@ async function handleVerifyFactor() {
|
||||
|
||||
password.value = ""
|
||||
|
||||
if (challenge.value.stepRemain === 0) {
|
||||
if (challenge.value!.stepRemain === 0) {
|
||||
stage.value = "token-exchange"
|
||||
await exchangeToken()
|
||||
} else {
|
||||
@@ -184,14 +181,19 @@ async function exchangeToken() {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
await api("/id/auth/token", {
|
||||
const tokenResponse = await api<{ token: string }>("/id/auth/token", {
|
||||
method: "POST",
|
||||
body: {
|
||||
grant_type: "authorization_code",
|
||||
code: challenge.value.id
|
||||
code: challenge.value!.id
|
||||
}
|
||||
})
|
||||
|
||||
// Store the token in localStorage via user store
|
||||
if (tokenResponse && tokenResponse.token) {
|
||||
userStore.setToken(tokenResponse.token)
|
||||
}
|
||||
|
||||
await userStore.fetchUser()
|
||||
|
||||
const redirectUri = route.query.redirect_uri as string
|
||||
@@ -233,7 +235,12 @@ function getFactorName(factorType: number) {
|
||||
<v-progress-circular indeterminate size="64" />
|
||||
</v-overlay>
|
||||
<div class="mb-4">
|
||||
<img :src="IconLight" alt="CloudyLamb" height="60" width="60">
|
||||
<img
|
||||
:src="$vuetify.theme.current.dark ? IconDark : IconLight"
|
||||
alt="CloudyLamb"
|
||||
height="60"
|
||||
width="60"
|
||||
/>
|
||||
</div>
|
||||
<v-row>
|
||||
<v-col cols="12" lg="6" class="d-flex align-start justify-start">
|
||||
@@ -244,41 +251,37 @@ function getFactorName(factorType: number) {
|
||||
</div>
|
||||
<div v-if="stage === 'select-factor'">
|
||||
<h2 class="text-2xl font-bold mb-1">Choose how to sign in</h2>
|
||||
<p class="text-lg">
|
||||
Select your preferred authentication method
|
||||
</p>
|
||||
<p class="text-lg">Select your preferred authentication method</p>
|
||||
</div>
|
||||
<div v-if="stage === 'enter-code' && selectedFactor">
|
||||
<h2 class="text-2xl font-bold mb-1">
|
||||
Enter your
|
||||
{{
|
||||
selectedFactor.type === 0 ? "password" : "verification code"
|
||||
}}
|
||||
Enter your
|
||||
{{
|
||||
selectedFactor.type === 0 ? "password" : "verification code"
|
||||
}}
|
||||
</h2>
|
||||
<p v-if="selectedFactor.type === 1" class="text-lg">
|
||||
A code has been sent to
|
||||
{{ selectedFactor.contact || "your email" }}.
|
||||
A code has been sent to
|
||||
{{ selectedFactor.contact || "your email" }}.
|
||||
</p>
|
||||
<p v-if="selectedFactor.type === 2" class="text-lg">
|
||||
Enter the code from your in-app authenticator.
|
||||
Enter the code from your in-app authenticator.
|
||||
</p>
|
||||
<p v-if="selectedFactor.type === 3" class="text-lg">
|
||||
Enter the timed verification code.
|
||||
Enter the timed verification code.
|
||||
</p>
|
||||
<p v-if="selectedFactor.type === 4" class="text-lg">
|
||||
Enter your PIN code.
|
||||
Enter your PIN code.
|
||||
</p>
|
||||
<p v-if="selectedFactor.type === 0" class="text-lg">
|
||||
Enter your password to continue.
|
||||
Enter your password to continue.
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="stage === 'token-exchange'">
|
||||
<h2 class="text-2xl font-bold mb-1">Finalizing Login</h2>
|
||||
<p class="text-lg">
|
||||
Please wait while we complete your sign in.
|
||||
</p>
|
||||
</div>
|
||||
<p class="text-lg">Please wait while we complete your sign in.</p>
|
||||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="12" lg="6" class="d-flex align-center justify-stretch">
|
||||
<div class="w-full d-flex flex-column md:text-right">
|
||||
@@ -328,16 +331,24 @@ function getFactorName(factorType: number) {
|
||||
<v-list>
|
||||
<v-list-item v-for="factor in factors" :key="factor.id">
|
||||
<v-list-item-action>
|
||||
<v-radio :value="factor.id" :label="getFactorName(factor.type)"></v-radio>
|
||||
<v-radio
|
||||
:value="factor.id"
|
||||
:label="getFactorName(factor.type)"
|
||||
/>
|
||||
</v-list-item-action>
|
||||
<template #append>
|
||||
<v-icon>{{
|
||||
factor.type === 0 ? "mdi-lock" :
|
||||
factor.type === 1 ? "mdi-email" :
|
||||
factor.type === 2 ? "mdi-cellphone" :
|
||||
factor.type === 3 ? "mdi-clock" :
|
||||
factor.type === 4 ? "mdi-numeric" :
|
||||
"mdi-shield-key"
|
||||
factor.type === 0
|
||||
? "mdi-lock"
|
||||
: factor.type === 1
|
||||
? "mdi-email"
|
||||
: factor.type === 2
|
||||
? "mdi-cellphone"
|
||||
: factor.type === 3
|
||||
? "mdi-clock"
|
||||
: factor.type === 4
|
||||
? "mdi-numeric"
|
||||
: "mdi-shield-key"
|
||||
}}</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
@@ -371,9 +382,7 @@ function getFactorName(factorType: number) {
|
||||
variant="text"
|
||||
class="text-capitalize pl-0"
|
||||
color="primary"
|
||||
@click="
|
||||
requestVerificationCode(selectedFactor.contact ?? null)
|
||||
"
|
||||
@click="requestVerificationCode"
|
||||
>
|
||||
Resend Code
|
||||
</v-btn>
|
||||
|
@@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<v-card v-if="!userStore.user" class="w-full" title="About">
|
||||
<v-card v-if="!userStore.isAuthenticated" class="w-full" title="About">
|
||||
<v-card-text>
|
||||
<p>Welcome to the <b>Solar Network</b></p>
|
||||
<p>The open social network. Friendly to everyone.</p>
|
||||
@@ -26,7 +26,7 @@
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card v-else class="mt-4 w-full">
|
||||
<v-card v-else class="w-full">
|
||||
<v-card-text>
|
||||
<post-editor @posted="refreshActivities" />
|
||||
</v-card-text>
|
||||
@@ -108,11 +108,12 @@ async function refreshActivities() {
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.main {
|
||||
order: 2;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
@@ -1,16 +1,26 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { useSolarNetwork } from '~/composables/useSolarNetwork'
|
||||
import type { SnPublisher } from '~/types/api'
|
||||
import { defineStore } from "pinia"
|
||||
import { ref } from "vue"
|
||||
import { useSolarNetwork } from "~/composables/useSolarNetwork"
|
||||
import type { SnPublisher } from "~/types/api"
|
||||
|
||||
export const usePubStore = defineStore('pub', () => {
|
||||
export const usePubStore = defineStore("pub", () => {
|
||||
const publishers = ref<SnPublisher[]>([])
|
||||
|
||||
async function fetchPublishers() {
|
||||
const api = useSolarNetwork()
|
||||
const resp = await api('/publishers')
|
||||
const resp = await api("/sphere/publishers")
|
||||
publishers.value = resp as SnPublisher[]
|
||||
}
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
watch(
|
||||
userStore,
|
||||
(value) => {
|
||||
if (value.isAuthenticated) fetchPublishers()
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
|
||||
return { publishers, fetchPublishers }
|
||||
})
|
||||
|
@@ -1,46 +1,65 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useSolarNetwork } from '~/composables/useSolarNetwork'
|
||||
import type { SnAccount } from '~/types/api'
|
||||
import { defineStore } from "pinia"
|
||||
import { ref, computed } from "vue"
|
||||
import { useSolarNetwork } from "~/composables/useSolarNetwork"
|
||||
import type { SnAccount } from "~/types/api"
|
||||
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
export const useUserStore = defineStore("user", () => {
|
||||
// State
|
||||
const user = ref<SnAccount | null>(null)
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// The name is match with the remote one (set by server Set-Cookie)
|
||||
const token = useCookie<string | null>("AuthToken", {
|
||||
default: () => null,
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24 * 365 * 10
|
||||
}) // 10 years
|
||||
|
||||
// Getters
|
||||
const isAuthenticated = computed(() => !!user.value)
|
||||
const isAuthenticated = computed(() => !!user.value && !!token.value)
|
||||
|
||||
// Call fetchUser immediately
|
||||
fetchUser()
|
||||
|
||||
// Actions
|
||||
async function fetchUser(reload = true) {
|
||||
if (!reload && user.value) return // Skip fetching if already loaded and not forced to
|
||||
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
const api = useSolarNetwork()
|
||||
try {
|
||||
const response = await api('/id/accounts/me')
|
||||
const response = await api("/id/accounts/me")
|
||||
|
||||
user.value = response as SnAccount
|
||||
console.log(`Logged in as ${user.value.name}`)
|
||||
} catch (e: unknown) {
|
||||
error.value = e instanceof Error ? e.message : 'An error occurred'
|
||||
error.value = e instanceof Error ? e.message : "An error occurred"
|
||||
user.value = null // Clear user data on error
|
||||
console.error('Failed to fetch user... ', e)
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function setToken(newToken: string) {
|
||||
token.value = newToken
|
||||
}
|
||||
|
||||
function logout() {
|
||||
user.value = null
|
||||
localStorage.removeItem('authToken')
|
||||
token.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
isLoading,
|
||||
error,
|
||||
isAuthenticated,
|
||||
fetchUser,
|
||||
setToken,
|
||||
logout,
|
||||
}
|
||||
})
|
||||
|
3
bun.lock
3
bun.lock
@@ -14,6 +14,7 @@
|
||||
"@nuxtjs/tailwindcss": "6.14.0",
|
||||
"@pinia/nuxt": "0.11.2",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"cfturnstile-vue3": "^2.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"luxon": "^3.7.2",
|
||||
@@ -707,6 +708,8 @@
|
||||
|
||||
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
|
||||
"blurhash": ["blurhash@2.0.5", "", {}, "sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w=="],
|
||||
|
||||
"boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
@@ -6,7 +6,8 @@ export default withNuxt(
|
||||
{
|
||||
rules: {
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-v-html': 'off'
|
||||
'vue/no-v-html': 'off',
|
||||
'vue/html-self-closing': 'off'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@@ -16,12 +16,21 @@ export default defineNuxtConfig({
|
||||
features: {
|
||||
inlineStyles: false
|
||||
},
|
||||
image: {
|
||||
domains: ["api.solian.app"]
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
development: process.env.NODE_ENV == "development",
|
||||
apiBase: process.env.NUXT_PUBLIC_API_BASE || "https://api.solian.app"
|
||||
}
|
||||
},
|
||||
image: {
|
||||
domains: ["api.solian.app"]
|
||||
nitro: {
|
||||
devProxy: {
|
||||
"/api": {
|
||||
target: process.env.NUXT_PUBLIC_API_BASE || "https://api.solian.app",
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@@ -20,6 +20,7 @@
|
||||
"@nuxtjs/tailwindcss": "6.14.0",
|
||||
"@pinia/nuxt": "0.11.2",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"blurhash": "^2.0.5",
|
||||
"cfturnstile-vue3": "^2.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"luxon": "^3.7.2",
|
||||
|
@@ -1,12 +1,44 @@
|
||||
import { defineVuetifyConfiguration } from "vuetify-nuxt-module/custom-configuration";
|
||||
import { md3 } from "vuetify/blueprints";
|
||||
import { defineVuetifyConfiguration } from "vuetify-nuxt-module/custom-configuration"
|
||||
import { md3 } from "vuetify/blueprints"
|
||||
|
||||
export default defineVuetifyConfiguration({
|
||||
blueprint: md3,
|
||||
icons: {
|
||||
defaultSet: 'mdi'
|
||||
defaultSet: "mdi"
|
||||
},
|
||||
theme: {
|
||||
defaultTheme: "system",
|
||||
themes: {
|
||||
light: {
|
||||
colors: {
|
||||
background: "#f0f4fa",
|
||||
surface: "#ffffff",
|
||||
primary: "#3f51b5",
|
||||
secondary: "#2196f3",
|
||||
accent: "#009688",
|
||||
error: "#f44336",
|
||||
warning: "#ffc107",
|
||||
info: "#03a9f4",
|
||||
success: "#4caf50"
|
||||
}
|
||||
},
|
||||
dark: {
|
||||
dark: true,
|
||||
colors: {
|
||||
background: "#1e1f20",
|
||||
surface: "#0e0e0e",
|
||||
primary: "#3f51b5",
|
||||
secondary: "#2196f3",
|
||||
accent: "#009688",
|
||||
error: "#f44336",
|
||||
warning: "#ffc107",
|
||||
info: "#03a9f4",
|
||||
success: "#4caf50"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
date: {
|
||||
adapter: 'luxon'
|
||||
},
|
||||
});
|
||||
adapter: "luxon"
|
||||
}
|
||||
})
|
||||
|
Reference in New Issue
Block a user