✨ Real feel-less refresh token
This commit is contained in:
		| @@ -7,20 +7,12 @@ import ( | ||||
| 	"time" | ||||
| ) | ||||
|  | ||||
| type AccountState = int8 | ||||
|  | ||||
| const ( | ||||
| 	PendingAccountState = AccountState(iota) | ||||
| 	ActiveAccountState | ||||
| ) | ||||
|  | ||||
| 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"` | ||||
|   | ||||
| @@ -2,6 +2,8 @@ package security | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/google/uuid" | ||||
| 	"strings" | ||||
| 	"time" | ||||
|  | ||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/database" | ||||
| @@ -83,5 +85,11 @@ func DoChallenge(challenge models.AuthChallenge, factor models.AuthFactor, code | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// Revoke some factor passwords | ||||
| 	if factor.Type == models.EmailPasswordFactor { | ||||
| 		factor.Secret = strings.ReplaceAll(uuid.NewString(), "-", "") | ||||
| 		database.C.Save(&factor) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -2,12 +2,16 @@ package security | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/golang-jwt/jwt/v5" | ||||
| 	"github.com/spf13/viper" | ||||
| ) | ||||
|  | ||||
| var CookieAccessKey = "identity_auth_key" | ||||
| var CookieRefreshKey = "identity_refresh_key" | ||||
|  | ||||
| type PayloadClaims struct { | ||||
| 	jwt.RegisteredClaims | ||||
|  | ||||
| @@ -56,3 +60,22 @@ func DecodeJwt(str string) (PayloadClaims, error) { | ||||
| 		return claims, fmt.Errorf("unexpected token payload: not payload claims type") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func SetJwtCookieSet(c *fiber.Ctx, access, refresh string) { | ||||
| 	c.Cookie(&fiber.Cookie{ | ||||
| 		Name:     CookieAccessKey, | ||||
| 		Value:    access, | ||||
| 		Domain:   viper.GetString("security.cookie_domain"), | ||||
| 		SameSite: viper.GetString("security.cookie_samesite"), | ||||
| 		Expires:  time.Now().Add(60 * time.Minute), | ||||
| 		Path:     "/", | ||||
| 	}) | ||||
| 	c.Cookie(&fiber.Cookie{ | ||||
| 		Name:     CookieRefreshKey, | ||||
| 		Value:    refresh, | ||||
| 		Domain:   viper.GetString("security.cookie_domain"), | ||||
| 		SameSite: viper.GetString("security.cookie_samesite"), | ||||
| 		Expires:  time.Now().Add(24 * 30 * time.Hour), | ||||
| 		Path:     "/", | ||||
| 	}) | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package security | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"strconv" | ||||
| 	"time" | ||||
|  | ||||
| @@ -84,15 +85,17 @@ func GetToken(session models.AuthSession) (string, string, error) { | ||||
| 		return refresh, access, err | ||||
| 	} | ||||
|  | ||||
| 	var err error | ||||
| 	accessDuration := time.Duration(viper.GetInt64("security.access_token_duration")) * time.Second | ||||
| 	refreshDuration := time.Duration(viper.GetInt64("security.refresh_token_duration")) * time.Second | ||||
|  | ||||
| 	var err error | ||||
| 	sub := strconv.Itoa(int(session.AccountID)) | ||||
| 	sed := strconv.Itoa(int(session.ID)) | ||||
| 	access, err = EncodeJwt(session.AccessToken, JwtAccessType, sub, sed, session.Audiences, time.Now().Add(30*time.Minute)) | ||||
| 	access, err = EncodeJwt(session.AccessToken, JwtAccessType, sub, sed, session.Audiences, time.Now().Add(accessDuration)) | ||||
| 	if err != nil { | ||||
| 		return refresh, access, err | ||||
| 	} | ||||
| 	refresh, err = EncodeJwt(session.RefreshToken, JwtRefreshType, sub, sed, session.Audiences, time.Now().Add(30*24*time.Hour)) | ||||
| 	refresh, err = EncodeJwt(session.RefreshToken, JwtRefreshType, sub, sed, session.Audiences, time.Now().Add(refreshDuration)) | ||||
| 	if err != nil { | ||||
| 		return refresh, access, err | ||||
| 	} | ||||
| @@ -153,5 +156,9 @@ func RefreshToken(token string) (string, string, error) { | ||||
| 		return "404", "403", err | ||||
| 	} | ||||
|  | ||||
| 	if session, err := RegenSession(session); err != nil { | ||||
| 		return "404", "403", err | ||||
| 	} else { | ||||
| 		return GetToken(session) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -137,13 +137,13 @@ func doRegister(c *fiber.Ctx) error { | ||||
| 		return err | ||||
| 	} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 { | ||||
| 		return fmt.Errorf("missing magic token in request") | ||||
| 	} | ||||
|  | ||||
| 	} else if viper.GetBool("use_registration_magic_token") { | ||||
| 		if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil { | ||||
| 			return err | ||||
| 		} else { | ||||
| 			database.C.Delete(&tk) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if user, err := services.CreateAccount( | ||||
| 		data.Name, | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/security" | ||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/services" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"github.com/gofiber/fiber/v2/middleware/keyauth" | ||||
| ) | ||||
|  | ||||
| var auth = keyauth.New(keyauth.Config{ | ||||
| 	KeyLookup:  "header:Authorization", | ||||
| 	AuthScheme: "Bearer", | ||||
| 	Validator: func(c *fiber.Ctx, token string) (bool, error) { | ||||
| 		claims, err := security.DecodeJwt(token) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
|  | ||||
| 		session, err := services.LookupSessionWithToken(claims.ID) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} else if err := session.IsAvailable(); err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
|  | ||||
| 		user, err := services.GetAccount(session.AccountID) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
|  | ||||
| 		c.Locals("principal", user) | ||||
|  | ||||
| 		return true, nil | ||||
| 	}, | ||||
| 	ContextKey: "token", | ||||
| }) | ||||
							
								
								
									
										72
									
								
								pkg/server/auth_middleware.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								pkg/server/auth_middleware.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| package server | ||||
|  | ||||
| import ( | ||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/security" | ||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/services" | ||||
| 	"fmt" | ||||
| 	"github.com/gofiber/fiber/v2" | ||||
| 	"strings" | ||||
| ) | ||||
|  | ||||
| func authMiddleware(c *fiber.Ctx) error { | ||||
| 	var token string | ||||
| 	if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 { | ||||
| 		token = cookie | ||||
| 	} | ||||
| 	if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 { | ||||
| 		tk := strings.Replace(header, "Bearer", "", 1) | ||||
| 		token = strings.TrimSpace(tk) | ||||
| 	} | ||||
|  | ||||
| 	c.Locals("token", token) | ||||
|  | ||||
| 	if err := authFunc(c); err != nil { | ||||
| 		fmt.Println(err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return c.Next() | ||||
| } | ||||
|  | ||||
| func authFunc(c *fiber.Ctx, overrides ...string) error { | ||||
| 	var token string | ||||
| 	if len(overrides) > 0 { | ||||
| 		token = overrides[0] | ||||
| 	} else { | ||||
| 		if tk, ok := c.Locals("token").(string); !ok { | ||||
| 			return fiber.NewError(fiber.StatusUnauthorized) | ||||
| 		} else { | ||||
| 			token = tk | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	claims, err := security.DecodeJwt(token) | ||||
| 	if err != nil { | ||||
| 		rtk := c.Cookies(security.CookieRefreshKey) | ||||
| 		if len(rtk) > 0 && len(overrides) < 1 { | ||||
| 			// Auto refresh and retry | ||||
| 			access, refresh, err := security.RefreshToken(rtk) | ||||
| 			if err == nil { | ||||
| 				security.SetJwtCookieSet(c, access, refresh) | ||||
| 				return authFunc(c, access) | ||||
| 			} | ||||
| 		} | ||||
| 		return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("invalid auth key: %v", err)) | ||||
| 	} | ||||
|  | ||||
| 	session, err := services.LookupSessionWithToken(claims.ID) | ||||
| 	if err != nil { | ||||
| 		return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("invalid auth session: %v", err)) | ||||
| 	} else if err := session.IsAvailable(); err != nil { | ||||
| 		return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("unavailable auth session: %v", err)) | ||||
| 	} | ||||
|  | ||||
| 	user, err := services.GetAccount(session.AccountID) | ||||
| 	if err != nil { | ||||
| 		return fiber.NewError(fiber.StatusUnauthorized, fmt.Sprintf("invalid account: %v", err)) | ||||
| 	} | ||||
|  | ||||
| 	c.Locals("principal", user) | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -127,6 +127,8 @@ func exchangeToken(c *fiber.Ctx) error { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type") | ||||
| 	} | ||||
|  | ||||
| 	security.SetJwtCookieSet(c, access, refresh) | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"id_token":      access, | ||||
| 		"access_token":  access, | ||||
|   | ||||
| @@ -58,18 +58,18 @@ func NewServer() { | ||||
| 	api := A.Group("/api").Name("API") | ||||
| 	{ | ||||
| 		api.Get("/avatar/:avatarId", getAvatar) | ||||
| 		api.Put("/avatar", auth, setAvatar) | ||||
| 		api.Put("/avatar", authMiddleware, setAvatar) | ||||
|  | ||||
| 		api.Get("/notifications", auth, getNotifications) | ||||
| 		api.Put("/notifications/:notificationId/read", auth, markNotificationRead) | ||||
| 		api.Post("/notifications/subscribe", auth, addNotifySubscriber) | ||||
| 		api.Get("/notifications", authMiddleware, getNotifications) | ||||
| 		api.Put("/notifications/:notificationId/read", authMiddleware, markNotificationRead) | ||||
| 		api.Post("/notifications/subscribe", authMiddleware, addNotifySubscriber) | ||||
|  | ||||
| 		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.Get("/users/me", authMiddleware, getUserinfo) | ||||
| 		api.Put("/users/me", authMiddleware, editUserinfo) | ||||
| 		api.Get("/users/me/events", authMiddleware, getEvents) | ||||
| 		api.Get("/users/me/challenges", authMiddleware, getChallenges) | ||||
| 		api.Get("/users/me/sessions", authMiddleware, getSessions) | ||||
| 		api.Delete("/users/me/sessions/:sessionId", authMiddleware, killSession) | ||||
|  | ||||
| 		api.Post("/users", doRegister) | ||||
| 		api.Post("/users/me/confirm", doRegisterConfirm) | ||||
| @@ -79,8 +79,8 @@ func NewServer() { | ||||
| 		api.Post("/auth/token", exchangeToken) | ||||
| 		api.Post("/auth/factors/:factorId", requestFactorToken) | ||||
|  | ||||
| 		api.Get("/auth/o/connect", auth, preConnect) | ||||
| 		api.Post("/auth/o/connect", auth, doConnect) | ||||
| 		api.Get("/auth/o/connect", authMiddleware, preConnect) | ||||
| 		api.Post("/auth/o/connect", authMiddleware, doConnect) | ||||
|  | ||||
| 		developers := api.Group("/dev").Name("Developers API") | ||||
| 		{ | ||||
|   | ||||
| @@ -21,8 +21,8 @@ func getOidcConfiguration(c *fiber.Ctx) error { | ||||
|  | ||||
| 	return c.JSON(fiber.Map{ | ||||
| 		"issuer":                                           basepath, | ||||
| 		"authorization_endpoint":                           fmt.Sprintf("%s/auth/o/connect", basepath), | ||||
| 		"token_endpoint":                                   fmt.Sprintf("%s/api/auth/token", basepath), | ||||
| 		"authorization_endpoint":                           fmt.Sprintf("%s/authMiddleware/o/connect", basepath), | ||||
| 		"token_endpoint":                                   fmt.Sprintf("%s/api/authMiddleware/token", basepath), | ||||
| 		"userinfo_endpoint":                                fmt.Sprintf("%s/api/users/me", basepath), | ||||
| 		"response_types_supported":                         []string{"code", "token"}, | ||||
| 		"grant_types_supported":                            []string{"authorization_code", "implicit", "refresh_token"}, | ||||
|   | ||||
| @@ -45,7 +45,6 @@ func CreateAccount(name, nick, email, password string) (models.Account, error) { | ||||
| 	user := models.Account{ | ||||
| 		Name: name, | ||||
| 		Nick: nick, | ||||
| 		State: models.PendingAccountState, | ||||
| 		Profile: models.AccountProfile{ | ||||
| 			Experience: 100, | ||||
| 		}, | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
|     "preview": "vite preview" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@fortawesome/fontawesome-free": "^6.5.1", | ||||
|     "@solidjs/router": "^0.10.10", | ||||
|     "solid-js": "^1.8.7", | ||||
|     "universal-cookie": "^7.0.2" | ||||
|   | ||||
| @@ -8,6 +8,8 @@ import "./assets/fonts/fonts.css"; | ||||
| import { lazy } from "solid-js"; | ||||
| import { Route, Router } from "@solidjs/router"; | ||||
|  | ||||
| import "@fortawesome/fontawesome-free/css/all.min.css"; | ||||
|  | ||||
| import RootLayout from "./layouts/RootLayout.tsx"; | ||||
| import { UserinfoProvider } from "./stores/userinfo.tsx"; | ||||
| import { WellKnownProvider } from "./stores/wellKnown.tsx"; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import Navbar from "./shared/Navbar.tsx"; | ||||
| import Navigatior from "./shared/Navigatior.tsx"; | ||||
| import { readProfiles, useUserinfo } from "../stores/userinfo.tsx"; | ||||
| import { createEffect, createMemo, createSignal, Show } from "solid-js"; | ||||
| import { readWellKnown } from "../stores/wellKnown.tsx"; | ||||
| @@ -52,7 +52,7 @@ export default function RootLayout(props: any) { | ||||
|       </div> | ||||
|     }> | ||||
|       <Show when={!searchParams["embedded"]}> | ||||
|         <Navbar /> | ||||
|         <Navigatior /> | ||||
|       </Show> | ||||
|  | ||||
|       <main class={`${mainContentStyles()} px-5`}>{props.children}</main> | ||||
|   | ||||
| @@ -8,7 +8,7 @@ interface MenuItem { | ||||
|   children?: MenuItem[]; | ||||
| } | ||||
| 
 | ||||
| export default function Navbar() { | ||||
| export default function Navigatior() { | ||||
|   const nav: MenuItem[] = [ | ||||
|     { | ||||
|       label: "You", children: [ | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { readProfiles } from "../../stores/userinfo.tsx"; | ||||
| import { useNavigate, useSearchParams } from "@solidjs/router"; | ||||
| import { createSignal, For, Match, Show, Switch } from "solid-js"; | ||||
| import Cookie from "universal-cookie"; | ||||
|  | ||||
| export default function LoginPage() { | ||||
|   const [title, setTitle] = createSignal("Sign in"); | ||||
| @@ -116,9 +115,6 @@ export default function LoginPage() { | ||||
|       setError(err); | ||||
|       throw new Error(err); | ||||
|     } else { | ||||
|       const data = await res.json(); | ||||
|       new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); | ||||
|       new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined }); | ||||
|       setError(null); | ||||
|     } | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { getAtk } from "../stores/userinfo.tsx"; | ||||
| import { createSignal, For, Show } from "solid-js"; | ||||
| import { createSignal, For, Match, Show, Switch } from "solid-js"; | ||||
|  | ||||
| export default function DashboardPage() { | ||||
|   const [challenges, setChallenges] = createSignal<any[]>([]); | ||||
| @@ -12,6 +12,8 @@ export default function DashboardPage() { | ||||
|   const [error, setError] = createSignal<string | null>(null); | ||||
|   const [submitting, setSubmitting] = createSignal(false); | ||||
|  | ||||
|   const [contentTab, setContentTab] = createSignal(0); | ||||
|  | ||||
|   async function readChallenges() { | ||||
|     const res = await fetch("/api/users/me/challenges?take=10", { | ||||
|       headers: { Authorization: `Bearer ${getAtk()}` } | ||||
| @@ -94,11 +96,7 @@ export default function DashboardPage() { | ||||
|         <div class="stats shadow w-full"> | ||||
|           <div class="stat"> | ||||
|             <div class="stat-figure text-secondary"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" | ||||
|                    class="inline-block w-8 h-8 stroke-current"> | ||||
|                 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||
|                       d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path> | ||||
|               </svg> | ||||
|               <i class="fa-solid fa-door-open inline-block text-[28px] w-8 h-8 stroke-current"></i> | ||||
|             </div> | ||||
|             <div class="stat-title">Challenges</div> | ||||
|             <div class="stat-value">{challengeCount()}</div> | ||||
| @@ -106,11 +104,7 @@ export default function DashboardPage() { | ||||
|  | ||||
|           <div class="stat"> | ||||
|             <div class="stat-figure text-secondary"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" | ||||
|                    class="inline-block w-8 h-8 stroke-current"> | ||||
|                 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||
|                       d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path> | ||||
|               </svg> | ||||
|               <i class="fa-solid fa-key inline-block text-[28px] w-8 h-8 stroke-current"></i> | ||||
|             </div> | ||||
|             <div class="stat-title">Sessions</div> | ||||
|             <div class="stat-value">{sessionCount()}</div> | ||||
| @@ -118,11 +112,7 @@ export default function DashboardPage() { | ||||
|  | ||||
|           <div class="stat"> | ||||
|             <div class="stat-figure text-secondary"> | ||||
|               <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" | ||||
|                    class="inline-block w-8 h-8 stroke-current"> | ||||
|                 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||
|                       d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4"></path> | ||||
|               </svg> | ||||
|               <i class="fa-solid fa-person-walking inline-block text-[28px] w-8 h-8 stroke-current"></i> | ||||
|             </div> | ||||
|             <div class="stat-title">Events</div> | ||||
|             <div class="stat-value">{eventCount()}</div> | ||||
| @@ -130,14 +120,20 @@ export default function DashboardPage() { | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div id="data-area" class="mt-5 shadow"> | ||||
|         <div class="join join-vertical w-full"> | ||||
|       <div id="switch-area" class="mt-5"> | ||||
|         <div role="tablist" class="tabs tabs-boxed"> | ||||
|           <input type="radio" name="content-switch" role="tab" class="tab" aria-label="Challenges" | ||||
|                  checked={contentTab() === 0} onChange={() => setContentTab(0)} /> | ||||
|           <input type="radio" name="content-switch" role="tab" class="tab" aria-label="Sessions" | ||||
|                  checked={contentTab() === 1} onChange={() => setContentTab(1)} /> | ||||
|           <input type="radio" name="content-switch" role="tab" class="tab" aria-label="Events" | ||||
|                  checked={contentTab() === 2} onChange={() => setContentTab(2)} /> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|           <details class="collapse collapse-plus join-item border-b border-base-200"> | ||||
|             <summary class="collapse-title text-lg font-medium"> | ||||
|               Challenges | ||||
|             </summary> | ||||
|             <div class="collapse-content mx-[-16px]"> | ||||
|       <div id="data-area" class="mt-5 shadow"> | ||||
|         <Switch> | ||||
|           <Match when={contentTab() === 0}> | ||||
|             <div class="overflow-x-auto"> | ||||
|               <table class="table"> | ||||
|                 <thead> | ||||
| @@ -166,14 +162,10 @@ export default function DashboardPage() { | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </div> | ||||
|             </div> | ||||
|           </details> | ||||
|           </Match> | ||||
|  | ||||
|           <details class="collapse collapse-plus join-item border-b border-base-200"> | ||||
|             <summary class="collapse-title text-lg font-medium"> | ||||
|               Sessions | ||||
|             </summary> | ||||
|             <div class="collapse-content mx-[-16px]"> | ||||
|           <Match when={contentTab() === 1}> | ||||
|             <div class="overflow-x-auto"> | ||||
|               <table class="table"> | ||||
|                 <thead> | ||||
|                 <tr> | ||||
| @@ -192,13 +184,8 @@ export default function DashboardPage() { | ||||
|                     <td>{item.audiences?.join(", ")}</td> | ||||
|                     <td>{new Date(item.created_at).toLocaleString()}</td> | ||||
|                     <td class="py-0"> | ||||
|                       <button class="btn btn-sm btn-square btn-error" disabled={submitting()} | ||||
|                               onClick={() => killSession(item)}> | ||||
|                         <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" class="h-5 w-5"> | ||||
|                           <path | ||||
|                             d="M256 48C141.31 48 48 141.31 48 256s93.31 208 208 208s208-93.31 208-208S370.69 48 256 48zm80 224H176a16 16 0 0 1 0-32h160a16 16 0 0 1 0 32z" | ||||
|                             fill="currentColor"></path> | ||||
|                         </svg> | ||||
|                       <button disabled={submitting()} onClick={() => killSession(item)}> | ||||
|                         <i class="fa-solid fa-right-from-bracket"></i> | ||||
|                       </button> | ||||
|                     </td> | ||||
|                   </tr>} | ||||
| @@ -206,13 +193,9 @@ export default function DashboardPage() { | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </div> | ||||
|           </details> | ||||
|           </Match> | ||||
|  | ||||
|           <details class="collapse collapse-plus join-item"> | ||||
|             <summary class="collapse-title text-lg font-medium"> | ||||
|               Events | ||||
|             </summary> | ||||
|             <div class="collapse-content mx-[-16px]"> | ||||
|           <Match when={contentTab() === 2}> | ||||
|             <div class="overflow-x-auto"> | ||||
|               <table class="table"> | ||||
|                 <thead> | ||||
| @@ -243,10 +226,8 @@ export default function DashboardPage() { | ||||
|                 </tbody> | ||||
|               </table> | ||||
|             </div> | ||||
|             </div> | ||||
|           </details> | ||||
|  | ||||
|         </div> | ||||
|           </Match> | ||||
|         </Switch> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
|   | ||||
| @@ -21,50 +21,24 @@ const defaultUserinfo: Userinfo = { | ||||
| const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo)); | ||||
|  | ||||
| export function getAtk(): string { | ||||
|   return new Cookie().get("access_token"); | ||||
| } | ||||
|  | ||||
| export async function refreshAtk() { | ||||
|   const rtk = new Cookie().get("refresh_token"); | ||||
|  | ||||
|   const res = await fetch("/api/auth/token", { | ||||
|     method: "POST", | ||||
|     headers: { "Content-Type": "application/json" }, | ||||
|     body: JSON.stringify({ | ||||
|       refresh_token: rtk, | ||||
|       grant_type: "refresh_token" | ||||
|     }) | ||||
|   }); | ||||
|   if (res.status !== 200) { | ||||
|     console.error(await res.text()) | ||||
|   } else { | ||||
|     const data = await res.json(); | ||||
|     new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); | ||||
|     new Cookie().set("refresh_token", data["refresh_token"], { path: "/", maxAge: undefined }); | ||||
|   } | ||||
|   return new Cookie().get("identity_auth_key"); | ||||
| } | ||||
|  | ||||
| function checkLoggedIn(): boolean { | ||||
|   return new Cookie().get("access_token"); | ||||
|   return new Cookie().get("identity_auth_key"); | ||||
| } | ||||
|  | ||||
| export async function readProfiles(recovering = true) { | ||||
| export async function readProfiles() { | ||||
|   if (!checkLoggedIn()) return; | ||||
|  | ||||
|   const res = await fetch("/api/users/me", { | ||||
|     headers: { "Authorization": `Bearer ${getAtk()}` } | ||||
|     credentials: "include" | ||||
|   }); | ||||
|  | ||||
|   if (res.status !== 200) { | ||||
|     if (recovering) { | ||||
|       // Auto retry after refresh access token | ||||
|       await refreshAtk(); | ||||
|       return await readProfiles(false); | ||||
|     } else { | ||||
|     clearUserinfo(); | ||||
|     window.location.reload(); | ||||
|   } | ||||
|   } | ||||
|  | ||||
|   const data = await res.json(); | ||||
|  | ||||
| @@ -77,8 +51,14 @@ export async function readProfiles(recovering = true) { | ||||
| } | ||||
|  | ||||
| export function clearUserinfo() { | ||||
|   new Cookie().remove("access_token", { path: "/", maxAge: undefined }); | ||||
|   new Cookie().remove("refresh_token", { path: "/", maxAge: undefined }); | ||||
|   const cookies = document.cookie.split(";"); | ||||
|   for (let i = 0; i < cookies.length; i++) { | ||||
|     const cookie = cookies[i]; | ||||
|     const eqPos = cookie.indexOf("="); | ||||
|     const name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie; | ||||
|     document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; | ||||
|   } | ||||
|  | ||||
|   setUserinfo(defaultUserinfo); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -335,6 +335,11 @@ | ||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" | ||||
|   integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== | ||||
|  | ||||
| "@fortawesome/fontawesome-free@^6.5.1": | ||||
|   version "6.5.1" | ||||
|   resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.5.1.tgz#55cc8410abf1003b726324661ce5b0d1c10de258" | ||||
|   integrity sha512-CNy5vSwN3fsUStPRLX7fUYojyuzoEMSXPl7zSLJ8TgtRfjv24LOnOWKT2zYwaHZCJGkdyRnTmstR0P+Ah503Gw== | ||||
|  | ||||
| "@isaacs/cliui@^8.0.2": | ||||
|   version "8.0.2" | ||||
|   resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" | ||||
| @@ -1472,6 +1477,7 @@ source-map-js@^1.0.2: | ||||
|   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== | ||||
|  | ||||
| "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: | ||||
|   name string-width-cjs | ||||
|   version "4.2.3" | ||||
|   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | ||||
|   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== | ||||
| @@ -1490,6 +1496,7 @@ string-width@^5.0.1, string-width@^5.1.2: | ||||
|     strip-ansi "^7.0.1" | ||||
|  | ||||
| "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: | ||||
|   name strip-ansi-cjs | ||||
|   version "6.0.1" | ||||
|   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" | ||||
|   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||||
|   | ||||
| @@ -9,7 +9,7 @@ secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" | ||||
|  | ||||
| content = "uploads" | ||||
|  | ||||
| use_registration_magic_token = true | ||||
| use_registration_magic_token = false | ||||
|  | ||||
| [external.firebase] | ||||
| credentials = "dist/firebase-certs.json" | ||||
| @@ -21,6 +21,12 @@ smtp_port = 465 | ||||
| username = "alphabot@smartsheep.studio" | ||||
| password = "gz937Zxxzfcd9SeH" | ||||
|  | ||||
| [security] | ||||
| cookie_domain = "localhost" | ||||
| cookie_samesite = "Lax" | ||||
| access_token_duration = 300 | ||||
| refresh_token_duration = 2592000 | ||||
|  | ||||
| [database] | ||||
| dsn = "host=localhost dbname=hy_identity port=5432 sslmode=disable" | ||||
| prefix = "identity_" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user