diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go index 412b01e..d7b02b0 100644 --- a/pkg/database/migrator.go +++ b/pkg/database/migrator.go @@ -16,6 +16,7 @@ func RunMigration(source *gorm.DB) error { &models.MagicToken{}, &models.ThirdClient{}, &models.ActionEvent{}, + &models.Notification{}, ); err != nil { return err } diff --git a/pkg/models/accounts.go b/pkg/models/accounts.go index 0645b54..26663f6 100644 --- a/pkg/models/accounts.go +++ b/pkg/models/accounts.go @@ -19,20 +19,21 @@ const ( type Account struct { BaseModel - Name string `json:"name" gorm:"uniqueIndex"` - Nick string `json:"nick"` - Avatar string `json:"avatar"` - State AccountState `json:"state"` - Profile AccountProfile `json:"profile"` - Sessions []AuthSession `json:"sessions"` - Challenges []AuthChallenge `json:"challenges"` - Factors []AuthFactor `json:"factors"` - Contacts []AccountContact `json:"contacts"` - Events []ActionEvent `json:"events"` - MagicTokens []MagicToken `json:"-" gorm:"foreignKey:AssignTo"` - ThirdClients []ThirdClient `json:"clients"` - ConfirmedAt *time.Time `json:"confirmed_at"` - Permissions datatypes.JSONType[[]string] `json:"permissions"` + Name string `json:"name" gorm:"uniqueIndex"` + Nick string `json:"nick"` + Avatar string `json:"avatar"` + State AccountState `json:"state"` + Profile AccountProfile `json:"profile"` + Sessions []AuthSession `json:"sessions"` + Challenges []AuthChallenge `json:"challenges"` + Factors []AuthFactor `json:"factors"` + Contacts []AccountContact `json:"contacts"` + Events []ActionEvent `json:"events"` + MagicTokens []MagicToken `json:"-" gorm:"foreignKey:AssignTo"` + ThirdClients []ThirdClient `json:"clients"` + Notifications []Notification `json:"notifications" gorm:"foreignKey:RecipientID"` + ConfirmedAt *time.Time `json:"confirmed_at"` + Permissions datatypes.JSONType[[]string] `json:"permissions"` } func (v Account) GetPrimaryEmail() AccountContact { diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go new file mode 100644 index 0000000..9eed3ad --- /dev/null +++ b/pkg/models/notifications.go @@ -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"` +} diff --git a/pkg/server/accounts_api.go b/pkg/server/accounts_api.go index 0360358..c405d2d 100644 --- a/pkg/server/accounts_api.go +++ b/pkg/server/accounts_api.go @@ -21,8 +21,7 @@ func getUserinfo(c *fiber.Ctx) error { Preload("Profile"). Preload("Contacts"). Preload("Factors"). - Preload("Sessions"). - Preload("Challenges"). + Preload("Notifications", "read_at IS NULL"). First(&data).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, err.Error()) } diff --git a/pkg/server/security_api.go b/pkg/server/security_api.go new file mode 100644 index 0000000..06d987b --- /dev/null +++ b/pkg/server/security_api.go @@ -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, + }) +} diff --git a/pkg/server/startup.go b/pkg/server/startup.go index 233cfe7..8e37bca 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -63,6 +63,8 @@ func NewServer() { api.Get("/users/me", auth, getUserinfo) api.Put("/users/me", auth, editUserinfo) 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.Post("/users", doRegister) diff --git a/pkg/view/src/layouts/shared/Navbar.tsx b/pkg/view/src/layouts/shared/Navbar.tsx index 50fd0ba..686a46e 100644 --- a/pkg/view/src/layouts/shared/Navbar.tsx +++ b/pkg/view/src/layouts/shared/Navbar.tsx @@ -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 { useNavigate } from "@solidjs/router"; import { useWellKnown } from "../../stores/wellKnown.tsx"; interface MenuItem { label: string; - href: string; + href?: string; + children?: MenuItem[]; } export default function Navbar() { const nav: MenuItem[] = [ - { label: "Dashboard", href: "/" }, - { label: "Security", href: "/security" }, - { label: "Personalise", href: "/personalise" } + { + label: "You", children: [ + { label: "Dashboard", href: "/" }, + { label: "Security", href: "/security" }, + { label: "Personalise", href: "/personalise" } + ] + } ]; const wellKnown = useWellKnown(); @@ -52,6 +57,17 @@ export default function Navbar() { {(item) => (
  • {item.label} + + +
  • )} @@ -66,7 +82,22 @@ export default function Navbar() { {(item) => (
  • - {item.label} + {item.label}}> +
    + + {item.label} + + +
    +
  • )}
    diff --git a/pkg/view/src/pages/security.tsx b/pkg/view/src/pages/security.tsx index 6efb470..e1a3205 100644 --- a/pkg/view/src/pages/security.tsx +++ b/pkg/view/src/pages/security.tsx @@ -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"; export default function DashboardPage() { - const userinfo = useUserinfo(); - + const [challenges, setChallenges] = createSignal([]); + const [challengeCount, setChallengeCount] = createSignal(0); + const [sessions, setSessions] = createSignal([]); + const [sessionCount, setSessionCount] = createSignal(0); const [events, setEvents] = createSignal([]); const [eventCount, setEventCount] = createSignal(0); const [error, setError] = createSignal(null); 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() { const res = await fetch("/api/users/me/events?take=10", { headers: { Authorization: `Bearer ${getAtk()}` } @@ -32,12 +60,14 @@ export default function DashboardPage() { if (res.status !== 200) { setError(await res.text()); } else { - await readProfiles(); + await readSessions(); setError(null); } setSubmitting(false); } + readChallenges(); + readSessions(); readEvents(); return ( @@ -71,7 +101,7 @@ export default function DashboardPage() {
    Challenges
    -
    {userinfo?.meta?.challenges?.length}
    +
    {challengeCount()}
    @@ -83,7 +113,7 @@ export default function DashboardPage() {
    Sessions
    -
    {userinfo?.meta?.sessions?.length}
    +
    {sessionCount()}
    @@ -120,7 +150,7 @@ export default function DashboardPage() { - + {item => {item.id} {item.state} @@ -155,7 +185,7 @@ export default function DashboardPage() { - + {item => {item.id} {item.client_id ? "Linked" : "Non-linked"}