Skip to content

Commit

Permalink
feat(status): websocket user validation
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Mar 17, 2021
1 parent c50ba31 commit 064810c
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 118 deletions.
5 changes: 0 additions & 5 deletions packages/plugin-status/client/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,3 @@ $bp_xs: 480px;
$navbarHeight: 4rem;
$sidebarWidth: 16rem;
$mainPadding: 2rem;

a {
color: $default;
text-decoration: none;
}
59 changes: 35 additions & 24 deletions packages/plugin-status/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,15 @@
/* eslint-disable no-undef */

import { ref } from 'vue'
import type { User } from 'koishi-core'
import type { Payload } from '~/server'

export const status = ref<Payload>(null)
export const socket = ref<WebSocket>(null)

export function start() {
socket.value = new WebSocket(KOISHI_ENDPOINT.replace(/^http/, 'ws'))
receive('update', body => status.value = body)
}

export function send(data: any) {
socket.value.send(JSON.stringify(data))
}
const prefix = 'koishi:'

export function receive<T = any>(event: string, listener: (data: T) => void) {
socket.value.onmessage = (ev) => {
const data = JSON.parse(ev.data)
if (data.type === event) {
console.log(event, data.body)
listener(data.body)
}
}
}

export namespace Storage {
export namespace storage {
export function get(key: string) {
if (typeof localStorage === 'undefined') return
const rawData = localStorage.getItem(key)
const rawData = localStorage.getItem(prefix + key)
if (!rawData) return
try {
return JSON.parse(rawData)
Expand All @@ -37,6 +18,36 @@ export namespace Storage {

export function set(key: string, value: any) {
if (typeof localStorage === 'undefined') return
localStorage.setItem(key, JSON.stringify(value))
localStorage.setItem(prefix + key, JSON.stringify(value))
}
}

export const user = ref<User>(storage.get('user'))
export const status = ref<Payload>(null)
export const socket = ref<WebSocket>(null)

const listeners: Record<string, (data: any) => void> = {}

export function start() {
socket.value = new WebSocket(KOISHI_ENDPOINT.replace(/^http/, 'ws'))
socket.value.onmessage = (ev) => {
const data = JSON.parse(ev.data)
console.log(data)
if (data.type in listeners) {
listeners[data.type](data.body)
}
}
receive('update', data => status.value = data)
receive('user', data => {
user.value = data
storage.set('user', data)
})
}

export function send(type: string, body: any) {
socket.value.send(JSON.stringify({ type, body }))
}

export function receive<T = any>(event: string, listener: (data: T) => void) {
listeners[event] = listener
}
17 changes: 14 additions & 3 deletions packages/plugin-status/client/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Card from './components/card.vue'
import Button from './components/button.vue'
import Input from './components/input.vue'
import App from './views/layout/index.vue'
import { start } from '.'
import { start, user } from '.'

// for el-collapse-transition
import 'element-plus/lib/theme-chalk/base.css'
Expand All @@ -24,7 +24,7 @@ declare module 'vue-router' {
icon?: string
status?: boolean
auth?: boolean
standalone?: boolean
frameless?: boolean
}
}

Expand Down Expand Up @@ -52,10 +52,15 @@ const router = createRouter({
name: '沙盒',
meta: { icon: 'laptop-code', auth: true },
component: () => import('./views/sandbox.vue'),
}, {
path: '/profile',
name: '资料',
meta: { icon: 'user-circle', auth: true },
component: () => import('./views/profile.vue'),
}, {
path: '/login',
name: '登录',
meta: { icon: 'sign-in-alt', standalone: true },
meta: { icon: 'sign-in-alt', frameless: true },
component: () => import('./views/login.vue'),
}],
})
Expand All @@ -71,6 +76,12 @@ app.use(ElCollapseTransition)

app.use(router)

router.beforeEach((route) => {
if (route.meta.auth && !user.value) {
return '/login'
}
})

router.afterEach((route) => {
if (typeof route.name === 'string') {
document.title = route.name + ' | Koishi 控制台'
Expand Down
15 changes: 10 additions & 5 deletions packages/plugin-status/client/views/layout/index.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<template>
<template v-if="!standalone">
<template v-if="!frameless">
<navbar/>
<sidebar/>
</template>
<main :class="{ standalone }">
<main :class="{ frameless }">
<router-view v-if="status"/>
</main>
</template>
Expand All @@ -17,13 +17,13 @@ import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const standalone = computed(() => route.meta.standalone)
const frameless = computed(() => route.meta.frameless)
</script>

<style lang="scss">
@import '../../index.scss';
@import '~/variables';
body {
margin: 0;
Expand All @@ -39,6 +39,11 @@ body {
position: relative;
}
a {
color: $default;
text-decoration: none;
}
main {
margin: $navbarHeight 0;
padding: 0 $mainPadding;
Expand All @@ -49,7 +54,7 @@ main {
left: $sidebarWidth;
}
main.standalone {
main.frameless {
margin: 0;
left: 0;
top: 50%;
Expand Down
9 changes: 8 additions & 1 deletion packages/plugin-status/client/views/layout/navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@
<nav>
<span class="title">Koishi 控制台</span>
<span class="right">
<router-link to="/login">登录</router-link>
<router-link v-if="user" to="/profile">{{ user.name }}</router-link>
<router-link v-else to="/login">登录</router-link>
</span>
</nav>
</template>

<script lang="ts" setup>
import { user } from '~/client'
</script>

<style lang="scss">
nav {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-status/client/views/layout/sidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<aside>
<ul>
<template v-for="(route, index) in $router.getRoutes()" :key="index">
<li v-if="!route.meta.standalone" :class="{ current: route.name === $route.name }">
<li v-if="!route.meta.frameless" :class="{ current: route.name === $route.name }">
<router-link :to="route.path">
<i :class="['fas', `fa-${route.meta.icon}`]"/>
{{ route.name }}
Expand Down
50 changes: 33 additions & 17 deletions packages/plugin-status/client/views/login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,62 @@
/
<span :class="{ inactive: type === 0 }" @click="type = 1">用户名密码登录</span>
</h1>
<template v-if="token">
<p class="hint">您的账号:{{ form2 }}</p>
<p class="hint">请用上述账号将下面的验证码私聊发送给任意机器人</p>
<p class="token">{{ token }}</p>
<template v-if="data.token">
<p class="hint">欢迎你,{{ data.name || 'Koishi 用户' }}!</p>
<p class="hint">请用上述账号将下面的验证码私聊发送给任意机器人</p>
<p class="token">{{ data.token }}</p>
</template>
<template v-else>
<k-input :prefix-icon="presets[type][0]" :placeholder="presets[type][1]" v-model="form1"/>
<k-input :prefix-icon="presets[type][2]" :placeholder="presets[type][3]" v-model="form2"/>
<k-input :prefix-icon="presets[type][2]" :placeholder="presets[type][3]" v-model="form2" @enter="enter"/>
<p class="error" v-if="data.message">{{ data.message }}</p>
<div class="control">
<k-button @click="$router.back()">返回</k-button>
<k-button @click="validate">{{ presets[type][4] }}</k-button>
<k-button @click="enter">{{ presets[type][4] }}</k-button>
</div>
</template>
</k-card>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { send, receive, user } from '~/client'
const presets = [
['at', '平台名', 'user', '账号', '获取验证码'],
['user', '用户名', 'lock', '密码', '登录'],
]
interface TokenData {
token?: string
name?: string
message?: string
}
const type = ref(0)
const token = ref('')
const data = ref<TokenData>({})
const form1 = ref('')
const form2 = ref('')
async function validate() {
const router = useRouter()
receive('token', body => data.value = body)
watch(user, (value) => {
if (!value) return
router.push('/profile')
})
const timestamp = ref(0)
async function enter() {
const now = Date.now()
if (now < timestamp.value) return
if (!form1.value || !form2.value) return
try {
const res = await fetch(`${KOISHI_ENDPOINT}/validate?platform=${form1.value}&userId=${form2.value}`, { mode: 'cors' })
const data = await res.json()
console.log(data)
token.value = data.token
} catch (err) {
console.error(err)
}
timestamp.value = now + 10000
send('token', { platform: form1.value, userId: form2.value })
}
</script>
Expand Down
9 changes: 9 additions & 0 deletions packages/plugin-status/client/views/profile.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
{{ user }}
</template>

<script lang="ts" setup>
import { user } from '~/client'
</script>
2 changes: 1 addition & 1 deletion packages/plugin-status/client/views/sandbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const messages = reactive<Message[]>([])
function onEnter() {
if (!text.value) return
messages.push({ from: 'user', content: text.value })
send({ type: 'sandbox', body: text.value })
send('sandbox', text.value)
text.value = ''
}
Expand Down
46 changes: 40 additions & 6 deletions packages/plugin-status/server/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Adapter, App, Bot, BotOptions, Random } from 'koishi-core'
import { Adapter, Bot, BotOptions, Context, Logger, Random } from 'koishi-core'
import Profile from './profile'
import WebSocket from 'ws'

Expand All @@ -25,21 +25,37 @@ class WebBot extends Bot<'sandbox'> {
}
}

const logger = new Logger('status')
const states: Record<string, [string, number, WebSocket]> = {}

export namespace WebAdapter {
export interface Config {
path?: string
expiration?: number
}
}

export class WebAdapter extends Adapter<'sandbox'> {
server: WebSocket.Server

constructor(app: App, config: WebAdapter.Config) {
super(app, WebBot)
constructor(ctx: Context, public config: WebAdapter.Config) {
super(ctx.app, WebBot)
this.server = new WebSocket.Server({
path: config.path,
server: app._httpServer,
server: ctx.app._httpServer,
})

ctx.all().middleware(async (session, next) => {
if (session.subtype !== 'private') return next()
const state = states[session.uid]
if (state && state[0] === session.content) {
return state[2].send(JSON.stringify({
type: 'user',
body: await session.observeUser(['id', 'name', 'authority']),
}))
}
return next()
}, true)
}

async start() {
Expand All @@ -50,9 +66,27 @@ export class WebAdapter extends Adapter<'sandbox'> {
bot.status = Bot.Status.GOOD
socket.on('close', () => {
bot.dispose()
for (const id in states) {
if (states[id][2] === socket) delete states[id]
}
})
socket.on('message', (data) => {
console.log(data)
socket.on('message', async (data) => {
const { type, body } = JSON.parse(data.toString())
if (type === 'token') {
const { platform, userId } = body
const user = await this.app.database.getUser(platform, userId, ['name'])
if (!user) return socket.send(JSON.stringify({ type: 'token', body: { message: '没有此账户。' } }))
const id = `${platform}:${userId}`
const token = Random.uuid()
const expire = Date.now() + this.config.expiration
states[id] = [token, expire, socket]
setTimeout(() => {
if (states[id]?.[1] > Date.now()) delete states[id]
}, this.config.expiration)
socket.send(JSON.stringify({ type: 'token', body: { token, name: user.name } }))
} else {
logger.info(type, body)
}
})
})
}
Expand Down

0 comments on commit 064810c

Please sign in to comment.