[+] Accounts and login

This commit is contained in:
2025-11-21 11:10:11 +08:00
parent 917df282e8
commit b3ec8d2894
8 changed files with 112 additions and 19 deletions
+4 -3
View File
@@ -9,12 +9,13 @@
text: string,
onclick: () => void
}[]
open: boolean
open: boolean,
noClose?: boolean
} = $props()
let buttons = $derived(p.buttons ?? [{
let buttons = $derived([...(p.buttons ?? []), ...(p.noClose ? [] : [{
text: '关闭', onclick: () => open = false
}])
}])])
</script>
{#if open}
+1 -1
View File
@@ -11,7 +11,7 @@
<Dialog title="错误" bind:open buttons={[{
text: "刷新重试",
onclick: () => location.reload()
}]}>
}]} noClose>
<div class="text-red-500">
{p.error}
</div>
+6 -1
View File
@@ -9,7 +9,7 @@ export async function post(endpoint: string, data: any) {
'Content-Type': 'application/json'
}
}).then(async res => {
if (res.status >= 400) throw new Error(`错误 ${res.status}${await res.text()}`)
if (res.status >= 400) throw new Error(`${res.status}: ${await res.json().then(json => json['message'])}`)
return res.json()
}).catch(e => {
console.error(e)
@@ -25,5 +25,10 @@ export const API = {
netease: {
startImport: async (link: string) => await post('/api/import/netease/start', { link }),
checkProgress: async (id: string) => await post('/api/import/netease/progress', { id })
},
user: {
createSyncCode: async () => await post('/api/user/sync-code', {}),
loginWithSyncCode: async (code: string) => await post('/api/auth/login', { code })
}
}
+30
View File
@@ -52,4 +52,34 @@ export async function createSyncCode(session: string): Promise<string> {
export async function updateUserData(user: UserDocument, data: Partial<UserData>): Promise<void> {
const newData = { ...(user.data || {}), ...data }
await users.updateOne({ _id: user._id }, { $set: { data: newData } })
}
/**
* Login with sync code.
* @param code Sync code
* @param newUA User Agent of the new device
* @returns New session token
*/
export async function loginWithSyncCode(code: string, newUA: string): Promise<string> {
const user = await users.findOne({ syncCode: code })
if (!user) throw error(401, 'Invalid sync code')
// Check expiration (7 days)
if (user.syncCodeCreated && (Date.now() - user.syncCodeCreated.getTime() > 7 * 24 * 60 * 60 * 1000)) {
await users.updateOne({ _id: user._id }, { $unset: { syncCode: "", syncCodeCreated: "" } })
throw error(401, 'Sync code expired')
}
const ses = `${crypto.randomUUID()}-${Date.now().toString(36)}`
// Add new session and clear sync code (one-time use)
await users.updateOne(
{ _id: user._id },
{
$push: { sessions: ses },
$unset: { syncCode: "", syncCodeCreated: "" }
}
)
return ses
}
+2 -1
View File
@@ -5,6 +5,7 @@
import type { PageProps } from "./$types";
import Button from "../components/Button.svelte";
import { Layer } from "m3-svelte";
import { goto } from "$app/navigation";
let { data }: PageProps = $props()
@@ -15,7 +16,7 @@
</script>
<AppBar account={() => alert('Account clicked')} right={[
<AppBar account={() => goto('/user')} right={[
{icon: "i-material-symbols:settings-rounded", onclick: () => alert('Settings clicked')}
]} />
+21
View File
@@ -0,0 +1,21 @@
import { error, json } from '@sveltejs/kit'
import { loginWithSyncCode } from '$lib/server/user'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ request, cookies }) => {
const { code } = await request.json()
if (!code) throw error(400, 'Missing sync code')
const ua = request.headers.get('user-agent') || 'unknown'
const session = await loginWithSyncCode(code, ua)
// Set session cookie
cookies.set('session', session, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 365 // 1 year
})
return json({ success: true })
}
+11
View File
@@ -0,0 +1,11 @@
import { error, json } from '@sveltejs/kit'
import { createSyncCode } from '$lib/server/user'
import type { RequestHandler } from './$types'
export const POST: RequestHandler = async ({ cookies }) => {
const session = cookies.get('session')
if (!session) throw error(401, 'Unauthorized')
const code = await createSyncCode(session)
return json({ code })
}
+37 -13
View File
@@ -1,17 +1,43 @@
<script lang="ts">
import { TextFieldOutlined } from "m3-svelte";
import AppBar from "../../components/appbar/AppBar.svelte";
import Button from "../../components/Button.svelte";
import Dialog from "../../components/status/Dialog.svelte";
import { TextFieldOutlined } from "m3-svelte"
import AppBar from "../../components/appbar/AppBar.svelte"
import Button from "../../components/Button.svelte"
import Dialog from "../../components/status/Dialog.svelte"
import { API } from "$lib/client"
import ErrorDialog from "../../components/status/ErrorDialog.svelte"
let open = $state(false)
let showCodeOpen = $state(false)
let loginSuccessOpen = $state(false)
let loginMode = $state(false)
let loginCode = $state('')
let generatedCode = $state('')
let error = $state('')
const generateCode = async () => await API.user.createSyncCode()
.then(res => {
generatedCode = res.code
showCodeOpen = true
}).catch(e => error = e.message)
const doLogin = async () => await API.user.loginWithSyncCode(loginCode)
.then(() => loginSuccessOpen = true).catch(e => error = e.message)
</script>
<Dialog title="生成引继码" bind:open>引继码生成成功!生成的引继码是:01234-56789-ABCDE-F0123
<ErrorDialog error={error}/>
<Dialog title="登录成功" bind:open={loginSuccessOpen} buttons={[{
text: '跳转', onclick: () => location.href = '/'
}]}>
登录成功!
</Dialog>
<Dialog title="生成引继码" bind:open={showCodeOpen} buttons={[{
text: '复制', onclick: () => navigator.clipboard.writeText(generatedCode)
}]}>
引继码生成成功!生成的引继码是:{generatedCode}
<br><br>
这个引继码将会在使用之后、或者未使用的 7 天后会失效</Dialog>
这个引继码将会在使用之后、或者未使用的 7 天后会失效
</Dialog>
<AppBar title="账号管理"/>
@@ -30,15 +56,13 @@
</div>
{/if}
<div class="vbox gap-16px flex-1 min-h-0">
</div>
<div class="vbox gap-16px flex-1 min-h-0"></div>
<div class="hbox p-16px gap-16px">
{#if !loginMode}
<Button big secondary icon="i-material-symbols:add" disabled={open} onclick={() => open = true}>生成引继码</Button>
<Button big secondary icon="i-material-symbols:login" disabled={open} onclick={() => loginMode = true}>用引继码登录</Button>
<Button big secondary icon="i-material-symbols:add" disabled={showCodeOpen} onclick={generateCode}>生成引继码</Button>
<Button big secondary icon="i-material-symbols:login" disabled={showCodeOpen} onclick={() => loginMode = true}>用引继码登录</Button>
{:else}
<Button big secondary icon="i-material-symbols:login" disabled={open} onclick={() => alert("TODO")}>登录</Button>
<Button big secondary icon="i-material-symbols:login" disabled={showCodeOpen} onclick={doLogin}>登录</Button>
{/if}
</div>