✨ User Center
This commit is contained in:
		| @@ -9,9 +9,10 @@ import ( | ||||
| 	jsoniter "github.com/json-iterator/go" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| func getPrincipal(c *fiber.Ctx) error { | ||||
| func getUserinfo(c *fiber.Ctx) error { | ||||
| 	user := c.Locals("principal").(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 { | ||||
| 	user := c.Locals("principal").(models.Account) | ||||
| 	id, _ := c.ParamsInt("sessionId", 0) | ||||
| @@ -84,10 +123,10 @@ func killSession(c *fiber.Ctx) error { | ||||
|  | ||||
| func doRegister(c *fiber.Ctx) error { | ||||
| 	var data struct { | ||||
| 		Name       string `json:"name"` | ||||
| 		Nick       string `json:"nick"` | ||||
| 		Email      string `json:"email"` | ||||
| 		Password   string `json:"password"` | ||||
| 		Name       string `json:"name" validate:"required,lowercase,alphanum,min=4,max=16"` | ||||
| 		Nick       string `json:"nick" validate:"required,min=4,max=24"` | ||||
| 		Email      string `json:"email" validate:"required,email"` | ||||
| 		Password   string `json:"password" validate:"required,min=4,max=32"` | ||||
| 		MagicToken string `json:"magic_token"` | ||||
| 	} | ||||
|  | ||||
| @@ -117,7 +156,7 @@ func doRegister(c *fiber.Ctx) error { | ||||
|  | ||||
| func doRegisterConfirm(c *fiber.Ctx) error { | ||||
| 	var data struct { | ||||
| 		Code string `json:"code"` | ||||
| 		Code string `json:"code" validate:"required"` | ||||
| 	} | ||||
|  | ||||
| 	if err := BindAndValidate(c, &data); err != nil { | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import ( | ||||
|  | ||||
| func startChallenge(c *fiber.Ctx) error { | ||||
| 	var data struct { | ||||
| 		ID string `json:"id"` | ||||
| 		ID string `json:"id" validate:"required"` | ||||
| 	} | ||||
|  | ||||
| 	if err := BindAndValidate(c, &data); err != nil { | ||||
| @@ -42,9 +42,9 @@ func startChallenge(c *fiber.Ctx) error { | ||||
|  | ||||
| func doChallenge(c *fiber.Ctx) error { | ||||
| 	var data struct { | ||||
| 		ChallengeID uint   `json:"challenge_id"` | ||||
| 		FactorID    uint   `json:"factor_id"` | ||||
| 		Secret      string `json:"secret"` | ||||
| 		ChallengeID uint   `json:"challenge_id" validate:"required"` | ||||
| 		FactorID    uint   `json:"factor_id" validate:"required"` | ||||
| 		Secret      string `json:"secret" validate:"required"` | ||||
| 	} | ||||
|  | ||||
| 	if err := BindAndValidate(c, &data); err != nil { | ||||
|   | ||||
| @@ -22,8 +22,8 @@ func NewServer() { | ||||
| 	A = fiber.New(fiber.Config{ | ||||
| 		DisableStartupMessage: true, | ||||
| 		EnableIPValidation:    true, | ||||
| 		ServerHeader:          "passport", | ||||
| 		AppName:               "passport", | ||||
| 		ServerHeader:          "Hydrogen.Passport", | ||||
| 		AppName:               "Hydrogen.Passport", | ||||
| 		ProxyHeader:           fiber.HeaderXForwardedFor, | ||||
| 		JSONEncoder:           jsoniter.ConfigCompatibleWithStandardLibrary.Marshal, | ||||
| 		JSONDecoder:           jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal, | ||||
| @@ -57,7 +57,8 @@ func NewServer() { | ||||
|  | ||||
| 	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.Delete("/users/me/sessions/:sessionId", auth, killSession) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ import ( | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| ) | ||||
|  | ||||
| var validation = validator.New() | ||||
| var validation = validator.New(validator.WithRequiredStructEnabled()) | ||||
|  | ||||
| func BindAndValidate(c *fiber.Ctx, out any) error { | ||||
| 	if err := c.BodyParser(out); err != nil { | ||||
|   | ||||
| @@ -19,6 +19,7 @@ render(() => ( | ||||
|     <UserinfoProvider> | ||||
|       <Router root={RootLayout}> | ||||
|         <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/register" component={lazy(() => import("./pages/auth/register.tsx"))} /> | ||||
|         <Route path="/auth/oauth/connect" component={lazy(() => import("./pages/auth/connect.tsx"))} /> | ||||
|   | ||||
| @@ -9,7 +9,10 @@ interface MenuItem { | ||||
| } | ||||
|  | ||||
| export default function Navbar() { | ||||
|   const nav: MenuItem[] = [{ label: "Dashboard", href: "/" }]; | ||||
|   const nav: MenuItem[] = [ | ||||
|     { label: "Dashboard", href: "/" }, | ||||
|     { label: "Personalise", href: "/personalise" } | ||||
|   ]; | ||||
|  | ||||
|   const wellKnown = useWellKnown(); | ||||
|   const userinfo = useUserinfo(); | ||||
|   | ||||
| @@ -117,8 +117,8 @@ export default function LoginPage() { | ||||
|       throw new Error(err); | ||||
|     } else { | ||||
|       const data = await res.json(); | ||||
|       new Cookie().set("access_token", data["access_token"], { path: "/" }); | ||||
|       new Cookie().set("refresh_token", data["refresh_token"], { path: "/" }); | ||||
|       new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); | ||||
|       new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined }); | ||||
|       setError(null); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -128,7 +128,7 @@ export default function DashboardPage() { | ||||
|       <div id="data-area" class="mt-5 shadow"> | ||||
|         <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"> | ||||
|               Challenges | ||||
|             </summary> | ||||
| @@ -164,7 +164,7 @@ export default function DashboardPage() { | ||||
|             </div> | ||||
|           </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"> | ||||
|               Sessions | ||||
|             </summary> | ||||
|   | ||||
							
								
								
									
										121
									
								
								pkg/view/src/pages/personalise.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								pkg/view/src/pages/personalise.tsx
									
									
									
									
									
										Normal 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> | ||||
|   ); | ||||
| } | ||||
| @@ -39,8 +39,8 @@ export async function refreshAtk() { | ||||
|     console.error(await res.text()) | ||||
|   } else { | ||||
|     const data = await res.json(); | ||||
|     new Cookie().set("access_token", data["access_token"], { path: "/" }); | ||||
|     new Cookie().set("refresh_token", data["refresh_token"], { path: "/" }); | ||||
|     new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); | ||||
|     new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -77,8 +77,8 @@ export async function readProfiles(recovering = true) { | ||||
| } | ||||
|  | ||||
| export function clearUserinfo() { | ||||
|   new Cookie().remove("access_token", { path: "/" }); | ||||
|   new Cookie().remove("refresh_token", { path: "/" }); | ||||
|   new Cookie().remove("access_token", { path: "/", maxAge: undefined }); | ||||
|   new Cookie().remove("refresh_token", { path: "/", maxAge: undefined }); | ||||
|   setUserinfo(defaultUserinfo); | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user