Skip to content

Commit

Permalink
feat: user namespace (#45)
Browse files Browse the repository at this point in the history
* feat: add user namespace & admin create api

* feat: enhanced admin panel

* feat: improved principal api

* chore: change package spec

* chore: bump versions

* chore: update
  • Loading branch information
thezzisu authored Feb 20, 2024
1 parent b57285d commit b1b5a83
Show file tree
Hide file tree
Showing 9 changed files with 119 additions and 18 deletions.
3 changes: 3 additions & 0 deletions .yarn/versions/9abdd00c.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
releases:
"@aoi-js/frontend": patch
"@aoi-js/server": patch
45 changes: 42 additions & 3 deletions apps/frontend/src/pages/admin/user.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,25 @@
<template v-slot:[`item._cap`]="{ item }">
<CapabilityChips :bits="userBits" :capability="item.capability ?? '0'" />
</template>
<template v-slot:[`item._namespace`]="{ item }">
<code>{{ item.namespace }}</code>
</template>
<template v-slot:[`item._tags`]="{ item }">
<code>{{ item.tags?.join(', ') }}</code>
</template>
<template v-slot:[`item._actions`]="{ item }">
<VBtn
icon="mdi-pencil"
icon="mdi-pencil-outline"
variant="text"
@click="openDialog(item._id, item.capability ?? '0')"
/>
<VBtn icon="mdi-cog-outline" variant="text" :to="`/user/${item._id}/settings`" />
<VBtn
v-if="appState.userCapability === '-1'"
icon="mdi-login-variant"
variant="text"
@click="loginAs.execute(item._id)"
/>
</template>
</VDataTableServer>
<VDialog v-model="dialog" width="auto">
Expand All @@ -51,18 +64,27 @@
import AppGravatar from '@/components/app/AppGravatar.vue'
import CapabilityChips from '@/components/utils/CapabilityChips.vue'
import CapabilityInput from '@/components/utils/CapabilityInput.vue'
import { useAppState } from '@/stores/app'
import { useAsyncTask } from '@/utils/async'
import { userBits } from '@/utils/capability'
import { http } from '@/utils/http'
import { http, login, logout } from '@/utils/http'
import { usePagination } from '@/utils/pagination'
import { useRouteQuery } from '@vueuse/router'
import { computed } from 'vue'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
const headers = [
{ title: 'Profile', key: 'profile', align: 'start', sortable: false },
{ title: 'ID', key: '_id' },
{ title: 'Capabilities', key: '_cap' },
{ title: 'Namespace', key: '_namespace' },
{ title: 'Tags', key: '_tags' },
{ title: 'Actions', key: '_actions' }
] as const
const search = useRouteQuery('search', '')
const {
page,
itemsPerPage,
Expand All @@ -74,7 +96,12 @@ const {
email: string
}
capability?: string
}>(`admin/user`, {})
namespace?: string
tags?: string[]
}>(
`admin/user`,
computed(() => ({ search: search.value || undefined }))
)
const dialog = ref(false)
const dialogUserId = ref('')
Expand All @@ -93,4 +120,16 @@ async function updatePrincipal() {
})
users.execute(0, page.value, itemsPerPage.value)
}
const router = useRouter()
const appState = useAppState()
const loginAs = useAsyncTask(async (userId: string) => {
const { token } = await http
.post(`admin/user/login`, { json: { userId } })
.json<{ token: string }>()
await router.replace('/')
logout()
login(token)
})
</script>
6 changes: 5 additions & 1 deletion apps/frontend/src/utils/pagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ function shallowEqual(a: Record<string, unknown>, b: Record<string, unknown>) {
return true
}

function removeUndefined(obj: Record<string, unknown>) {
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined))
}

export function usePagination<T>(
endpoint: MaybeRef<string>,
params: MaybeRef<Record<string, unknown>>
Expand All @@ -30,7 +34,7 @@ export function usePagination<T>(
page: page,
perPage: itemsPerPage,
count: cachedCount === -1,
...paramsRef.value
...removeUndefined(paramsRef.value)
}
})
.json<{ items: T[]; total: number }>()
Expand Down
4 changes: 1 addition & 3 deletions apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
"license": "AGPL-3.0-only",
"main": "lib/index.js",
"files": [
"lib",
"!lib/**/*.map",
"!lib/**/*.d.ts"
"lib"
],
"bin": {
"aoi-server": "lib/cli/index.js"
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/auth/password.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class PasswordAuthProvider extends BaseAuthProvider {
if (!PasswordLoginPayload.Check(payload)) throw new Error('invalid payload')
const { username, password } = payload
const user = await users.findOne(
{ 'profile.name': username },
{ namespace: { $exists: false }, 'profile.name': username },
{ projection: { _id: 1, authSources: 1 } }
)
if (!user || !user.authSources.password) throw new Error('user not found')
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/db/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ export interface IUser {
profile: IUserProfile
authSources: IUserAuthSources
capability?: BSON.Long
namespace?: string
tags?: string[]
}

export const users = db.collection<IUser>('users')
await users.createIndex({ 'profile.name': 1 }, { unique: true })
await users.createIndex({ namespace: 1, 'profile.name': 1 }, { unique: true })
51 changes: 49 additions & 2 deletions apps/server/src/routes/admin/user.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Type } from '@sinclair/typebox'
import { defineRoutes } from '../common/index.js'
import { BSON, Document } from 'mongodb'
import { findPaginated, users } from '../../index.js'
import { BSON, Document, UUID } from 'mongodb'
import { SUserProfile, findPaginated, users } from '../../index.js'

export const adminUserRoutes = defineRoutes(async (s) => {
s.get(
Expand Down Expand Up @@ -63,4 +63,51 @@ export const adminUserRoutes = defineRoutes(async (s) => {
return {}
}
)

s.post(
'/',
{
schema: {
description: 'Manually create a user',
body: Type.StrictObject({
profile: SUserProfile,
capability: Type.Optional(Type.String()),
namespace: Type.Optional(Type.String()),
tags: Type.Optional(Type.Array(Type.String()))
}),
response: {
200: Type.Object({
userId: Type.String()
})
}
}
},
async (req) => {
const { insertedId } = await users.insertOne({
...req.body,
_id: new UUID(),
authSources: {},
capability: req.body.capability ? new BSON.Long(req.body.capability) : undefined
})
return { userId: insertedId.toString() }
}
)

s.post(
'/login',
{
schema: {
description: 'Login as given user, bypassing authentication',
body: Type.Object({
userId: Type.UUID(),
tags: Type.Optional(Type.Array(Type.String()))
})
}
},
async (req, rep) => {
const { userId, tags } = req.body
const token = await rep.jwtSign({ userId: userId.toString(), tags }, { expiresIn: '7d' })
return { token }
}
)
})
14 changes: 10 additions & 4 deletions apps/server/src/routes/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ export const publicRoutes = defineRoutes(async (s) => {
principalId: Type.UUID(),
principalType: Type.StringEnum(['user', 'group']),
name: Type.String(),
emailHash: Type.String()
emailHash: Type.String(),
namespace: Type.Optional(Type.String()),
tags: Type.Optional(Type.Array(Type.String()))
}),
{ maxItems: 50 }
)
Expand All @@ -38,7 +40,9 @@ export const publicRoutes = defineRoutes(async (s) => {
{
projection: {
'profile.name': 1,
'profile.email': 1
'profile.email': 1,
namespace: 1,
tags: 1
}
}
)
Expand All @@ -55,11 +59,13 @@ export const publicRoutes = defineRoutes(async (s) => {
)
.toArray()
const result = [
...matchedUsers.map(({ _id, profile: { name, email } }) => ({
...matchedUsers.map(({ _id, profile: { name, email }, namespace, tags }) => ({
principalId: _id,
principalType: 'user' as const,
name,
emailHash: md5(email)
emailHash: md5(email),
namespace,
tags
})),
...matchedGroups.map(({ _id, profile: { name, email } }) => ({
principalId: _id,
Expand Down
8 changes: 5 additions & 3 deletions apps/server/src/routes/user/scoped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,21 @@ export const userScopedRoutes = defineRoutes(async (s) => {
response: {
200: Type.Object({
profile: SUserProfile,
capability: Type.Optional(Type.String())
capability: Type.Optional(Type.String()),
namespace: Type.Optional(Type.String()),
tags: Type.Optional(Type.Array(Type.String()))
})
}
}
},
async (req, rep) => {
const user = await users.findOne(
{ _id: req.inject(kUserContext)._userId },
{ projection: { profile: 1, capability: 1 } }
{ projection: { profile: 1, capability: 1, namespace: 1, tags: 1 } }
)
if (!user) return rep.notFound()
return {
profile: user.profile,
...user,
capability: user.capability?.toString()
}
}
Expand Down

0 comments on commit b1b5a83

Please sign in to comment.