Skip to content

Commit

Permalink
feat(auth): support platform login
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Feb 13, 2022
1 parent 7aeaef0 commit 375fba3
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 66 deletions.
1 change: 1 addition & 0 deletions packages/core/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ export class Context {
private createTimerDispose(timer: NodeJS.Timeout) {
const dispose = () => {
clearTimeout(timer)
if (!this.state) return
return remove(this.state.disposables, dispose)
}
this.state.disposables.push(dispose)
Expand Down
15 changes: 11 additions & 4 deletions plugins/frontend/auth/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Context } from '~/client'
import { Context, store } from '~/client'
import { Icons } from '~/components'
import { user } from './utils'
import Login from './login.vue'
import Profile from './profile.vue'
import SignIn from './icons/sign-in.vue'
Expand All @@ -12,19 +11,27 @@ Icons.register('sign-out', SignOut)
Icons.register('user-full', UserFull)

export default (ctx: Context) => {
const dispose = Context.router.beforeEach((route, from) => {
if (route.meta.authority && !store.user) {
return history.state.forward === '/login' ? '/' : '/login'
}
})

ctx.disposables.push(dispose)

ctx.addPage({
path: '/login',
name: '登录',
icon: 'sign-in',
position: () => user.value ? 'hidden' : 'bottom',
position: () => store.user ? 'hidden' : 'bottom',
component: Login,
})

ctx.addPage({
path: '/profile',
name: '用户资料',
icon: 'user-full',
position: () => user.value ? 'bottom' : 'hidden',
position: () => store.user ? 'bottom' : 'hidden',
component: Profile,
})
}
51 changes: 30 additions & 21 deletions plugins/frontend/auth/client/login.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<template>
<k-card class="login">
<template v-if="data.token">
<template v-if="user">
<h1><span>平台账户登录</span></h1>
<p class="hint">欢迎你,{{ data.name || 'Koishi 用户' }}!</p>
<p class="hint">欢迎你,{{ user.name || 'Koishi 用户' }}!</p>
<p class="hint">请用上述账号将下面的验证码私聊发送给任意机器人</p>
<p class="token">{{ data.token }}</p>
<p class="token">{{ user.token }}</p>
<div class="control">
<k-button @click="data.token = null">返回上一步</k-button>
<k-button @click="user.token = null">返回上一步</k-button>
</div>
</template>
<template v-else>
Expand All @@ -19,7 +19,7 @@
<template v-if="config.authType === 0">
<k-input prefix="at" placeholder="平台名" v-model="config.platform"/>
<k-input prefix="user" placeholder="账号" v-model="config.userId" @enter="enter"/>
<p class="error" v-if="data.message">{{ data.message }}</p>
<p class="error" v-if="message">{{ message }}</p>
<div class="control">
<k-button @click="$router.back()">返回</k-button>
<k-button @click="enter">获取验证码</k-button>
Expand All @@ -32,7 +32,7 @@
:suffix="config.showPass ? 'eye' : 'eye-slash'"
@click-suffix="config.showPass = !config.showPass"
/>
<p class="error" v-if="data.message">{{ data.message }}</p>
<p class="error" v-if="message">{{ message }}</p>
<div class="control">
<k-button @click="$router.back()">返回</k-button>
<k-button @click="login">登录</k-button>
Expand All @@ -45,41 +45,50 @@
<script lang="ts" setup>
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { config, sha256, user } from './utils'
import { send, receive } from '~/client'
interface LoginData {
token?: string
name?: string
message?: string
}
import { useRouter, useRoute } from 'vue-router'
import { config, sha256 } from './utils'
import { send, store } from '~/client'
import { LoginUser } from '@koishijs/plugin-auth'
const secure = isSecureContext
if (!secure) config.authType = 0
const data = ref<LoginData>({})
const router = useRouter()
receive('login', body => data.value = body)
watch(user, (value) => {
watch(() => store.user, (value) => {
if (!value) return
router.push(router.currentRoute.value.redirectedFrom?.fullPath || '/profile')
const from = router.currentRoute.value.redirectedFrom
if (from && from.name !== '登录') {
router.push(from)
} else {
router.push('/profile')
}
})
const message = ref<string>()
const user = ref<LoginUser>()
let timestamp = 0
async function enter() {
const now = Date.now()
if (now < timestamp) return
const { platform, userId } = config
if (!platform || !userId) return
timestamp = now + 1000
send('token', { platform, userId })
try {
user.value = await send('login/platform', platform, userId)
} catch (e) {
message.value = e.message
}
}
async function login() {
const { username, password } = config
send('login', { username, password: await sha256(password) })
try {
await send('login/password', username, await sha256(password))
} catch (e) {
message.value = e.message
}
}
</script>
Expand Down
11 changes: 5 additions & 6 deletions plugins/frontend/auth/client/profile.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<k-card title="基本资料">
<p>用户名:{{ user.name }}</p>
<p>权限等级:{{ user.authority }}</p>
<p>用户名:{{ store.user.name }}</p>
<p>权限等级:{{ store.user.authority }}</p>
</k-card>
<k-card title="设置密码" v-if="secure">
<k-input v-model="password" @enter="enter"
Expand All @@ -17,17 +17,16 @@

<script lang="ts" setup>
import { send } from '~/client'
import { sha256, config, user } from './utils'
import { send, store } from '~/client'
import { sha256, config } from './utils'
import { ref } from 'vue'
const secure = isSecureContext
const password = ref(config.password)
async function enter() {
if (!password.value) return
const { id, token } = user.value
send('password', { id, token, password: await sha256(password.value) })
send('user/modify', { password: await sha256(password.value) })
config.password = password.value
}
Expand Down
9 changes: 5 additions & 4 deletions plugins/frontend/auth/client/utils.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import { useStorage } from '@vueuse/core'
import { reactive, ref } from 'vue'
import {} from '@koishijs/plugin-auth'

export function useVersionStorage<T extends object>(key: string, version: string, fallback?: () => T) {
const storage = useStorage('koishi.' + key, {})
const storage = useStorage('koishi.console.' + key, {})
if (storage.value['version'] !== version) {
storage.value = { version, data: fallback() }
}
return reactive<T>(storage.value['data'])
}

export const user = ref()

interface AuthConfig {
authType: 0 | 1
username?: string
password?: string
platform?: string
userId?: string
showPass?: boolean
token?: string
expire?: number
}

export const config = useVersionStorage<AuthConfig>('managerConfig', '2.0', () => ({
export const config = useVersionStorage<AuthConfig>('auth', '1.0', () => ({
authType: 0,
}))

Expand Down
1 change: 1 addition & 0 deletions plugins/frontend/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"authentication",
"login",
"console",
"required:database",
"required:console"
],
"peerDependencies": {
Expand Down
148 changes: 137 additions & 11 deletions plugins/frontend/auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,143 @@
import { Context, Schema } from 'koishi'
import {} from '@koishijs/plugin-console'
import { Awaitable, Context, pick, Schema, Time, User } from 'koishi'
import { DataService, SocketHandle } from '@koishijs/plugin-console'
import { resolve } from 'path'
import { v4 } from 'uuid'

export interface Config {}
declare module 'koishi' {
interface User {
password: string
token: string
expire: number
}
}

declare module '@koishijs/plugin-console' {
interface SocketHandle {
user?: AuthUser
}

export const name = 'auth'
export const using = ['console'] as const
export const schema: Schema<Config> = Schema.object({})
namespace Console {
interface Services {
user: AuthService
}
}

export function apply(ctx: Context) {
if (ctx.console.config.devMode) {
ctx.console.addEntry(resolve(__dirname, '../client/index.ts'))
} else {
ctx.console.addEntry(resolve(__dirname, '../dist'))
interface Events {
'login/platform'(this: SocketHandle, platform: string, userId: string): Awaitable<LoginUser>
'login/password'(this: SocketHandle, name: string, password: string): void
'login/token'(this: SocketHandle, id: string, token: string): void
'user/modify'(this: SocketHandle, data: Partial<Pick<User, 'name' | 'password'>>): void
}
}

export type AuthUser = Pick<User, 'id' | 'name' | 'password' | 'authority' | 'token' | 'expire'>
export type LoginUser = Pick<User, 'id' | 'name' | 'token' | 'expire'>

const authFields = ['name', 'password', 'authority', 'id', 'expire', 'token'] as (keyof AuthUser)[]

function initAuth(handle: SocketHandle, value: AuthUser) {
handle.user = value
handle.send({ type: 'data', body: { key: 'user', value } })
}

class AuthService extends DataService<AuthUser> {
static using = ['console', 'database'] as const

constructor(ctx: Context, private config: AuthService.Config) {
super(ctx, 'user')

ctx.model.extend('user', {
password: 'string(63)',
token: 'string(63)',
expire: 'unsigned(20)',
})

if (ctx.console.config.devMode) {
ctx.console.addEntry(resolve(__dirname, '../client/index.ts'))
} else {
ctx.console.addEntry(resolve(__dirname, '../dist'))
}

this.initLogin()
}

initLogin() {
const { ctx, config } = this
const states: Record<string, [string, number, SocketHandle]> = {}

ctx.console.addListener('login/password', async function (name, password) {
const user = await ctx.database.getUser('name', name, authFields)
if (!user || user.password !== password) throw new Error('用户名或密码错误。')
user.token = v4()
user.expire = Date.now() + config.authTokenExpire
await ctx.database.setUser('name', name, pick(user, ['token', 'expire']))
initAuth(this, user)
})

ctx.console.addListener('login/token', async function (id, token) {
const user = await ctx.database.getUser('id', id, authFields)
if (!user) throw new Error('用户不存在。')
if (user.token !== token || user.expire <= Date.now()) throw new Error('令牌已失效。')
initAuth(this, user)
})

ctx.console.addListener('login/platform', async function (platform, userId) {
const user = await ctx.database.getUser(platform, userId, ['name'])
if (!user) throw new Error('找不到此账户。')
const id = `${platform}:${userId}`
const token = v4()
const expire = Date.now() + config.loginTokenExpire
states[id] = [token, expire, this]

const listener = () => {
delete states[id]
dispose()
this.socket.off('close', listener)
}
const dispose = ctx.setTimeout(() => {
if (states[id]?.[1] >= Date.now()) listener()
}, config.loginTokenExpire)
this.socket.on('close', listener)

return { id: user.id, name: user.name, token, expire }
})

ctx.any().private().middleware(async (session, next) => {
const state = states[session.uid]
if (state && state[0] === session.content) {
const user = await session.observeUser(authFields)
user.token = v4()
user.expire = Date.now() + config.authTokenExpire
return initAuth(state[2], user)
}
return next()
}, true)

ctx.on('console/intercept', async (handle, listener) => {
if (!listener.authority) return false
if (!handle.user) return true
if (handle.user.expire <= Date.now()) return true
return handle.user.authority < listener.authority
})

ctx.console.addListener('user/modify', async function ({ name, password }) {
// const user = await ctx.database.getUser('id', id, ['token', 'expire', 'authority', 'password'])
// if (!user || password === user.password) return
// await ctx.database.setUser('id', id, { password })
})
}
}

namespace AuthService {
export interface Config {
authTokenExpire?: number
loginTokenExpire?: number
}

export const Config: Schema<Config> = Schema.object({
authTokenExpire: Schema.natural().role('ms').default(Time.week).description('用户令牌有效期。'),
loginTokenExpire: Schema.natural().role('ms').default(Time.minute * 10).description('登录令牌有效期。'),
})
}

export default AuthService
1 change: 1 addition & 0 deletions plugins/frontend/console/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export function getValue<T>(computed: Computed<T>): T {

export class Context {
static app: App
static router = router
static pending: Dict<DisposableExtension[]> = {}

public disposables: Disposable[] = []
Expand Down
Loading

0 comments on commit 375fba3

Please sign in to comment.