The identity cookie stores only token / user_id / has_login / id. Other profile fields use encrypted user-profile localStorage. Client-side info merges both parts; SSR exposes identity fields only. setInfo replaces the full profile, splitting every login and automatic re-login before writing the cookie. setToken preserves profile fields. Both expireLogin and logout clear the profile. Legacy full cookies migrate on the client. If nginx already rejects the request, clear the affected site identity cookie first: page scripts cannot bypass an entry-point 400.

userStore

The framework's built-in user-identity Pinia store. Composable name is useUserStore (NOT useUserInfoStore; source: src/runtime/store/user.ts).

Login credentials are persisted as an atomic tokenInfo object in cookies: refresh / SSR auto-restore, "token present means user_id present" — never split.

Basic usage

<script setup lang="ts">
const userStore = useUserStore()

// All state is computed, use directly in templates
console.log(userStore.isLoggedIn, userStore.info?.nickname)

// Actions
async function onLogin() {
  await userStore.loginByUsername(form.username, form.password)
  await navigateTo(route.query.redirect as string || '/')
}

async function onLogout() {
  await userStore.logout() // revokes the server-side session first, then clears local state; local cleanup is still synchronous without await
  navigateTo('/login')
}
</script>

The framework never calls userStore.autoLogin() during SSR. Logged-in users' cookies pass through transparently via useCookie, and the request util auto-injects authToken; device login for new visitors is established by auth.client.ts after client hydrate. See Rendering Quick Guide.

State (computed, read-only)

Identity state derives from tokenInfoCookie; client-side info also merges encrypted localStorage profile fields.

FieldTypeDescription
infoRef<UserInfo | null>Full login response (user_id / nickname / vip / has_login / …). null = not logged in
tokenRef<string>Pure token string (no user_id). Empty '' when not logged in
authTokenRef<string>Request credential: logged-in = ${token}_${user_id}; device-login = token; not-logged-in = ''. framework request util uses this automatically
isLoggedInRef<boolean>True only for real user login (has_login === 'y'). Device login does NOT count

Device-login users (has_login = 'n') can hit anonymous endpoints and do tracking, but isLoggedIn stays false.

Actions

MethodSignatureDescription
getDeviceId()() => stringGet the device ID. On first call generates one via framework randomUuid() + writes a 10-year cookie; falls back automatically in non-secure contexts (LAN HTTP), not relying on bare crypto.randomUUID
getSessionId()() => stringGet the tracking session ID (framework#415). Reused if the last call was under 30 minutes ago, otherwise a new ID is generated; every call refreshes the activity time. Stored in localStorage (shared across tabs); used by the self-hosted tracker as context.session_id
setInfo(data)(data: UserInfo) => voidReplace user information: identity fields go to cookies, other fields to encrypted localStorage. Throws when a token is already set and the new data has none; to add or change profile fields, merge first: setInfo({ ...userStore.info, ...data })
setToken(value)(value: string) => voidOnly update tokenInfo.token, other fields preserved
setAccount(data)(data: AccountInfo) => voidSave login credentials. data.remember === false clears accountLocal
getAccountInfo()() => AccountInfo | nullRead saved credentials
clearAccount()() => voidClear AccountInfo only (leaves tokenInfo untouched)
autoLogin()() => Promise<boolean>Auto-login orchestrator: routing decided by saved AccountInfo.remember; no credentials → falls back to loginByDevice()
loginByDevice()() => Promise<Record>Device login: anonymous identity for credential-less users (identified by deviceId, no AccountInfo written); used by autoLogin fallback or called explicitly
loginByUsername(username, password, options?)(string, string, LoginOptions?) => Promise<Record>Username/password login. options.remember (default true) decides whether AccountInfo persists. options.extra is passed through to the backend
loginByQrcode(code, options?)(string, LoginOptions?) => Promise<Record>QR-code login. options.remember / options.extra same as above
register(loginName, password, options?)(string, string, LoginOptions?) => Promise<Record>Self-serve sign-up (POST /login/register; backend returns the same response shape as /login/username). On success the user is logged in immediately (reuses the setInfo/setAccount chain); options.remember / options.extra same as above. Failures (duplicate name, etc.) throw an Error and leave auth state untouched
expireLogin()() => voidMinimal cleanup on login exhaustion (fw#302): called by the request plugin when the silent 2002 re-login retry fails again — clears tokenInfo and profile data (isLoggedIn becomes false); keeps deviceId / accountLocal / channel codes / IDB. Business code rarely calls this directly — listen to the swift-web:auth-expired hook instead
logout()() => Promise<void>Revokes the server-side session first (POST /login/logout; failures are swallowed and never block local cleanup), then clears tokenInfo / channel_code / share_code cookies + accountLocal / user-profile (localStorage) + IDB cache. Keeps deviceId. Local cleanup happens synchronously, so it takes effect even without await

UserInfo type

interface UserInfo {
  user_id?: number | string
  username?: string
  nickname?: string
  headico?: string        // avatar URL
  token?: string
  has_login?: 'y' | 'n'   // 'y' real user login; 'n' device login
  vip?: number            // VIP tier, 0 = regular
  vip_expire?: string
  balance?: number
  websocket?: string      // WebSocket connection URL
  [key: string]: unknown  // Backend may extend with arbitrary fields
}

Persistent storage

Allocated by "SSR readability" + "travels with every request" + "sensitivity":

KeyMediumTTLContent
${tokenKey}Cookie (AES encrypted + secure)30 daystoken / user_id / has_login / id
${deviceKey}Cookie (AES encrypted + secure)10 yearsdeviceId UUID
${accountKey}localStorage (AES encrypted)No TTLAccountInfo object (lives in localStorage, never in cookies)
channel_code / share_codeCookie (AES encrypted + secure)30 daysChannel / referral codes
  • All cookies have secure: true by default (localhost / 127.0.0.1 auto-exempted) + sameSite: lax.
  • AES encryption is handled transparently by the framework useCookie / useLocal wrappers.
  • ⚠️ Encryption ≠ security barrier. See Client-side Encryption — The Security Boundary.
  • AccountInfo in localStorage has no TTL; "Remember me" means "remembered until logout or user opt-out" (aligned with browser password managers).

Cross-tab sync

Cookies are natively cross-tab (same origin + path). One tab logout() clears cookies → other tabs' useCookie refs return empty on next access → state becomes "not logged in".

But already-loaded pages don't auto re-render — listen to storage events or use BroadcastChannel for real-time sync. The framework doesn't bundle this layer (avoiding redundant subscriptions).

Identity establishment & security model

Device + auth identity is established entirely client-only (design: framework#44; security hardening: framework#45):

  • The framework never calls mutating methods during SSR (autoLogin / loginByDevice / getDeviceId / setAccount / setInfo, …) — no useCookie-writing mutation runs server-side, so the Pinia payload is never baked into ISR caches; these methods carry an assertClientOnly() guard and throw when called on the server
  • Device login is established by the client-side auth.client.ts plugin after hydrate; a logged-in user's token passes to SSR naturally via cookies; business calls to request() automatically await auth-ready
  • Password-class credentials never enter cookies: AccountInfo lives in encrypted localStorage — invisible in the DevTools Cookies panel and never sent with requests
  • remember semantics: the options.remember of loginByUsername / loginByQrcode (default true) decides whether AccountInfo persists; remember: false skips saving; call clearAccount() when the user unticks "remember me"
  • All core cookies are AES-encrypted + secure: true by default (localhost exempt) + sameSite: lax

Recipes

<!-- Recipe 1: LoginForm passes the "remember me" checkbox through (STORE-002) -->
<script setup lang="ts">
const isRemember = ref(true)
async function onSubmit() {
  await useUserStore().loginByUsername(form.username, form.password, {
    remember: isRemember.value,
  })
}
</script>
// Recipe 2: wait for the auth chain, then check login state (UAR-001)
const userStore = useUserStore()
onMounted(async () => {
  await useAuthReady()
  if (!userStore.isLoggedIn) return useLoginCheck()
  console.log(userStore.info?.nickname)
})
// Recipe 3: extend the store by wrapping, not rewriting (zero-conflict upgrades)
export const useAppUserStore = defineStore('app-user', () => {
  const base = useUserStore()
  const { info, isLoggedIn } = storeToRefs(base)
  const vipLevel = computed(() => (info.value?.vip as number) ?? 0)
  return { info, isLoggedIn, vipLevel, logout: base.logout }
})
// Recipe 4: SSR read/write boundary (STORE-001)
const userStore = useUserStore()
const logged = userStore.isLoggedIn   // ✅ read-only members are SSR-safe
// userStore.getDeviceId()            // ❌ mutating call in sync setup → server throw
onMounted(() => userStore.getDeviceId()) // ✅ move writes to client lifecycle

Caveats

  • State-mutating methods are client-only and throw when called during SSR (Rule STORE-001) — getDeviceId / getSessionId / setInfo / setToken / setAccount / clearAccount / loginByUsername / register / loginByQrcode / loginByDevice / autoLogin / expireLogin / logout all start with an assertClientOnly() guard. The reason: device id and token are established entirely on the client, so an SSR-time call writes Set-Cookie and bakes into the Pinia payload — which ISR then caches and broadcasts to every visitor, handing them all the same identity (a security-grade incident). Move such calls into onMounted, an event handler, or <ClientOnly>.
  • Read-only members are SSR-safe — info / token / authToken / isLoggedIn / getAccountInfo are exempt from the above.
  • "Remember me" must be passed through explicitly (Rule STORE-002) — remember defaults to true on loginByUsername / loginByQrcode. If your LoginForm doesn't forward the checkbox state, users who unchecked it are still persisted (account plus encrypted password in localStorage). Write loginByUsername(u, p, { remember: isRemember.value }).
  • Don't bypass the store to read core cookies (the swift-web/no-direct-cookie-in-app eslint rule blocks this) — note the two direct-read paths fail for different reasons: document.cookie.match(...) really does yield AES ciphertext; useCookie(...), however, yields plaintext (the framework overrides Nuxt's built-in useCookie under the same name, auto-prefixing cachePrefix and decrypting transparently). What's wrong with it is different — the real key is runtimeConfig.public.tokenKey (e.g. tt), so useCookie('token') reads a different, non-existent cookie and silently returns null; and even with the right key you bypass the store's write guards, auth-ready ordering, and field normalization, welding business code to the framework's internal storage layout.