User Center

This commit is contained in:
LittleSheep 2024-01-31 21:16:54 +08:00
parent e9762d5e70
commit d4e437624a
10 changed files with 188 additions and 23 deletions

View File

@ -9,9 +9,10 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
"github.com/spf13/viper" "github.com/spf13/viper"
"strconv" "strconv"
"time"
) )
func getPrincipal(c *fiber.Ctx) error { func getUserinfo(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
var data models.Account var data models.Account
@ -68,6 +69,44 @@ func getEvents(c *fiber.Ctx) error {
}) })
} }
func editUserinfo(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data struct {
Nick string `json:"nick" validate:"required,min=4,max=24"`
FirstName string `json:"first_name"`
MiddleName string `json:"middle_name"`
LastName string `json:"last_name"`
Birthday time.Time `json:"birthday"`
}
if err := BindAndValidate(c, &data); err != nil {
return err
}
var account models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
Preload("Profile").
First(&account).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
account.Nick = data.Nick
account.Profile.FirstName = data.FirstName
account.Profile.MiddleName = data.MiddleName
account.Profile.LastName = data.LastName
account.Profile.Birthday = &data.Birthday
if err := database.C.Save(&account).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else if err := database.C.Save(&account.Profile).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.SendStatus(fiber.StatusOK)
}
func killSession(c *fiber.Ctx) error { func killSession(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account) user := c.Locals("principal").(models.Account)
id, _ := c.ParamsInt("sessionId", 0) id, _ := c.ParamsInt("sessionId", 0)
@ -84,10 +123,10 @@ func killSession(c *fiber.Ctx) error {
func doRegister(c *fiber.Ctx) error { func doRegister(c *fiber.Ctx) error {
var data struct { var data struct {
Name string `json:"name"` Name string `json:"name" validate:"required,lowercase,alphanum,min=4,max=16"`
Nick string `json:"nick"` Nick string `json:"nick" validate:"required,min=4,max=24"`
Email string `json:"email"` Email string `json:"email" validate:"required,email"`
Password string `json:"password"` Password string `json:"password" validate:"required,min=4,max=32"`
MagicToken string `json:"magic_token"` MagicToken string `json:"magic_token"`
} }
@ -117,7 +156,7 @@ func doRegister(c *fiber.Ctx) error {
func doRegisterConfirm(c *fiber.Ctx) error { func doRegisterConfirm(c *fiber.Ctx) error {
var data struct { var data struct {
Code string `json:"code"` Code string `json:"code" validate:"required"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {

View File

@ -11,7 +11,7 @@ import (
func startChallenge(c *fiber.Ctx) error { func startChallenge(c *fiber.Ctx) error {
var data struct { var data struct {
ID string `json:"id"` ID string `json:"id" validate:"required"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {
@ -42,9 +42,9 @@ func startChallenge(c *fiber.Ctx) error {
func doChallenge(c *fiber.Ctx) error { func doChallenge(c *fiber.Ctx) error {
var data struct { var data struct {
ChallengeID uint `json:"challenge_id"` ChallengeID uint `json:"challenge_id" validate:"required"`
FactorID uint `json:"factor_id"` FactorID uint `json:"factor_id" validate:"required"`
Secret string `json:"secret"` Secret string `json:"secret" validate:"required"`
} }
if err := BindAndValidate(c, &data); err != nil { if err := BindAndValidate(c, &data); err != nil {

View File

@ -22,8 +22,8 @@ func NewServer() {
A = fiber.New(fiber.Config{ A = fiber.New(fiber.Config{
DisableStartupMessage: true, DisableStartupMessage: true,
EnableIPValidation: true, EnableIPValidation: true,
ServerHeader: "passport", ServerHeader: "Hydrogen.Passport",
AppName: "passport", AppName: "Hydrogen.Passport",
ProxyHeader: fiber.HeaderXForwardedFor, ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal, JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal, JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
@ -57,7 +57,8 @@ func NewServer() {
api := A.Group("/api").Name("API") api := A.Group("/api").Name("API")
{ {
api.Get("/users/me", auth, getPrincipal) api.Get("/users/me", auth, getUserinfo)
api.Put("/users/me", auth, editUserinfo)
api.Get("/users/me/events", auth, getEvents) api.Get("/users/me/events", auth, getEvents)
api.Delete("/users/me/sessions/:sessionId", auth, killSession) api.Delete("/users/me/sessions/:sessionId", auth, killSession)

View File

@ -5,7 +5,7 @@ import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
var validation = validator.New() var validation = validator.New(validator.WithRequiredStructEnabled())
func BindAndValidate(c *fiber.Ctx, out any) error { func BindAndValidate(c *fiber.Ctx, out any) error {
if err := c.BodyParser(out); err != nil { if err := c.BodyParser(out); err != nil {

View File

@ -19,6 +19,7 @@ render(() => (
<UserinfoProvider> <UserinfoProvider>
<Router root={RootLayout}> <Router root={RootLayout}>
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} /> <Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
<Route path="/personalise" component={lazy(() => import("./pages/personalise.tsx"))} />
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} /> <Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
<Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} /> <Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
<Route path="/auth/oauth/connect" component={lazy(() => import("./pages/auth/connect.tsx"))} /> <Route path="/auth/oauth/connect" component={lazy(() => import("./pages/auth/connect.tsx"))} />

View File

@ -9,7 +9,10 @@ interface MenuItem {
} }
export default function Navbar() { export default function Navbar() {
const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }]; const nav: MenuItem[] = [
{ label: "Dashboard", href: "/" },
{ label: "Personalise", href: "/personalise" }
];
const wellKnown = useWellKnown(); const wellKnown = useWellKnown();
const userinfo = useUserinfo(); const userinfo = useUserinfo();

View File

@ -117,8 +117,8 @@ export default function LoginPage() {
throw new Error(err); throw new Error(err);
} else { } else {
const data = await res.json(); const data = await res.json();
new Cookie().set("access_token", data["access_token"], { path: "/" }); new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined });
new Cookie().set("refresh_token", data["refresh_token"], { path: "/" }); new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined });
setError(null); setError(null);
} }
} }

View File

@ -128,7 +128,7 @@ export default function DashboardPage() {
<div id="data-area" class="mt-5 shadow"> <div id="data-area" class="mt-5 shadow">
<div class="join join-vertical w-full"> <div class="join join-vertical w-full">
<details class="collapse collapse-plus join-item"> <details class="collapse collapse-plus join-item border-b border-base-200">
<summary class="collapse-title text-lg font-medium"> <summary class="collapse-title text-lg font-medium">
Challenges Challenges
</summary> </summary>
@ -164,7 +164,7 @@ export default function DashboardPage() {
</div> </div>
</details> </details>
<details class="collapse collapse-plus join-item"> <details class="collapse collapse-plus join-item border-b border-base-200">
<summary class="collapse-title text-lg font-medium"> <summary class="collapse-title text-lg font-medium">
Sessions Sessions
</summary> </summary>

View File

@ -0,0 +1,121 @@
import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx";
import { createSignal, Show } from "solid-js";
export default function PersonalPage() {
const userinfo = useUserinfo();
const [error, setError] = createSignal<null | string>(null);
const [success, setSuccess] = createSignal<null | string>(null);
const [loading, setLoading] = createSignal(false);
async function update(evt: SubmitEvent) {
evt.preventDefault();
const data = Object.fromEntries(new FormData(evt.target as HTMLFormElement));
setLoading(true);
const res = await fetch("/api/users/me", {
method: "PUT",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${getAtk()}`
},
body: JSON.stringify(data)
});
if (res.status !== 200) {
setSuccess(null);
setError(await res.text());
} else {
await readProfiles();
setSuccess("Your basic information has been update.");
setError(null);
}
setLoading(false);
}
return (
<div class="max-w-[720px] mx-auto px-5 pt-12">
<div class="px-5">
<h1 class="text-2xl font-bold">{userinfo?.displayName}</h1>
<p>Joined at {new Date(userinfo?.meta?.created_at).toLocaleString()}</p>
</div>
<div class="card shadow mt-5">
<div class="card-body">
<Show when={error()}>
<div id="alerts">
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="capitalize">{error()}</span>
</div>
</div>
</Show>
<Show when={success()}>
<div id="alerts">
<div role="alert" class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="capitalize">{success()}</span>
</div>
</div>
</Show>
<form class="grid grid-cols-1 gap-2" onSubmit={update}>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Username</span>
</div>
<input value={userinfo?.meta?.name} name="name" type="text" placeholder="Type here"
class="input input-bordered w-full" disabled />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Nickname</span>
</div>
<input value={userinfo?.meta?.nick} name="nick" type="text" placeholder="Type here"
class="input input-bordered w-full" />
</label>
<div class="grid grid-cols-1 md:grid-cols-3 gap-x-4">
<label class="form-control w-full">
<div class="label">
<span class="label-text">First Name</span>
</div>
<input value={userinfo?.meta?.profile?.first_name} name="first_name" type="text"
placeholder="Type here" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Middle Name</span>
</div>
<input value={userinfo?.meta?.profile?.middle_name} name="middle_name" type="text"
placeholder="Type here" class="input input-bordered w-full" />
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Last Name</span>
</div>
<input value={userinfo?.meta?.profile?.last_name} name="last_name" type="text"
placeholder="Type here" class="input input-bordered w-full" />
</label>
</div>
<button type="submit" class="btn btn-primary btn-block mt-5" disabled={loading()}>
<Show when={loading()} fallback={"Save changes"}>
<span class="loading loading-spinner"></span>
</Show>
</button>
</form>
</div>
</div>
</div>
);
}

View File

@ -39,8 +39,8 @@ export async function refreshAtk() {
console.error(await res.text()) console.error(await res.text())
} else { } else {
const data = await res.json(); const data = await res.json();
new Cookie().set("access_token", data["access_token"], { path: "/" }); new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined });
new Cookie().set("refresh_token", data["refresh_token"], { path: "/" }); new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined });
} }
} }
@ -77,8 +77,8 @@ export async function readProfiles(recovering = true) {
} }
export function clearUserinfo() { export function clearUserinfo() {
new Cookie().remove("access_token", { path: "/" }); new Cookie().remove("access_token", { path: "/", maxAge: undefined });
new Cookie().remove("refresh_token", { path: "/" }); new Cookie().remove("refresh_token", { path: "/", maxAge: undefined });
setUserinfo(defaultUserinfo); setUserinfo(defaultUserinfo);
} }