⚡ Optimized userinfo endpoint
This commit is contained in:
parent
cfc1115b2f
commit
e2b609cf43
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
13
pkg/models/notifications.go
Normal file
13
pkg/models/notifications.go
Normal 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"`
|
||||||
|
}
|
@ -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())
|
||||||
}
|
}
|
||||||
|
63
pkg/server/security_api.go
Normal file
63
pkg/server/security_api.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user