✨ Real feel-less refresh token
This commit is contained in:
parent
cc2aa8ef40
commit
00028cfce8
@ -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_"
|
||||||
|
Loading…
Reference in New Issue
Block a user