Optimized userinfo endpoint

This commit is contained in:
LittleSheep 2024-02-01 15:08:40 +08:00
parent cfc1115b2f
commit e2b609cf43
8 changed files with 170 additions and 30 deletions

View File

@ -16,6 +16,7 @@ func RunMigration(source *gorm.DB) error {
&models.MagicToken{}, &models.MagicToken{},
&models.ThirdClient{}, &models.ThirdClient{},
&models.ActionEvent{}, &models.ActionEvent{},
&models.Notification{},
); err != nil { ); err != nil {
return err return err
} }

View File

@ -19,20 +19,21 @@ const (
type Account struct { type Account struct {
BaseModel BaseModel
Name string `json:"name" gorm:"uniqueIndex"` Name string `json:"name" gorm:"uniqueIndex"`
Nick string `json:"nick"` Nick string `json:"nick"`
Avatar string `json:"avatar"` Avatar string `json:"avatar"`
State AccountState `json:"state"` State AccountState `json:"state"`
Profile AccountProfile `json:"profile"` Profile AccountProfile `json:"profile"`
Sessions []AuthSession `json:"sessions"` Sessions []AuthSession `json:"sessions"`
Challenges []AuthChallenge `json:"challenges"` Challenges []AuthChallenge `json:"challenges"`
Factors []AuthFactor `json:"factors"` Factors []AuthFactor `json:"factors"`
Contacts []AccountContact `json:"contacts"` Contacts []AccountContact `json:"contacts"`
Events []ActionEvent `json:"events"` Events []ActionEvent `json:"events"`
MagicTokens []MagicToken `json:"-" gorm:"foreignKey:AssignTo"` MagicTokens []MagicToken `json:"-" gorm:"foreignKey:AssignTo"`
ThirdClients []ThirdClient `json:"clients"` ThirdClients []ThirdClient `json:"clients"`
ConfirmedAt *time.Time `json:"confirmed_at"` Notifications []Notification `json:"notifications" gorm:"foreignKey:RecipientID"`
Permissions datatypes.JSONType[[]string] `json:"permissions"` ConfirmedAt *time.Time `json:"confirmed_at"`
Permissions datatypes.JSONType[[]string] `json:"permissions"`
} }
func (v Account) GetPrimaryEmail() AccountContact { func (v Account) GetPrimaryEmail() AccountContact {

View File

@ -0,0 +1,13 @@
package models
import "time"
type Notification struct {
BaseModel
Subject string `json:"subject"`
Content string `json:"content"`
IsImportant bool `json:"is_important"`
ReadAt *time.Time `json:"read_at"`
RecipientID uint `json:"recipient_id"`
}

View File

@ -21,8 +21,7 @@ func getUserinfo(c *fiber.Ctx) error {
Preload("Profile"). Preload("Profile").
Preload("Contacts"). Preload("Contacts").
Preload("Factors"). Preload("Factors").
Preload("Sessions"). Preload("Notifications", "read_at IS NULL").
Preload("Challenges").
First(&data).Error; err != nil { First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error()) return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} }

View File

@ -0,0 +1,63 @@
package server
import (
"code.smartsheep.studio/hydrogen/passport/pkg/database"
"code.smartsheep.studio/hydrogen/passport/pkg/models"
"github.com/gofiber/fiber/v2"
)
func getChallenges(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
var count int64
var challenges []models.AuthChallenge
if err := database.C.
Where(&models.AuthChallenge{AccountID: user.ID}).
Model(&models.AuthChallenge{}).
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if err := database.C.
Where(&models.AuthChallenge{AccountID: user.ID}).
Limit(take).
Offset(offset).
Find(&challenges).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": challenges,
})
}
func getSessions(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
var count int64
var sessions []models.AuthSession
if err := database.C.
Where(&models.AuthSession{AccountID: user.ID}).
Model(&models.AuthSession{}).
Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if err := database.C.
Where(&models.AuthSession{AccountID: user.ID}).
Limit(take).
Offset(offset).
Find(&sessions).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": sessions,
})
}

View File

@ -63,6 +63,8 @@ func NewServer() {
api.Get("/users/me", auth, getUserinfo) api.Get("/users/me", auth, getUserinfo)
api.Put("/users/me", auth, editUserinfo) api.Put("/users/me", auth, editUserinfo)
api.Get("/users/me/events", auth, getEvents) api.Get("/users/me/events", auth, getEvents)
api.Get("/users/me/challenges", auth, getChallenges)
api.Get("/users/me/sessions", auth, getSessions)
api.Delete("/users/me/sessions/:sessionId", auth, killSession) api.Delete("/users/me/sessions/:sessionId", auth, killSession)
api.Post("/users", doRegister) api.Post("/users", doRegister)

View File

@ -1,18 +1,23 @@
import { For, Match, Switch } from "solid-js"; import { For, Match, Show, Switch } from "solid-js";
import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx"; import { clearUserinfo, useUserinfo } from "../../stores/userinfo.tsx";
import { useNavigate } from "@solidjs/router"; import { useNavigate } from "@solidjs/router";
import { useWellKnown } from "../../stores/wellKnown.tsx"; import { useWellKnown } from "../../stores/wellKnown.tsx";
interface MenuItem { interface MenuItem {
label: string; label: string;
href: string; href?: string;
children?: MenuItem[];
} }
export default function Navbar() { export default function Navbar() {
const nav: MenuItem[] = [ const nav: MenuItem[] = [
{ label: "Dashboard", href: "/" }, {
{ label: "Security", href: "/security" }, label: "You", children: [
{ label: "Personalise", href: "/personalise" } { label: "Dashboard", href: "/" },
{ label: "Security", href: "/security" },
{ label: "Personalise", href: "/personalise" }
]
}
]; ];
const wellKnown = useWellKnown(); const wellKnown = useWellKnown();
@ -52,6 +57,17 @@ export default function Navbar() {
{(item) => ( {(item) => (
<li> <li>
<a href={item.href}>{item.label}</a> <a href={item.href}>{item.label}</a>
<Show when={item.children}>
<ul class="p-2">
<For each={item.children}>
{(item) =>
<li>
<a href={item.href}>{item.label}</a>
</li>
}
</For>
</ul>
</Show>
</li> </li>
)} )}
</For> </For>
@ -66,7 +82,22 @@ export default function Navbar() {
<For each={nav}> <For each={nav}>
{(item) => ( {(item) => (
<li> <li>
<a href={item.href}>{item.label}</a> <Show when={item.children} fallback={<a href={item.href}>{item.label}</a>}>
<details>
<summary>
<a href={item.href}>{item.label}</a>
</summary>
<ul class="p-2">
<For each={item.children}>
{(item) =>
<li>
<a href={item.href}>{item.label}</a>
</li>
}
</For>
</ul>
</details>
</Show>
</li> </li>
)} )}
</For> </For>

View File

@ -1,15 +1,43 @@
import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx"; import { getAtk } from "../stores/userinfo.tsx";
import { createSignal, For, Show } from "solid-js"; import { createSignal, For, Show } from "solid-js";
export default function DashboardPage() { export default function DashboardPage() {
const userinfo = useUserinfo(); const [challenges, setChallenges] = createSignal<any[]>([]);
const [challengeCount, setChallengeCount] = createSignal(0);
const [sessions, setSessions] = createSignal<any[]>([]);
const [sessionCount, setSessionCount] = createSignal(0);
const [events, setEvents] = createSignal<any[]>([]); const [events, setEvents] = createSignal<any[]>([]);
const [eventCount, setEventCount] = createSignal(0); const [eventCount, setEventCount] = createSignal(0);
const [error, setError] = createSignal<string | null>(null); const [error, setError] = createSignal<string | null>(null);
const [submitting, setSubmitting] = createSignal(false); const [submitting, setSubmitting] = createSignal(false);
async function readChallenges() {
const res = await fetch("/api/users/me/challenges?take=10", {
headers: { Authorization: `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
setError(await res.text());
} else {
const data = await res.json();
setChallenges(data["data"]);
setChallengeCount(data["count"]);
}
}
async function readSessions() {
const res = await fetch("/api/users/me/sessions?take=10", {
headers: { Authorization: `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
setError(await res.text());
} else {
const data = await res.json();
setSessions(data["data"]);
setSessionCount(data["count"]);
}
}
async function readEvents() { async function readEvents() {
const res = await fetch("/api/users/me/events?take=10", { const res = await fetch("/api/users/me/events?take=10", {
headers: { Authorization: `Bearer ${getAtk()}` } headers: { Authorization: `Bearer ${getAtk()}` }
@ -32,12 +60,14 @@ export default function DashboardPage() {
if (res.status !== 200) { if (res.status !== 200) {
setError(await res.text()); setError(await res.text());
} else { } else {
await readProfiles(); await readSessions();
setError(null); setError(null);
} }
setSubmitting(false); setSubmitting(false);
} }
readChallenges();
readSessions();
readEvents(); readEvents();
return ( return (
@ -71,7 +101,7 @@ export default function DashboardPage() {
</svg> </svg>
</div> </div>
<div class="stat-title">Challenges</div> <div class="stat-title">Challenges</div>
<div class="stat-value">{userinfo?.meta?.challenges?.length}</div> <div class="stat-value">{challengeCount()}</div>
</div> </div>
<div class="stat"> <div class="stat">
@ -83,7 +113,7 @@ export default function DashboardPage() {
</svg> </svg>
</div> </div>
<div class="stat-title">Sessions</div> <div class="stat-title">Sessions</div>
<div class="stat-value">{userinfo?.meta?.sessions?.length}</div> <div class="stat-value">{sessionCount()}</div>
</div> </div>
<div class="stat"> <div class="stat">
@ -120,7 +150,7 @@ export default function DashboardPage() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<For each={userinfo?.meta?.challenges ?? []}> <For each={challenges()}>
{item => <tr> {item => <tr>
<th>{item.id}</th> <th>{item.id}</th>
<td>{item.state}</td> <td>{item.state}</td>
@ -155,7 +185,7 @@ export default function DashboardPage() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<For each={userinfo?.meta?.sessions ?? []}> <For each={sessions()}>
{item => <tr> {item => <tr>
<th>{item.id}</th> <th>{item.id}</th>
<td>{item.client_id ? "Linked" : "Non-linked"}</td> <td>{item.client_id ? "Linked" : "Non-linked"}</td>