Skip to content

Commit

Permalink
sessions
Browse files Browse the repository at this point in the history
  • Loading branch information
arily committed Jul 10, 2023
1 parent 473c82e commit 6fa0d23
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 47 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"trpc-nuxt": "^0.10.5",
"ts-enum-util": "^4.0.2",
"ua-parser-js": "^1.0.35",
"useragent": "^2.3.0",
"uuid": "^9.0.0",
"v-lazy-image": "^2.1.1",
"validator": "^13.9.0",
Expand Down
16 changes: 16 additions & 0 deletions src/def/device.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export enum OS {
Unknown = -1,
Linux,
Windows,
Android,
macOS,
iOS,
iPadOS,
ChromiumOS,
}

export enum Client {
Unknown = -1,
Browser,
OsuStableClient,
}
114 changes: 93 additions & 21 deletions src/pages/me/settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Cropper } from 'vue-advanced-cropper'
import 'vue-advanced-cropper/dist/style.css'
import md5 from 'md5'
import { Client, OS } from '~/def/device'
import type { ContentEditor, TModal, TResponsiveModal } from '#components'
import { useSession } from '~/store/session'
Expand Down Expand Up @@ -34,6 +35,7 @@ useHead({
})
const { data: user, refresh } = await useAsyncData(() => app$.$client.me.settings.query())
const { data: sessions } = await useAsyncData(() => app$.$client.me.sessions.query())
if (!user.value) {
await navigateTo({
Expand Down Expand Up @@ -342,8 +344,6 @@ function resetAvatar() {
</form>
</div>
</t-modal>
<!-- used as padding placeholders -->
<header-simple-title-with-sub />
<div class="container mx-auto">
<div class="flex justify-between p-2 items-end">
<div class="text-3xl font-bold">
Expand All @@ -367,20 +367,23 @@ function resetAvatar() {
<div class="flex flex-col flex-wrap justify-between md:flex-row">
<div class="grow w-full lg:[max-width:50%]">
<div
class="flex items-end justify-center p-3 overflow-hidden shadow-md gap-4 md:justify-start bg-gbase-200/30 dark:bg-gbase-700/40 sm:rounded-3xl lg:mr-4"
class="flex items-end p-3 overflow-hidden gap-4 lg:mr-4"
>
<div class="relative z-10 mask mask-squircle hoverable w-100 self-center [&>img]:hover:blur-lg [&>img]:hover:opacity-50 no-animation">
<button
class="absolute top-0 z-20 w-full h-full btn btn-primary hover:bg-primary/50 focus:active:bg-primary/50"
type="button"
@click="() => changeAvatar?.showModal()"
>
<icon name="ic:round-edit-note" class="w-5 h-5" size="100%" /> change
</button>
<img
:src="newAvatarURL || `${user.avatarSrc}`"
class="pointer-events-none _avatar"
>
<div class="drop-shadow-md">
<div class="relative z-10 mask mask-squircle hoverable w-100 self-center [&>img]:hover:blur-lg [&>img]:hover:opacity-50 no-animation">
<button
class="absolute top-0 z-20 w-full h-full btn btn-primary hover:bg-primary/50 focus:active:bg-primary/50"
type="button"
@click="() => changeAvatar?.showModal()"
>
<icon name="ic:round-edit-note" class="w-5 h-5" size="100%" />
change
</button>
<img
:src="newAvatarURL || `${user.avatarSrc}`"
class="pointer-events-none _avatar w-40 h-40"
>
</div>
</div>
<div>
<h1 class="text-5xl text-left">
Expand All @@ -394,13 +397,76 @@ function resetAvatar() {
<div class="pb-4" />
</div>
</div>
<div class="p-3 lg:mr-4">
<label class="label" for="session">
<span class="pl-3 label-text">Session</span>
</label>
<div id="session">
<div class="overflow-x-auto">
<table class="table">
<!-- head -->
<thead>
<tr>
<!-- <th>
<label>
<input type="checkbox" class="checkbox">
</label>
</th> -->
<th>Name</th>
<th>Last seen at</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<!-- eslint-disable-next-line vue/no-template-shadow -->
<tr v-for="session, id of sessions" :key="id">
<!-- <th>
<label>
<input type="checkbox" class="checkbox">
</label>
</th> -->
<td>
<div class="flex items-center space-x-3">
<div class="avatar">
<div class="mask mask-squircle">
<!-- <img src="/[email protected]" alt="Windows"> -->
<icon v-if="session.OS === OS.Windows" name="basil:windows-solid" class="w-9 h-9" />
<icon v-if="session.OS === OS.macOS" name="ic:baseline-apple" class="w-9 h-9" />
</div>
</div>
<div>
<div class="font-bold">
{{ OS[session.OS] }}
</div>
<div class="text-sm opacity-50">
{{ Client[session.client] }}
</div>
</div>
</div>
</td>
<td>
{{ session.lastActivity.toLocaleString() }}
<br>
<span v-if="session.current" class="badge badge-ghost badge-sm">Current Session</span>
</td>
<th>
<button class="btn btn-ghost btn-xs">
Kick
</button>
</th>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="grow lg:[max-width:50%] mt-4 lg:mt-0 lg:pl-4 pr-2 lg:mr-0">
<div class="text-red-500">
{{ errorMessage.join(",") }}
</div>
<div class="form-control">
<label class="label">
<label class="label" for="username">
<span class="pl-3 label-text">Username</span>
</label>
<div
Expand All @@ -409,6 +475,7 @@ function resetAvatar() {
"
>
<input
id="username"
v-model="user.name"
type="text"
placeholder="Username"
Expand Down Expand Up @@ -436,11 +503,12 @@ function resetAvatar() {
</div>
</div>
<div>
<label class="label">
<label class="label" for="userlink">
<span class="pl-3 label-text">Link</span>
</label>
<div class="flex gap-4">
<input
id="userlink"
v-model="user.safeName"
type="text"
class="input input-sm grow"
Expand All @@ -458,7 +526,7 @@ function resetAvatar() {
</div>
</div>
<div class="form-control">
<label class="label">
<label class="label" for="email">
<span class="pl-3 label-text">Email</span>
</label>
<div
Expand All @@ -467,6 +535,7 @@ function resetAvatar() {
"
>
<input
id="email"
v-model="user.email"
type="email"
placeholder="[email protected]"
Expand All @@ -493,9 +562,10 @@ function resetAvatar() {
</div>
</div>
<div>
<label class="label">
<label class="label" for="password">
<span class="pl-3 label-text">Password</span>
<button
id="#password"
class="btn btn-sm btn-secondary"
type="button"
@click.prevent="() => changePassword?.showModal()"
Expand All @@ -505,11 +575,12 @@ function resetAvatar() {
</label>
</div>
<div>
<label class="label">
<label class="label" for="api">
<span class="pl-3 label-text">API Key</span>
</label>
<div class="flex gap-4">
<input
id="api"
v-model="user.secrets.apiKey"
type="text"
placeholder="Your API Key"
Expand Down Expand Up @@ -543,10 +614,11 @@ function resetAvatar() {
</div>
</div>

<label class="label">
<label class="label" for="profile">
<span class="pl-3 label-text">profile</span>
</label>
<lazy-content-editor
id="profile"
ref="editor"
v-model.lazy="profile"
:html="user.profile?.html"
Expand Down
9 changes: 1 addition & 8 deletions src/server/backend/$base/server/article/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,7 @@ import { Logger } from '$base/log'
const logger = Logger.child({ label: 'article' })

async function access(file: PathLike, constant?: typeof fs['constants'][keyof typeof fs['constants']]) {
return fs.access(file, constant).then(() => true).catch(() => {
logger.info({
message: `Trying to access ${file} (mode: ${constant}) failed.`,
file,
constant,
})
return false
})
return fs.access(file, constant).then(() => true).catch(() => false)
}

export abstract class ArticleProvider {
Expand Down
48 changes: 36 additions & 12 deletions src/server/backend/$base/server/session.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,51 @@
import { v4 } from 'uuid'
import { match } from 'switch-pattern'
import { Logger } from '$base/log'
import { Client, OS } from '~/def/device'

const logger = Logger.child({ label: 'session' })

export interface Session<Id = string> {
userId?: Id
export interface SessionBase<UserIdType = string> {
userId?: UserIdType
lastActivity: Date
OS: OS
}

export interface SessionStore<TSessionId, TSession> {
export interface SessionBrowser {
client: Client.Browser
browser: string
}
export interface SessionOsuClient {

client: Client.OsuStableClient
version: string
}
export interface SessionUnknown {
client: Client.Unknown
}

export type SessionClient = SessionBrowser | SessionOsuClient | SessionUnknown

export type Session<T = string> = SessionBase<T> & SessionClient

export interface SessionStore<TSessionId extends string | symbol, TSession extends Session<any>> {
get(key: TSessionId): PromiseLike<TSession | undefined>
set(key: TSessionId, value: TSession): PromiseLike<TSessionId>
destroy(key: TSessionId): PromiseLike<boolean>
forEach(cb: (arg0: TSession, arg1: TSessionId) => PromiseLike<void>): PromiseLike<void>
forEach(cb: (session: TSession, id: TSessionId) => void | PromiseLike<void>): PromiseLike<void>
findAll(query: Pick<TSession, 'OS' | 'userId'>): PromiseLike<Record<TSessionId, TSession>>
}
export const config = {
expire: 1000 * 60 * 60,
}

// const store = new Map<any, any>()
let store: Map<any, any>
function createStoreSingleton<TSessionId, TSession>() {
function createStoreSingleton<TSessionId extends string | symbol, TSession extends Session<string>>() {
logger.warn('Warn: You are using memory session store.')
store = new Map<TSessionId, TSession>()
}
export function createSessionStore<TSessionId, TSession>(): SessionStore<TSessionId, TSession> {
export function createSessionStore<TSessionId extends string | symbol, TSession extends Session<string>>(): SessionStore<TSessionId, TSession> {
if (!store) {
createStoreSingleton<TSessionId, TSession>()
}
Expand All @@ -44,10 +65,13 @@ export function createSessionStore<TSessionId, TSession>(): SessionStore<TSessio
async forEach(cb) {
return typedStore.forEach(cb)
},
async findAll(query) {
return Object.fromEntries([...typedStore.entries()].filter(([_, s]) => match(s).deepSome(query))) as Record<TSessionId, TSession>
},
} satisfies SessionStore<TSessionId, TSession>
}

export class SessionProvider<TSessionId, TSession extends Session> {
export class SessionProvider<TSessionId extends string | symbol, TSession extends Session<any>> {
houseKeeping: Partial<Record<'minutely' | 'hourly' | 'daily', (store: SessionStore<TSessionId, TSession>, _config: typeof config) => PromiseLike<void>>> = {
minutely: async (sessionStore) => {
sessionStore.forEach((session, sessionId) => this.removeIfExpired(session, sessionId))
Expand All @@ -66,11 +90,11 @@ export class SessionProvider<TSessionId, TSession extends Session> {
return createSessionStore<TSessionId, TSession>()
}

async create(data?: { id: string }) {
async create(data: Omit<Session, 'lastActivity'>) {
const sessionId = v4() as TSessionId

const _session = {
userId: data?.id,
...data,
lastActivity: new Date(),
} as TSession
await this.store.set(sessionId, _session)
Expand Down Expand Up @@ -100,11 +124,11 @@ export class SessionProvider<TSessionId, TSession extends Session> {
return sessionId
}

expired(session: TSession) {
expired(session: Session) {
return Date.now() - session.lastActivity.getTime() > config.expire
}

async update(sessionId: TSessionId, data: Partial<TSession>) {
async update(sessionId: TSessionId, data: Partial<Session>) {
const _session = await this.store.get(sessionId)
if (!_session) {
return undefined
Expand All @@ -118,7 +142,7 @@ export class SessionProvider<TSessionId, TSession extends Session> {
return maybeNewSessionId
}

async removeIfExpired(session: TSession, sessionId: TSessionId) {
async removeIfExpired(session: Session, sessionId: TSessionId) {
if (this.expired(session)) {
this.destroy(sessionId)
}
Expand Down
11 changes: 11 additions & 0 deletions src/server/backend/bancho.py/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer'
import type { Document } from 'bson'
import * as BSON from 'bson'
import { commandOptions } from 'redis'
import { match } from 'switch-pattern'
import { Logger } from '../log'
import { client as getRedis } from './source/redis'
import { env } from '~/server/env'
Expand Down Expand Up @@ -61,6 +62,16 @@ function createStore<TSessionKey extends string, TSession extends Session & Docu
cb(session, sessionId as TSessionKey)
}
},
async findAll(query) {
const returnValue = {} as Record<TSessionKey, TSession>

await this.forEach((val, key) => {
if (match(val).deepSome(query)) {
returnValue[key] = val
}
})
return returnValue
},
} as SessionStore<TSessionKey, TSession>
}

Expand Down
Loading

0 comments on commit 6fa0d23

Please sign in to comment.