[+] Accounts and login
This commit is contained in:
@@ -9,12 +9,13 @@
|
|||||||
text: string,
|
text: string,
|
||||||
onclick: () => void
|
onclick: () => void
|
||||||
}[]
|
}[]
|
||||||
open: boolean
|
open: boolean,
|
||||||
|
noClose?: boolean
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
let buttons = $derived(p.buttons ?? [{
|
let buttons = $derived([...(p.buttons ?? []), ...(p.noClose ? [] : [{
|
||||||
text: '关闭', onclick: () => open = false
|
text: '关闭', onclick: () => open = false
|
||||||
}])
|
}])])
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<Dialog title="错误" bind:open buttons={[{
|
<Dialog title="错误" bind:open buttons={[{
|
||||||
text: "刷新重试",
|
text: "刷新重试",
|
||||||
onclick: () => location.reload()
|
onclick: () => location.reload()
|
||||||
}]}>
|
}]} noClose>
|
||||||
<div class="text-red-500">
|
<div class="text-red-500">
|
||||||
{p.error}
|
{p.error}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+6
-1
@@ -9,7 +9,7 @@ export async function post(endpoint: string, data: any) {
|
|||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
}
|
}
|
||||||
}).then(async res => {
|
}).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()
|
return res.json()
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
@@ -25,5 +25,10 @@ export const API = {
|
|||||||
netease: {
|
netease: {
|
||||||
startImport: async (link: string) => await post('/api/import/netease/start', { link }),
|
startImport: async (link: string) => await post('/api/import/netease/start', { link }),
|
||||||
checkProgress: async (id: string) => await post('/api/import/netease/progress', { id })
|
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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,3 +53,33 @@ export async function updateUserData(user: UserDocument, data: Partial<UserData>
|
|||||||
const newData = { ...(user.data || {}), ...data }
|
const newData = { ...(user.data || {}), ...data }
|
||||||
await users.updateOne({ _id: user._id }, { $set: { data: newData } })
|
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 type { PageProps } from "./$types";
|
||||||
import Button from "../components/Button.svelte";
|
import Button from "../components/Button.svelte";
|
||||||
import { Layer } from "m3-svelte";
|
import { Layer } from "m3-svelte";
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
|
|
||||||
let { data }: PageProps = $props()
|
let { data }: PageProps = $props()
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<AppBar account={() => alert('Account clicked')} right={[
|
<AppBar account={() => goto('/user')} right={[
|
||||||
{icon: "i-material-symbols:settings-rounded", onclick: () => alert('Settings clicked')}
|
{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">
|
<script lang="ts">
|
||||||
import { TextFieldOutlined } from "m3-svelte";
|
import { TextFieldOutlined } from "m3-svelte"
|
||||||
import AppBar from "../../components/appbar/AppBar.svelte";
|
import AppBar from "../../components/appbar/AppBar.svelte"
|
||||||
import Button from "../../components/Button.svelte";
|
import Button from "../../components/Button.svelte"
|
||||||
import Dialog from "../../components/status/Dialog.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 loginMode = $state(false)
|
||||||
let loginCode = $state('')
|
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>
|
</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>
|
<br><br>
|
||||||
这个引继码将会在使用之后、或者未使用的 7 天后会失效</Dialog>
|
这个引继码将会在使用之后、或者未使用的 7 天后会失效
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<AppBar title="账号管理"/>
|
<AppBar title="账号管理"/>
|
||||||
|
|
||||||
@@ -30,15 +56,13 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="vbox gap-16px flex-1 min-h-0">
|
<div class="vbox gap-16px flex-1 min-h-0"></div>
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hbox p-16px gap-16px">
|
<div class="hbox p-16px gap-16px">
|
||||||
{#if !loginMode}
|
{#if !loginMode}
|
||||||
<Button big secondary icon="i-material-symbols:add" disabled={open} onclick={() => open = true}>生成引继码</Button>
|
<Button big secondary icon="i-material-symbols:add" disabled={showCodeOpen} onclick={generateCode}>生成引继码</Button>
|
||||||
<Button big secondary icon="i-material-symbols:login" disabled={open} onclick={() => loginMode = true}>用引继码登录</Button>
|
<Button big secondary icon="i-material-symbols:login" disabled={showCodeOpen} onclick={() => loginMode = true}>用引继码登录</Button>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user