[+] Accounts and login
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<Dialog title="错误" bind:open buttons={[{
|
||||
text: "刷新重试",
|
||||
onclick: () => location.reload()
|
||||
}]}>
|
||||
}]} noClose>
|
||||
<div class="text-red-500">
|
||||
{p.error}
|
||||
</div>
|
||||
|
||||
+6
-1
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')}
|
||||
]} />
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user