Compare commits

..

No commits in common. "8c7f255473a536021b56c3fc419611287654172f" and "da15c72fb320569277b2f8a517df4cd905f0912e" have entirely different histories.

14 changed files with 307 additions and 1173 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,2 @@
#n:public
!<md> [10102, 0, null, null, -2147483648, -2147483648]
!<md> [7430, 0, null, null, -2147483648, -2147483648]

72
.idea/workspace.xml generated
View File

@ -4,14 +4,12 @@
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":bug: Fix clear function doesn't real clear items in slice">
<change afterPath="$PROJECT_DIR$/web/src/components/admin/UserFactorPanel.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25/storage_v2/_src_/database/hy_passport.gNOKQQ/schema/public.abK9xQ.meta" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources/74bcf3ef-a2b9-435b-b9e5-f32902a33b25/storage_v2/_src_/database/hy_passport.gNOKQQ/schema/public.abK9xQ.meta" afterDir="false" />
<list default="true" id="3fefb2c4-b6f9-466b-a523-53352e8d6f95" name="更改" comment=":sparkles: Admin notify one user">
<change afterPath="$PROJECT_DIR$/pkg/internal/server/admin/factors_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/web/src/components/admin/UserAssignPermsPanel.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/components/admin/UserAssignPermsPanel.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/web/src/components/admin/UserDetailPanel.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/components/admin/UserDetailPanel.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/web/src/views/admin/users.vue" beforeDir="false" afterPath="$PROJECT_DIR$/web/src/views/admin/users.vue" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/admin/index.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/admin/index.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/admin/user_api.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/admin/users_api.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/settings.toml" beforeDir="false" afterPath="$PROJECT_DIR$/settings.toml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -46,41 +44,41 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"DefaultGoTemplateProperty": "Go File",
"Go Build.Backend.executor": "Run",
"Go 构建.Backend.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.go.formatter.settings.were.checked": "true",
"RunOnceActivity.go.migrated.go.modules.settings": "true",
"RunOnceActivity.go.modules.automatic.dependencies.download": "true",
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
"git-widget-placeholder": "master",
"go.import.settings.migrated": "true",
"go.sdk.automatically.set": "true",
"last_opened_file_path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/src/components/admin",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"run.code.analysis.last.selected.profile": "pProject Default",
"settings.editor.selected.configurable": "preferences.pluginManager",
"ts.external.directory.path": "/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;DefaultGoTemplateProperty&quot;: &quot;Go File&quot;,
&quot;Go Build.Backend.executor&quot;: &quot;Run&quot;,
&quot;Go 构建.Backend.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.formatter.settings.were.checked&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.migrated.go.modules.settings&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.automatic.dependencies.download&quot;: &quot;true&quot;,
&quot;RunOnceActivity.go.modules.go.list.on.any.changes.was.set&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;go.import.settings.migrated&quot;: &quot;true&quot;,
&quot;go.sdk.automatically.set&quot;: &quot;true&quot;,
&quot;last_opened_file_path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/src/views&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;run.code.analysis.last.selected.profile&quot;: &quot;pProject Default&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;,
&quot;ts.external.directory.path&quot;: &quot;/Users/littlesheep/Documents/Projects/Hydrogen/Passport/web/node_modules/typescript/lib&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
},
"keyToStringList": {
"DatabaseDriversLRU": [
"postgresql"
&quot;keyToStringList&quot;: {
&quot;DatabaseDriversLRU&quot;: [
&quot;postgresql&quot;
]
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/web/src/components/admin" />
<recent name="$PROJECT_DIR$/web/src/views" />
<recent name="$PROJECT_DIR$/pkg/internal/server/api" />
<recent name="$PROJECT_DIR$/web" />
<recent name="$PROJECT_DIR$/pkg/services" />
<recent name="$PROJECT_DIR$/pkg/server/ui" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/web/src/views/flow" />
@ -157,6 +155,9 @@
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value=":sparkles: Last seen at" />
<MESSAGE value=":sparkles: Edit, delete current status" />
<MESSAGE value=":bug: Fix clear status affected the statutes cleared before" />
<MESSAGE value=":sparkles: Get self-current status API" />
<MESSAGE value=":sparkles: Get myself current status API" />
<MESSAGE value=":bug: Fix miscall function" />
@ -179,10 +180,7 @@
<MESSAGE value=":zap: Optimized audit, event logging system&#10;:sparkles: Audit logs&#10;:sparkles: Admin edit user permissions" />
<MESSAGE value=":sparkles: Admin force confirm account" />
<MESSAGE value=":sparkles: Admin notify one user" />
<MESSAGE value=":sparkles: Admin check users' auth factor" />
<MESSAGE value=":sparkles: Admin panel &amp; users, users' permissions management" />
<MESSAGE value=":bug: Fix clear function doesn't real clear items in slice" />
<option name="LAST_COMMIT_MESSAGE" value=":bug: Fix clear function doesn't real clear items in slice" />
<option name="LAST_COMMIT_MESSAGE" value=":sparkles: Admin notify one user" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>

View File

@ -37,12 +37,12 @@ func SaveEventChanges() {
count := len(writeEventQueue)
database.C.CreateInBatches(writeEventQueue, min(count, 1000))
log.Info().Int("count", count).Msg("Saved action events changes into database...")
writeEventQueue = nil
clear(writeEventQueue)
}
if len(writeAuditQueue) > 0 {
count := len(writeAuditQueue)
database.C.CreateInBatches(writeAuditQueue, min(count, 1000))
log.Info().Int("count", count).Msg("Saved audit records changes into database...")
writeAuditQueue = nil
clear(writeAuditQueue)
}
}

View File

@ -1,164 +0,0 @@
<template>
<v-dialog class="max-w-[720px]" :model-value="props.data != null" @update:model-value="(val) => !val && emits('close')">
<template v-slot:default="{ isActive }">
<v-card title="Assign permissions" :subtitle="`To user @${props.data?.name}`" :loading="submitting">
<v-card-text>
<v-sheet elevation="2" rounded="lg">
<v-table density="comfortable">
<thead>
<tr>
<th class="text-left">
Key
</th>
<th class="text-left">
Value
</th>
</tr>
</thead>
<tbody>
<tr
v-for="[key, val] in Object.entries(perms)"
:key="key"
>
<td class="w-1/2">
<div>
<p>{{ key }}</p>
<div class="flex mx-[-8px]">
<v-btn color="error" text="Delete" variant="plain" size="x-small"
@click="() => deleteNode(key)" />
<v-btn class="ms-[-8px]" color="info" text="Change" variant="plain" size="x-small"
@click="() => changeNodeType(key)" />
</div>
</div>
</td>
<td class="w-1/2">
<div class="w-full flex items-center">
<v-checkbox v-if="typeof val === 'boolean'" class="my-1" density="comfortable"
:hide-details="true"
v-model="perms[key]" />
<v-number-input v-else-if="typeof val === 'number'"
controlVariant="default"
:reverse="false"
:hideInput="false"
:inset="false"
class="font-mono my-2"
density="compact" :hide-details="true"
v-model="perms[key]" />
<v-text-field v-else class="font-mono my-2" density="compact" :hide-details="true"
v-model="perms[key]" />
</div>
</td>
</tr>
<tr>
<td>
<v-text-field class="my-3.5" label="Key" density="compact" variant="solo-filled"
v-model="pendingNodeKey"
:hide-details="true" />
</td>
<td>
<div class="w-full flex justify-center">
<v-btn prepend-icon="mdi-plus-circle" text="Add one" block rounded="md" @click="addNode" />
</div>
</td>
</tr>
</tbody>
</v-table>
</v-sheet>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
:disabled="submitting"
text="Cancel"
color="grey"
@click="isActive.value = false"
></v-btn>
<v-btn
:disabled="submitting"
text="Apply Changes"
@click="saveNode"
></v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
const perms = ref<any>({})
const pendingNodeKey = ref("")
const props = defineProps<{ data: any }>()
const emits = defineEmits(["close", "success", "error"])
watch(props, (v) => {
if (v.data != null) {
perms.value = v.data["perm_nodes"]
}
}, { immediate: true, deep: true })
function addNode() {
if (pendingNodeKey.value) {
perms.value[pendingNodeKey.value] = false
pendingNodeKey.value = ""
}
}
function deleteNode(key: string) {
delete perms.value[key]
}
function changeNodeType(key: string) {
const typelist = [
"boolean",
"number",
"string",
]
const idx = typelist.indexOf(typeof perms.value[key])
if (idx == -1 || idx == typelist.length - 1) {
perms.value[key] = false
return
}
switch (typelist[idx + 1]) {
case "boolean":
perms.value[key] = false
break
case "number":
perms.value[key] = 0
break
default:
perms.value[key] = ""
break
}
}
const submitting = ref(false)
async function saveNode() {
submitting.value = true
const res = await request(`/api/admin/users/${props.data.id}/permissions`, {
method: 'PUT',
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${getAtk()}`,
},
body: JSON.stringify({
'perm_nodes': perms.value,
}),
})
if (res.status !== 200) {
emits("error", await res.text())
} else {
emits("success")
emits("close")
}
submitting.value = false
}
</script>

View File

@ -1,46 +0,0 @@
<template>
<v-dialog class="max-w-[720px]" :model-value="props.data != null" @update:model-value="(val) => !val && emits('close')">
<template v-slot:default="{ isActive }">
<v-card :title="`User @${props.data?.name}`">
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<h4 class="field-title">Name</h4>
<p>{{ props.data?.name }}</p>
</v-col>
<v-col cols="12" md="6">
<h4 class="field-title">Nick</h4>
<p>{{ props.data?.nick }}</p>
</v-col>
<v-col cols="12">
<h4 class="field-title">Entire Payload</h4>
<v-code class="font-mono overflow-x-scroll max-h-[360px]">
<pre>{{ JSON.stringify(props.data, null, 4) }}</pre>
</v-code>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Close"
@click="isActive.value = false"
></v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>
<script setup lang="ts">
const props = defineProps<{ data: any }>()
const emits = defineEmits(["close"])
</script>
<style scoped>
.field-title {
font-weight: bold;
}
</style>

View File

@ -1,74 +0,0 @@
<template>
<v-dialog class="max-w-[720px]" :model-value="props.data != null"
@update:model-value="(val) => !val && emits('close')"
:loading="reverting">
<template v-slot:default="{ isActive }">
<v-card title="Auth Factors" :subtitle="`Of user @${props.data?.name}`">
<v-card-text>
<v-sheet elevation="2" rounded="lg">
<v-table density="compact">
<thead>
<tr>
<th class="text-left">
Name
</th>
<th class="text-left">
Secret
</th>
</tr>
</thead>
<tbody>
<tr
v-for="item in factors"
:key="item.name"
>
<td class="w-1/2">{{ item.id }}</td>
<td class="w-1/2"><code>{{ item.secret }}</code></td>
</tr>
</tbody>
</v-table>
</v-sheet>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
text="Close"
@click="isActive.value = false"
></v-btn>
</v-card-actions>
</v-card>
</template>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from "vue"
import { request } from "@/scripts/request"
const props = defineProps<{ data: any }>()
const emits = defineEmits(["close", "error"])
const reverting = ref(false)
const factors = ref<any[]>([])
async function load() {
reverting.value = true
const res = await request(`/api/admin/users/${props.data.id}/factors`)
if (res.status !== 200) {
emits("error", await res.text())
} else {
factors.value = await res.json()
}
reverting.value = false
}
watch(props, (v) => {
if (v.data != null) {
factors.value = []
load()
}
}, { immediate: true, deep: true })
</script>

View File

@ -3,7 +3,7 @@
<div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
<router-link :to="{ name: 'dashboard' }" class="flex gap-1 ms-0.5">
<img src="/favicon.png" alt="logo" width="27" height="24" class="icon-filter" />
<h2 class="ml-2 text-lg font-500">{{ props.title ?? "Solarpass" }}</h2>
<h2 class="ml-2 text-lg font-500">Solarpass</h2>
</router-link>
<v-spacer />
@ -23,7 +23,7 @@
</div>
</div>
<template v-if="slots.extension" #extension>
<template #extension>
<slot name="extension" />
</template>
</v-app-bar>
@ -35,13 +35,10 @@
import NotificationList from "@/components/NotificationList.vue"
import UserMenu from "@/components/UserMenu.vue"
import { useNotifications } from "@/stores/notifications"
import { ref, useSlots } from "vue"
import { ref } from "vue"
const notify = useNotifications()
const slots = useSlots()
const props = defineProps<{ title?: String }>()
const openNotify = ref(false)
</script>

View File

@ -1,30 +0,0 @@
<template>
<app-bar title="Solarpass Administration" />
<v-main>
<router-view />
</v-main>
</template>
<script setup lang="ts">
import { useUserinfo } from "@/stores/userinfo"
import { useRouter } from "vue-router"
import { onMounted } from "vue"
import AppBar from "@/components/navigation/AppBar.vue"
const id = useUserinfo()
const router = useRouter()
onMounted(async () => {
await id.readProfiles()
if (!id.userinfo.data.perm_nodes["AdminView"]) {
await router.push({ name: "dashboard" })
}
})
</script>
<style scoped>
.icon-filter {
filter: invert(100%) sepia(100%) saturate(14%) hue-rotate(212deg) brightness(104%) contrast(104%);
}
</style>

View File

@ -25,6 +25,6 @@ import Copyright from "@/components/Copyright.vue"
<style scoped>
.p-container {
max-width: 64rem;
max-width: 40rem;
}
</style>

View File

@ -1,7 +1,6 @@
import { createRouter, createWebHistory } from "vue-router"
import { useUserinfo } from "@/stores/userinfo"
import UserCenterLayout from "@/layouts/user-center.vue"
import AdministratorLayout from "@/layouts/administrator.vue"
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -75,22 +74,6 @@ const router = createRouter({
},
],
},
{
path: "/admin",
component: AdministratorLayout,
children: [
{
path: "",
name: "admin.dashboard",
component: () => import("@/views/admin/dashboard.vue"),
},
{
path: "users",
name: "admin.users",
component: () => import("@/views/admin/users.vue"),
},
]
}
],
})

View File

@ -1,51 +0,0 @@
<template>
<div class="w-full h-full flex justify-center items-center">
<v-empty-state
headline="Administration"
icon="mdi-cog"
title="What would you like to do today?"
>
<v-container>
<v-row>
<v-col cols="12" md="6">
<v-card
:to="{ name: 'admin.users' }"
prepend-icon="mdi-account-group"
text="Manage to help users do something they can't"
title="Users"
></v-card>
</v-col>
<v-col cols="12" md="6">
<v-card
disabled
prepend-icon="mdi-comment-quote"
text="Manage the content on the platform"
title="Posts & Articles"
></v-card>
</v-col>
<v-col cols="12" md="6">
<v-card
disabled
prepend-icon="mdi-file-cabinet"
text="Manage attachments on the platform"
title="Attachments"
></v-card>
</v-col>
<v-col cols="12" md="6">
<v-card
disabled
prepend-icon="mdi-ticket"
text="Solve the tickets issued by users"
title="Tickets"
></v-card>
</v-col>
</v-row>
</v-container>
</v-empty-state>
</div>
</template>
<script setup lang="ts">
</script>

View File

@ -1,138 +0,0 @@
<template>
<div>
<v-data-table-server
fixed-header
class="h-full"
density="compact"
:headers="dataDefinitions.users"
:items="users"
:items-length="pagination.total"
:loading="reverting"
v-model:items-per-page="pagination.pageSize"
@update:options="readUsers"
item-value="id"
>
<template v-slot:top>
<v-toolbar color="secondary">
<div class="max-md:px-5 md:px-12 flex flex-grow-1 items-center">
<v-btn class="me-2" icon="mdi-account-group" density="compact" :to="{ name: 'admin.dashboard' }" exact />
<h3 class="ml-2 text-lg font-500">Users</h3>
</div>
</v-toolbar>
</template>
<template v-slot:item="{ item }: { item: any }">
<tr>
<td>{{ item.id }}</td>
<td>{{ item.name }}</td>
<td>{{ item.nick }}</td>
<td>{{ new Date(item.created_at).toLocaleString() }}</td>
<td>
<v-tooltip text="Details">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="info"
icon="mdi-dots-horizontal"
@click="viewingUser = item"
/>
</template>
</v-tooltip>
<v-tooltip text="Assign Permissions">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="teal"
icon="mdi-code-block-braces"
@click="assigningPermUser = item"
/>
</template>
</v-tooltip>
<v-tooltip text="View Auth Factors">
<template #activator="{ props }">
<v-btn
v-bind="props"
variant="text"
size="x-small"
color="warning"
icon="mdi-lock"
@click="viewingFactorUser = item"
/>
</template>
</v-tooltip>
</td>
</tr>
</template>
</v-data-table-server>
<user-detail-panel :data="viewingUser" @close="viewingUser = null" />
<user-assign-perms-panel :data="assigningPermUser" @close="assigningPermUser = null"
@success="readUsers(pagination)"
@error="val => error = val" />
<user-factor-panel :data="viewingFactorUser" @close="viewingFactorUser = null" @error="val => error = val" />
<v-snackbar :timeout="3000" :model-value="error != null" @update:model-value="_ => error = null">
{{ error }}
</v-snackbar>
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue"
import { request } from "@/scripts/request"
import { getAtk } from "@/stores/userinfo"
import UserDetailPanel from "@/components/admin/UserDetailPanel.vue"
import UserAssignPermsPanel from "@/components/admin/UserAssignPermsPanel.vue"
import UserFactorPanel from "@/components/admin/UserFactorPanel.vue"
const error = ref<string | null>(null)
const users = ref<any[]>([])
const viewingUser = ref<any>(null)
const viewingFactorUser = ref<any>(null)
const assigningPermUser = ref<any>(null)
const dataDefinitions: { [id: string]: any[] } = {
users: [
{ align: "start", key: "id", title: "ID" },
{ align: "start", key: "name", title: "Name" },
{ align: "start", key: "nick", title: "Nick" },
{ align: "start", key: "created_at", title: "Created At" },
{ align: "start", key: "actions", title: "Actions", sortable: false },
],
}
const reverting = ref(true)
const pagination = reactive({
page: 1, pageSize: 5, total: 0,
})
async function readUsers({ page, itemsPerPage }: { page?: number; itemsPerPage?: number }) {
reverting.value = true
const res = await request(
"/api/admin/users?" +
new URLSearchParams({
take: pagination.pageSize.toString(),
offset: ((pagination.page - 1) * pagination.pageSize).toString(),
}),
{
headers: { Authorization: `Bearer ${getAtk()}` },
},
)
if (res.status !== 200) {
error.value = await res.text()
} else {
const data = await res.json()
users.value = data["data"]
pagination.total = data["count"]
}
reverting.value = false
}
onMounted(() => readUsers({}))
</script>

View File

@ -29,7 +29,7 @@
</td>
<td>{{ new Date(item.created_at).toLocaleString() }}</td>
<td>
<v-tooltip text="Sign Out">
<v-tooltip text="Sign out">
<template #activator="{ props }">
<v-btn
v-bind="props"