✨ Real feel-less refresh token
This commit is contained in:
		| @@ -7,20 +7,12 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type AccountState = int8 |  | ||||||
|  |  | ||||||
| const ( |  | ||||||
| 	PendingAccountState = AccountState(iota) |  | ||||||
| 	ActiveAccountState |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| 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"` |  | ||||||
| 	Profile           AccountProfile           `json:"profile"` | 	Profile           AccountProfile           `json:"profile"` | ||||||
| 	Sessions          []AuthSession            `json:"sessions"` | 	Sessions          []AuthSession            `json:"sessions"` | ||||||
| 	Challenges        []AuthChallenge          `json:"challenges"` | 	Challenges        []AuthChallenge          `json:"challenges"` | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ package security | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.smartsheep.studio/hydrogen/identity/pkg/database" | 	"code.smartsheep.studio/hydrogen/identity/pkg/database" | ||||||
| @@ -83,5 +85,11 @@ func DoChallenge(challenge models.AuthChallenge, factor models.AuthFactor, code | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// Revoke some factor passwords | ||||||
|  | 	if factor.Type == models.EmailPasswordFactor { | ||||||
|  | 		factor.Secret = strings.ReplaceAll(uuid.NewString(), "-", "") | ||||||
|  | 		database.C.Save(&factor) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,12 +2,16 @@ package security | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/gofiber/fiber/v2" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/golang-jwt/jwt/v5" | 	"github.com/golang-jwt/jwt/v5" | ||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | var CookieAccessKey = "identity_auth_key" | ||||||
|  | var CookieRefreshKey = "identity_refresh_key" | ||||||
|  |  | ||||||
| type PayloadClaims struct { | type PayloadClaims struct { | ||||||
| 	jwt.RegisteredClaims | 	jwt.RegisteredClaims | ||||||
|  |  | ||||||
| @@ -56,3 +60,22 @@ func DecodeJwt(str string) (PayloadClaims, error) { | |||||||
| 		return claims, fmt.Errorf("unexpected token payload: not payload claims type") | 		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 ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"github.com/spf13/viper" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| @@ -84,15 +85,17 @@ func GetToken(session models.AuthSession) (string, string, error) { | |||||||
| 		return refresh, access, err | 		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)) | 	sub := strconv.Itoa(int(session.AccountID)) | ||||||
| 	sed := strconv.Itoa(int(session.ID)) | 	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 { | 	if err != nil { | ||||||
| 		return refresh, access, err | 		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 { | 	if err != nil { | ||||||
| 		return refresh, access, err | 		return refresh, access, err | ||||||
| 	} | 	} | ||||||
| @@ -153,5 +156,9 @@ func RefreshToken(token string) (string, string, error) { | |||||||
| 		return "404", "403", err | 		return "404", "403", err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if session, err := RegenSession(session); err != nil { | ||||||
|  | 		return "404", "403", err | ||||||
|  | 	} else { | ||||||
| 		return GetToken(session) | 		return GetToken(session) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -137,13 +137,13 @@ func doRegister(c *fiber.Ctx) error { | |||||||
| 		return err | 		return err | ||||||
| 	} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 { | 	} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 { | ||||||
| 		return fmt.Errorf("missing magic token in request") | 		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 { | 		if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} else { | 		} else { | ||||||
| 			database.C.Delete(&tk) | 			database.C.Delete(&tk) | ||||||
| 		} | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if user, err := services.CreateAccount( | 	if user, err := services.CreateAccount( | ||||||
| 		data.Name, | 		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") | 		return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	security.SetJwtCookieSet(c, access, refresh) | ||||||
|  |  | ||||||
| 	return c.JSON(fiber.Map{ | 	return c.JSON(fiber.Map{ | ||||||
| 		"id_token":      access, | 		"id_token":      access, | ||||||
| 		"access_token":  access, | 		"access_token":  access, | ||||||
|   | |||||||
| @@ -58,18 +58,18 @@ func NewServer() { | |||||||
| 	api := A.Group("/api").Name("API") | 	api := A.Group("/api").Name("API") | ||||||
| 	{ | 	{ | ||||||
| 		api.Get("/avatar/:avatarId", getAvatar) | 		api.Get("/avatar/:avatarId", getAvatar) | ||||||
| 		api.Put("/avatar", auth, setAvatar) | 		api.Put("/avatar", authMiddleware, setAvatar) | ||||||
|  |  | ||||||
| 		api.Get("/notifications", auth, getNotifications) | 		api.Get("/notifications", authMiddleware, getNotifications) | ||||||
| 		api.Put("/notifications/:notificationId/read", auth, markNotificationRead) | 		api.Put("/notifications/:notificationId/read", authMiddleware, markNotificationRead) | ||||||
| 		api.Post("/notifications/subscribe", auth, addNotifySubscriber) | 		api.Post("/notifications/subscribe", authMiddleware, addNotifySubscriber) | ||||||
|  |  | ||||||
| 		api.Get("/users/me", auth, getUserinfo) | 		api.Get("/users/me", authMiddleware, getUserinfo) | ||||||
| 		api.Put("/users/me", auth, editUserinfo) | 		api.Put("/users/me", authMiddleware, editUserinfo) | ||||||
| 		api.Get("/users/me/events", auth, getEvents) | 		api.Get("/users/me/events", authMiddleware, getEvents) | ||||||
| 		api.Get("/users/me/challenges", auth, getChallenges) | 		api.Get("/users/me/challenges", authMiddleware, getChallenges) | ||||||
| 		api.Get("/users/me/sessions", auth, getSessions) | 		api.Get("/users/me/sessions", authMiddleware, getSessions) | ||||||
| 		api.Delete("/users/me/sessions/:sessionId", auth, killSession) | 		api.Delete("/users/me/sessions/:sessionId", authMiddleware, killSession) | ||||||
|  |  | ||||||
| 		api.Post("/users", doRegister) | 		api.Post("/users", doRegister) | ||||||
| 		api.Post("/users/me/confirm", doRegisterConfirm) | 		api.Post("/users/me/confirm", doRegisterConfirm) | ||||||
| @@ -79,8 +79,8 @@ func NewServer() { | |||||||
| 		api.Post("/auth/token", exchangeToken) | 		api.Post("/auth/token", exchangeToken) | ||||||
| 		api.Post("/auth/factors/:factorId", requestFactorToken) | 		api.Post("/auth/factors/:factorId", requestFactorToken) | ||||||
|  |  | ||||||
| 		api.Get("/auth/o/connect", auth, preConnect) | 		api.Get("/auth/o/connect", authMiddleware, preConnect) | ||||||
| 		api.Post("/auth/o/connect", auth, doConnect) | 		api.Post("/auth/o/connect", authMiddleware, doConnect) | ||||||
|  |  | ||||||
| 		developers := api.Group("/dev").Name("Developers API") | 		developers := api.Group("/dev").Name("Developers API") | ||||||
| 		{ | 		{ | ||||||
|   | |||||||
| @@ -21,8 +21,8 @@ func getOidcConfiguration(c *fiber.Ctx) error { | |||||||
|  |  | ||||||
| 	return c.JSON(fiber.Map{ | 	return c.JSON(fiber.Map{ | ||||||
| 		"issuer":                                           basepath, | 		"issuer":                                           basepath, | ||||||
| 		"authorization_endpoint":                           fmt.Sprintf("%s/auth/o/connect", basepath), | 		"authorization_endpoint":                           fmt.Sprintf("%s/authMiddleware/o/connect", basepath), | ||||||
| 		"token_endpoint":                                   fmt.Sprintf("%s/api/auth/token", basepath), | 		"token_endpoint":                                   fmt.Sprintf("%s/api/authMiddleware/token", basepath), | ||||||
| 		"userinfo_endpoint":                                fmt.Sprintf("%s/api/users/me", basepath), | 		"userinfo_endpoint":                                fmt.Sprintf("%s/api/users/me", basepath), | ||||||
| 		"response_types_supported":                         []string{"code", "token"}, | 		"response_types_supported":                         []string{"code", "token"}, | ||||||
| 		"grant_types_supported":                            []string{"authorization_code", "implicit", "refresh_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{ | 	user := models.Account{ | ||||||
| 		Name: name, | 		Name: name, | ||||||
| 		Nick: nick, | 		Nick: nick, | ||||||
| 		State: models.PendingAccountState, |  | ||||||
| 		Profile: models.AccountProfile{ | 		Profile: models.AccountProfile{ | ||||||
| 			Experience: 100, | 			Experience: 100, | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ | |||||||
|     "preview": "vite preview" |     "preview": "vite preview" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "@fortawesome/fontawesome-free": "^6.5.1", | ||||||
|     "@solidjs/router": "^0.10.10", |     "@solidjs/router": "^0.10.10", | ||||||
|     "solid-js": "^1.8.7", |     "solid-js": "^1.8.7", | ||||||
|     "universal-cookie": "^7.0.2" |     "universal-cookie": "^7.0.2" | ||||||
|   | |||||||
| @@ -8,6 +8,8 @@ import "./assets/fonts/fonts.css"; | |||||||
| import { lazy } from "solid-js"; | import { lazy } from "solid-js"; | ||||||
| import { Route, Router } from "@solidjs/router"; | import { Route, Router } from "@solidjs/router"; | ||||||
|  |  | ||||||
|  | import "@fortawesome/fontawesome-free/css/all.min.css"; | ||||||
|  |  | ||||||
| import RootLayout from "./layouts/RootLayout.tsx"; | import RootLayout from "./layouts/RootLayout.tsx"; | ||||||
| import { UserinfoProvider } from "./stores/userinfo.tsx"; | import { UserinfoProvider } from "./stores/userinfo.tsx"; | ||||||
| import { WellKnownProvider } from "./stores/wellKnown.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 { readProfiles, useUserinfo } from "../stores/userinfo.tsx"; | ||||||
| import { createEffect, createMemo, createSignal, Show } from "solid-js"; | import { createEffect, createMemo, createSignal, Show } from "solid-js"; | ||||||
| import { readWellKnown } from "../stores/wellKnown.tsx"; | import { readWellKnown } from "../stores/wellKnown.tsx"; | ||||||
| @@ -52,7 +52,7 @@ export default function RootLayout(props: any) { | |||||||
|       </div> |       </div> | ||||||
|     }> |     }> | ||||||
|       <Show when={!searchParams["embedded"]}> |       <Show when={!searchParams["embedded"]}> | ||||||
|         <Navbar /> |         <Navigatior /> | ||||||
|       </Show> |       </Show> | ||||||
|  |  | ||||||
|       <main class={`${mainContentStyles()} px-5`}>{props.children}</main> |       <main class={`${mainContentStyles()} px-5`}>{props.children}</main> | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ interface MenuItem { | |||||||
|   children?: MenuItem[]; |   children?: MenuItem[]; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default function Navbar() { | export default function Navigatior() { | ||||||
|   const nav: MenuItem[] = [ |   const nav: MenuItem[] = [ | ||||||
|     { |     { | ||||||
|       label: "You", children: [ |       label: "You", children: [ | ||||||
| @@ -1,7 +1,6 @@ | |||||||
| import { readProfiles } from "../../stores/userinfo.tsx"; | import { readProfiles } from "../../stores/userinfo.tsx"; | ||||||
| import { useNavigate, useSearchParams } from "@solidjs/router"; | import { useNavigate, useSearchParams } from "@solidjs/router"; | ||||||
| import { createSignal, For, Match, Show, Switch } from "solid-js"; | import { createSignal, For, Match, Show, Switch } from "solid-js"; | ||||||
| import Cookie from "universal-cookie"; |  | ||||||
|  |  | ||||||
| export default function LoginPage() { | export default function LoginPage() { | ||||||
|   const [title, setTitle] = createSignal("Sign in"); |   const [title, setTitle] = createSignal("Sign in"); | ||||||
| @@ -116,9 +115,6 @@ export default function LoginPage() { | |||||||
|       setError(err); |       setError(err); | ||||||
|       throw new Error(err); |       throw new Error(err); | ||||||
|     } else { |     } 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); |       setError(null); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { getAtk } from "../stores/userinfo.tsx"; | 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() { | export default function DashboardPage() { | ||||||
|   const [challenges, setChallenges] = createSignal<any[]>([]); |   const [challenges, setChallenges] = createSignal<any[]>([]); | ||||||
| @@ -12,6 +12,8 @@ export default function DashboardPage() { | |||||||
|   const [error, setError] = createSignal<string | null>(null); |   const [error, setError] = createSignal<string | null>(null); | ||||||
|   const [submitting, setSubmitting] = createSignal(false); |   const [submitting, setSubmitting] = createSignal(false); | ||||||
|  |  | ||||||
|  |   const [contentTab, setContentTab] = createSignal(0); | ||||||
|  |  | ||||||
|   async function readChallenges() { |   async function readChallenges() { | ||||||
|     const res = await fetch("/api/users/me/challenges?take=10", { |     const res = await fetch("/api/users/me/challenges?take=10", { | ||||||
|       headers: { Authorization: `Bearer ${getAtk()}` } |       headers: { Authorization: `Bearer ${getAtk()}` } | ||||||
| @@ -94,11 +96,7 @@ export default function DashboardPage() { | |||||||
|         <div class="stats shadow w-full"> |         <div class="stats shadow w-full"> | ||||||
|           <div class="stat"> |           <div class="stat"> | ||||||
|             <div class="stat-figure text-secondary"> |             <div class="stat-figure text-secondary"> | ||||||
|               <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" |               <i class="fa-solid fa-door-open inline-block text-[28px] w-8 h-8 stroke-current"></i> | ||||||
|                    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> |  | ||||||
|             </div> |             </div> | ||||||
|             <div class="stat-title">Challenges</div> |             <div class="stat-title">Challenges</div> | ||||||
|             <div class="stat-value">{challengeCount()}</div> |             <div class="stat-value">{challengeCount()}</div> | ||||||
| @@ -106,11 +104,7 @@ export default function DashboardPage() { | |||||||
|  |  | ||||||
|           <div class="stat"> |           <div class="stat"> | ||||||
|             <div class="stat-figure text-secondary"> |             <div class="stat-figure text-secondary"> | ||||||
|               <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" |               <i class="fa-solid fa-key inline-block text-[28px] w-8 h-8 stroke-current"></i> | ||||||
|                    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> |  | ||||||
|             </div> |             </div> | ||||||
|             <div class="stat-title">Sessions</div> |             <div class="stat-title">Sessions</div> | ||||||
|             <div class="stat-value">{sessionCount()}</div> |             <div class="stat-value">{sessionCount()}</div> | ||||||
| @@ -118,11 +112,7 @@ export default function DashboardPage() { | |||||||
|  |  | ||||||
|           <div class="stat"> |           <div class="stat"> | ||||||
|             <div class="stat-figure text-secondary"> |             <div class="stat-figure text-secondary"> | ||||||
|               <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" |               <i class="fa-solid fa-person-walking inline-block text-[28px] w-8 h-8 stroke-current"></i> | ||||||
|                    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> |  | ||||||
|             </div> |             </div> | ||||||
|             <div class="stat-title">Events</div> |             <div class="stat-title">Events</div> | ||||||
|             <div class="stat-value">{eventCount()}</div> |             <div class="stat-value">{eventCount()}</div> | ||||||
| @@ -130,14 +120,20 @@ export default function DashboardPage() { | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div id="data-area" class="mt-5 shadow"> |       <div id="switch-area" class="mt-5"> | ||||||
|         <div class="join join-vertical w-full"> |         <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"> |       <div id="data-area" class="mt-5 shadow"> | ||||||
|             <summary class="collapse-title text-lg font-medium"> |         <Switch> | ||||||
|               Challenges |           <Match when={contentTab() === 0}> | ||||||
|             </summary> |  | ||||||
|             <div class="collapse-content mx-[-16px]"> |  | ||||||
|             <div class="overflow-x-auto"> |             <div class="overflow-x-auto"> | ||||||
|               <table class="table"> |               <table class="table"> | ||||||
|                 <thead> |                 <thead> | ||||||
| @@ -166,14 +162,10 @@ export default function DashboardPage() { | |||||||
|                 </tbody> |                 </tbody> | ||||||
|               </table> |               </table> | ||||||
|             </div> |             </div> | ||||||
|             </div> |           </Match> | ||||||
|           </details> |  | ||||||
|  |  | ||||||
|           <details class="collapse collapse-plus join-item border-b border-base-200"> |           <Match when={contentTab() === 1}> | ||||||
|             <summary class="collapse-title text-lg font-medium"> |             <div class="overflow-x-auto"> | ||||||
|               Sessions |  | ||||||
|             </summary> |  | ||||||
|             <div class="collapse-content mx-[-16px]"> |  | ||||||
|               <table class="table"> |               <table class="table"> | ||||||
|                 <thead> |                 <thead> | ||||||
|                 <tr> |                 <tr> | ||||||
| @@ -192,13 +184,8 @@ export default function DashboardPage() { | |||||||
|                     <td>{item.audiences?.join(", ")}</td> |                     <td>{item.audiences?.join(", ")}</td> | ||||||
|                     <td>{new Date(item.created_at).toLocaleString()}</td> |                     <td>{new Date(item.created_at).toLocaleString()}</td> | ||||||
|                     <td class="py-0"> |                     <td class="py-0"> | ||||||
|                       <button class="btn btn-sm btn-square btn-error" disabled={submitting()} |                       <button disabled={submitting()} onClick={() => killSession(item)}> | ||||||
|                               onClick={() => killSession(item)}> |                         <i class="fa-solid fa-right-from-bracket"></i> | ||||||
|                         <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> |                       </button> | ||||||
|                     </td> |                     </td> | ||||||
|                   </tr>} |                   </tr>} | ||||||
| @@ -206,13 +193,9 @@ export default function DashboardPage() { | |||||||
|                 </tbody> |                 </tbody> | ||||||
|               </table> |               </table> | ||||||
|             </div> |             </div> | ||||||
|           </details> |           </Match> | ||||||
|  |  | ||||||
|           <details class="collapse collapse-plus join-item"> |           <Match when={contentTab() === 2}> | ||||||
|             <summary class="collapse-title text-lg font-medium"> |  | ||||||
|               Events |  | ||||||
|             </summary> |  | ||||||
|             <div class="collapse-content mx-[-16px]"> |  | ||||||
|             <div class="overflow-x-auto"> |             <div class="overflow-x-auto"> | ||||||
|               <table class="table"> |               <table class="table"> | ||||||
|                 <thead> |                 <thead> | ||||||
| @@ -243,10 +226,8 @@ export default function DashboardPage() { | |||||||
|                 </tbody> |                 </tbody> | ||||||
|               </table> |               </table> | ||||||
|             </div> |             </div> | ||||||
|             </div> |           </Match> | ||||||
|           </details> |         </Switch> | ||||||
|  |  | ||||||
|         </div> |  | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -21,50 +21,24 @@ const defaultUserinfo: Userinfo = { | |||||||
| const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo)); | const [userinfo, setUserinfo] = createStore<Userinfo>(structuredClone(defaultUserinfo)); | ||||||
|  |  | ||||||
| export function getAtk(): string { | export function getAtk(): string { | ||||||
|   return new Cookie().get("access_token"); |   return new Cookie().get("identity_auth_key"); | ||||||
| } |  | ||||||
|  |  | ||||||
| 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 }); |  | ||||||
|   } |  | ||||||
| } | } | ||||||
|  |  | ||||||
| function checkLoggedIn(): boolean { | 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; |   if (!checkLoggedIn()) return; | ||||||
|  |  | ||||||
|   const res = await fetch("/api/users/me", { |   const res = await fetch("/api/users/me", { | ||||||
|     headers: { "Authorization": `Bearer ${getAtk()}` } |     credentials: "include" | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   if (res.status !== 200) { |   if (res.status !== 200) { | ||||||
|     if (recovering) { |  | ||||||
|       // Auto retry after refresh access token |  | ||||||
|       await refreshAtk(); |  | ||||||
|       return await readProfiles(false); |  | ||||||
|     } else { |  | ||||||
|     clearUserinfo(); |     clearUserinfo(); | ||||||
|     window.location.reload(); |     window.location.reload(); | ||||||
|   } |   } | ||||||
|   } |  | ||||||
|  |  | ||||||
|   const data = await res.json(); |   const data = await res.json(); | ||||||
|  |  | ||||||
| @@ -77,8 +51,14 @@ export async function readProfiles(recovering = true) { | |||||||
| } | } | ||||||
|  |  | ||||||
| export function clearUserinfo() { | export function clearUserinfo() { | ||||||
|   new Cookie().remove("access_token", { path: "/", maxAge: undefined }); |   const cookies = document.cookie.split(";"); | ||||||
|   new Cookie().remove("refresh_token", { path: "/", maxAge: undefined }); |   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); |   setUserinfo(defaultUserinfo); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -335,6 +335,11 @@ | |||||||
|   resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" |   resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" | ||||||
|   integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== |   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": | "@isaacs/cliui@^8.0.2": | ||||||
|   version "8.0.2" |   version "8.0.2" | ||||||
|   resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" |   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== |   integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== | ||||||
|  |  | ||||||
| "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: | "string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: | ||||||
|  |   name string-width-cjs | ||||||
|   version "4.2.3" |   version "4.2.3" | ||||||
|   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" |   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" | ||||||
|   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== |   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 "^7.0.1" | ||||||
|  |  | ||||||
| "strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.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" |   version "6.0.1" | ||||||
|   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" |   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" | ||||||
|   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== |   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" | |||||||
|  |  | ||||||
| content = "uploads" | content = "uploads" | ||||||
|  |  | ||||||
| use_registration_magic_token = true | use_registration_magic_token = false | ||||||
|  |  | ||||||
| [external.firebase] | [external.firebase] | ||||||
| credentials = "dist/firebase-certs.json" | credentials = "dist/firebase-certs.json" | ||||||
| @@ -21,6 +21,12 @@ smtp_port = 465 | |||||||
| username = "alphabot@smartsheep.studio" | username = "alphabot@smartsheep.studio" | ||||||
| password = "gz937Zxxzfcd9SeH" | password = "gz937Zxxzfcd9SeH" | ||||||
|  |  | ||||||
|  | [security] | ||||||
|  | cookie_domain = "localhost" | ||||||
|  | cookie_samesite = "Lax" | ||||||
|  | access_token_duration = 300 | ||||||
|  | refresh_token_duration = 2592000 | ||||||
|  |  | ||||||
| [database] | [database] | ||||||
| dsn = "host=localhost dbname=hy_identity port=5432 sslmode=disable" | dsn = "host=localhost dbname=hy_identity port=5432 sslmode=disable" | ||||||
| prefix = "identity_" | prefix = "identity_" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user