Login

This commit is contained in:
LittleSheep 2024-02-02 00:53:22 +08:00
parent 434773976f
commit 19e1775476
15 changed files with 236 additions and 165 deletions

12
go.mod
View File

@ -6,10 +6,16 @@ require (
code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a
github.com/go-playground/validator/v10 v10.17.0 github.com/go-playground/validator/v10 v10.17.0
github.com/gofiber/fiber/v2 v2.52.0 github.com/gofiber/fiber/v2 v2.52.0
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/json-iterator/go v1.1.12 github.com/json-iterator/go v1.1.12
github.com/rs/zerolog v1.31.0 github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.39.0 github.com/samber/lo v1.39.0
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
golang.org/x/crypto v0.18.0
golang.org/x/oauth2 v0.16.0
gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.4 gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.6 gorm.io/gorm v1.25.6
) )
@ -21,9 +27,7 @@ require (
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
@ -31,7 +35,6 @@ require (
github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/klauspost/compress v1.17.0 // indirect github.com/klauspost/compress v1.17.0 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
@ -56,10 +59,8 @@ require (
github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/net v0.20.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sync v0.5.0 // indirect golang.org/x/sync v0.5.0 // indirect
golang.org/x/sys v0.16.0 // indirect golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
@ -67,6 +68,5 @@ require (
google.golang.org/protobuf v1.32.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/datatypes v1.2.0 // indirect
gorm.io/driver/mysql v1.5.2 // indirect gorm.io/driver/mysql v1.5.2 // indirect
) )

4
go.sum
View File

@ -146,8 +146,6 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE= golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
@ -158,8 +156,6 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=

View File

@ -42,11 +42,11 @@ func main() {
go server.Listen() go server.Listen()
// Messages // Messages
log.Info().Msgf("Passport v%s is started...", interactive.AppVersion) log.Info().Msgf("Interactive v%s is started...", interactive.AppVersion)
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit <-quit
log.Info().Msgf("Passport v%s is quitting...", interactive.AppVersion) log.Info().Msgf("Interactive v%s is quitting...", interactive.AppVersion)
} }

View File

@ -11,19 +11,24 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
var cfg = oauth2.Config{ var cfg oauth2.Config
RedirectURL: fmt.Sprintf("https://%s/api/auth/callback", viper.GetString("domain")),
ClientID: viper.GetString("passport.client_id"), func buildOauth2Config() {
ClientSecret: viper.GetString("passport.client_secret"), cfg = oauth2.Config{
Scopes: []string{"openid"}, RedirectURL: fmt.Sprintf("https://%s/auth/callback", viper.GetString("domain")),
Endpoint: oauth2.Endpoint{ ClientID: viper.GetString("passport.client_id"),
AuthURL: fmt.Sprintf("%s/auth/o/connect", viper.GetString("passport.endpoint")), ClientSecret: viper.GetString("passport.client_secret"),
TokenURL: fmt.Sprintf("%s/api/auth/token", viper.GetString("passport.endpoint")), Scopes: []string{"openid"},
AuthStyle: oauth2.AuthStyleInParams, Endpoint: oauth2.Endpoint{
}, AuthURL: fmt.Sprintf("%s/auth/o/connect", viper.GetString("passport.endpoint")),
TokenURL: fmt.Sprintf("%s/api/auth/token", viper.GetString("passport.endpoint")),
AuthStyle: oauth2.AuthStyleInParams,
},
}
} }
func doLogin(c *fiber.Ctx) error { func doLogin(c *fiber.Ctx) error {
buildOauth2Config()
url := cfg.AuthCodeURL(uuid.NewString()) url := cfg.AuthCodeURL(uuid.NewString())
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
@ -32,6 +37,7 @@ func doLogin(c *fiber.Ctx) error {
} }
func doPostLogin(c *fiber.Ctx) error { func doPostLogin(c *fiber.Ctx) error {
buildOauth2Config()
code := c.Query("code") code := c.Query("code")
token, err := cfg.Exchange(context.Background(), code) token, err := cfg.Exchange(context.Background(), code)

View File

@ -56,6 +56,8 @@ func NewServer() {
api := A.Group("/api").Name("API") api := A.Group("/api").Name("API")
{ {
api.Get("/users/me", auth, getUserinfo)
api.Get("/auth", doLogin) api.Get("/auth", doLogin)
api.Get("/auth/callback", doPostLogin) api.Get("/auth/callback", doPostLogin)
api.Post("/auth/refresh", doRefreshToken) api.Post("/auth/refresh", doRefreshToken)

20
pkg/server/users_api.go Normal file
View File

@ -0,0 +1,20 @@
package server
import (
"code.smartsheep.studio/hydrogen/interactive/pkg/database"
"code.smartsheep.studio/hydrogen/interactive/pkg/models"
"github.com/gofiber/fiber/v2"
)
func getUserinfo(c *fiber.Ctx) error {
user := c.Locals("principal").(models.Account)
var data models.Account
if err := database.C.
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
First(&data).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(data)
}

View File

@ -13,24 +13,26 @@ import (
) )
type PassportUserinfo struct { type PassportUserinfo struct {
Sub uint `json:"sub"` Sub string `json:"sub"`
Email string `json:"email"` Email string `json:"email"`
Picture string `json:"picture"` Picture string `json:"picture"`
PreferredUsernames string `json:"preferred_usernames"` PreferredUsername string `json:"preferred_username"`
} }
func LinkAccount(userinfo PassportUserinfo) (models.Account, error) { func LinkAccount(userinfo PassportUserinfo) (models.Account, error) {
id, _ := strconv.Atoi(userinfo.Sub)
var account models.Account var account models.Account
if err := database.C.Where(&models.Account{ if err := database.C.Where(&models.Account{
ExternalID: userinfo.Sub, ExternalID: uint(id),
}).First(&account).Error; err != nil { }).First(&account).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
account = models.Account{ account = models.Account{
Name: userinfo.PreferredUsernames, Name: userinfo.PreferredUsername,
Avatar: userinfo.Picture, Avatar: userinfo.Picture,
EmailAddress: userinfo.Email, EmailAddress: userinfo.Email,
PowerLevel: 0, PowerLevel: 0,
ExternalID: userinfo.Sub, ExternalID: uint(id),
} }
return account, database.C.Save(&account).Error return account, database.C.Save(&account).Error
} }

View File

@ -19,13 +19,8 @@ render(() => (
<UserinfoProvider> <UserinfoProvider>
<Router root={RootLayout}> <Router root={RootLayout}>
<Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} /> <Route path="/" component={lazy(() => import("./pages/dashboard.tsx"))} />
<Route path="/security" component={lazy(() => import("./pages/security.tsx"))} /> <Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} />
<Route path="/personalise" component={lazy(() => import("./pages/personalise.tsx"))} /> <Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
<Route path="/auth/login" component={lazy(() => import("./pages/auth/login.tsx"))} />
<Route path="/auth/register" component={lazy(() => import("./pages/auth/register.tsx"))} />
<Route path="/auth/oauth/connect" component={lazy(() => import("./pages/auth/connect.tsx"))} />
<Route path="/auth/oauth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} />
<Route path="/users/me/confirm" component={lazy(() => import("./pages/users/confirm.tsx"))} />
</Router> </Router>
</UserinfoProvider> </UserinfoProvider>
</WellKnownProvider> </WellKnownProvider>

View File

@ -21,7 +21,7 @@ export default function RootLayout(props: any) {
}, [ready, userinfo]); }, [ready, userinfo]);
function keepGate(path: string, e?: BeforeLeaveEventArgs) { function keepGate(path: string, e?: BeforeLeaveEventArgs) {
const whitelist = ["/auth/login", "/auth/register", "/users/me/confirm"]; const whitelist = ["/auth", "/auth/callback"];
if (!userinfo?.isLoggedIn && !whitelist.includes(path)) { if (!userinfo?.isLoggedIn && !whitelist.includes(path)) {
if (!e?.defaultPrevented) e?.preventDefault(); if (!e?.defaultPrevented) e?.preventDefault();

View File

@ -74,7 +74,7 @@ export default function Navbar() {
</ul> </ul>
</div> </div>
<a href="/" class="btn btn-ghost text-xl"> <a href="/" class="btn btn-ghost text-xl">
{wellKnown?.name ?? "Goatpass"} {wellKnown?.name ?? "Interactive"}
</a> </a>
</div> </div>
<div class="navbar-center hidden lg:flex"> <div class="navbar-center hidden lg:flex">
@ -109,7 +109,7 @@ export default function Navbar() {
<button type="button" class="btn btn-sm btn-ghost" onClick={() => logout()}>Logout</button> <button type="button" class="btn btn-sm btn-ghost" onClick={() => logout()}>Logout</button>
</Match> </Match>
<Match when={!userinfo?.isLoggedIn}> <Match when={!userinfo?.isLoggedIn}>
<a href="/auth/login" class="btn btn-sm btn-primary">Login</a> <a href="/auth" class="btn btn-sm btn-primary">Login</a>
</Match> </Match>
</Switch> </Switch>
</div> </div>

View File

@ -0,0 +1,64 @@
import { createSignal, Show } from "solid-js";
import { readProfiles } from "../../stores/userinfo.tsx";
import { useNavigate } from "@solidjs/router";
import Cookie from "universal-cookie";
export default function AuthCallback() {
const [error, setError] = createSignal<string | null>(null);
const [status, setStatus] = createSignal("Communicating with Goatpass...");
const navigate = useNavigate();
async function callback() {
const res = await fetch(`/api/auth/callback${location.search}`);
if (res.status !== 200) {
setError(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 });
setStatus("Pulling your personal data...");
await readProfiles();
setStatus("Redirecting...")
setTimeout(() => navigate("/"), 1850)
}
}
callback();
return (
<div class="w-full h-full flex justify-center items-center">
<div class="card w-[480px] max-w-screen shadow-xl">
<div class="card-body">
<div id="header" class="text-center mb-5">
<h1 class="text-xl font-bold">Authenticate</h1>
<p>Via your Goatpass account</p>
</div>
<div class="pt-16 text-center">
<div class="text-center">
<div>
<span class="loading loading-lg loading-bars"></span>
</div>
<span>{status()}</span>
</div>
</div>
<Show when={error()} fallback={<div class="mt-16"></div>}>
<div id="alerts" class="mt-16">
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="capitalize">{error()}</span>
</div>
</div>
</Show>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,55 @@
import { createSignal, Show } from "solid-js";
export default function AuthCallout() {
const [error, setError] = createSignal<string | null>(null);
const [status, setStatus] = createSignal("Communicating with Goatpass...");
async function communicate() {
const res = await fetch(`/api/auth${location.search}`);
if (res.status !== 200) {
setError(await res.text());
} else {
const data = await res.json();
setStatus("Got you! Now redirecting...");
window.open(data["target"], "_self");
}
}
communicate();
return (
<div class="w-full h-full flex justify-center items-center">
<div class="card w-[480px] max-w-screen shadow-xl">
<div class="card-body">
<div id="header" class="text-center mb-5">
<h1 class="text-xl font-bold">Authenticate</h1>
<p>Via your Goatpass account</p>
</div>
<div class="pt-16 text-center">
<div class="text-center">
<div>
<span class="loading loading-lg loading-bars"></span>
</div>
<span>{status()}</span>
</div>
</div>
<Show when={error()} fallback={<div class="mt-16"></div>}>
<div id="alerts" class="mt-16">
<div role="alert" class="alert alert-error">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="capitalize">{error()}</span>
</div>
</div>
</Show>
</div>
</div>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx"; import { useUserinfo } from "../stores/userinfo.tsx";
import { createSignal, For, Show } from "solid-js"; import { createSignal, Show } from "solid-js";
export default function DashboardPage() { export default function DashboardPage() {
const userinfo = useUserinfo(); const userinfo = useUserinfo();
@ -18,19 +18,6 @@ export default function DashboardPage() {
} }
} }
async function readNotification(item: any) {
const res = await fetch(`/api/notifications/${item.id}/read`, {
method: "PUT",
headers: { Authorization: `Bearer ${getAtk()}` }
});
if (res.status !== 200) {
setError(await res.text());
} else {
await readProfiles();
setError(null);
}
}
return ( return (
<div class="max-w-[720px] mx-auto px-5 pt-12"> <div class="max-w-[720px] mx-auto px-5 pt-12">
<div id="greeting" class="px-5"> <div id="greeting" class="px-5">
@ -39,19 +26,6 @@ export default function DashboardPage() {
</div> </div>
<div id="alerts"> <div id="alerts">
<Show when={!userinfo?.meta?.confirmed_at}>
<div role="alert" class="alert alert-warning mt-5">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<span>Your account isn't confirmed yet. Please check your inbox and confirm your account.</span> <br />
<span>Otherwise your account will be deactivate after 48 hours.</span>
</div>
</div>
</Show>
<Show when={error()}> <Show when={error()}>
<div role="alert" class="alert alert-error mt-5"> <div role="alert" class="alert alert-error mt-5">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none"
@ -64,45 +38,6 @@ export default function DashboardPage() {
</Show> </Show>
</div> </div>
<div class="card shadow-xl mt-5">
<div class="card-body">
<h2 class="card-title">Notifications</h2>
<div class="bg-base-200 mt-3 mx-[-32px]">
<Show when={userinfo?.meta?.notifications?.length <= 0}>
<table class="table">
<tbody>
<tr>
<td class="px-[32px]">You're done! There are no notifications unread for you.</td>
</tr>
</tbody>
</table>
</Show>
<Show when={userinfo?.meta?.notifications?.length > 0}>
<table class="table">
<tbody>
<For each={userinfo?.meta?.notifications}>
{item =>
<tr>
<td class="px-[32px]">
<h2 class="font-bold">{item.subject}</h2>
<p>{item.content}</p>
<div class="flex gap-2">
<Show when={item.is_important}>
<span class="font-bold">Important</span>
</Show>
<a class="link" onClick={() => readNotification(item)}>Mark as read</a>
</div>
</td>
</tr>
}
</For>
</tbody>
</table>
</Show>
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@ -1,95 +1,91 @@
import Cookie from "universal-cookie"; import Cookie from "universal-cookie";
import { createContext, useContext } from "solid-js"; import {createContext, useContext} from "solid-js";
import { createStore } from "solid-js/store"; import {createStore} from "solid-js/store";
export interface Userinfo { export interface Userinfo {
isLoggedIn: boolean, isLoggedIn: boolean,
displayName: string, displayName: string,
profiles: any, profiles: any,
meta: any
} }
const UserinfoContext = createContext<Userinfo>(); const UserinfoContext = createContext<Userinfo>();
const defaultUserinfo: Userinfo = { const defaultUserinfo: Userinfo = {
isLoggedIn: false, isLoggedIn: false,
displayName: "Citizen", displayName: "Citizen",
profiles: null, profiles: null,
meta: null
}; };
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("access_token");
} }
export async function refreshAtk() { export async function refreshAtk() {
const rtk = new Cookie().get("refresh_token"); const rtk = new Cookie().get("refresh_token");
const res = await fetch("/api/auth/token", { const res = await fetch("/api/auth/refresh", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: {"Content-Type": "application/json"},
body: JSON.stringify({ body: JSON.stringify({
refresh_token: rtk, refresh_token: rtk,
grant_type: "refresh_token" })
}) });
}); if (res.status !== 200) {
if (res.status !== 200) { console.error(await res.text())
console.error(await res.text()) } else {
} else { const data = await res.json();
const data = await res.json(); new Cookie().set("access_token", data["access_token"], {path: "/", maxAge: undefined});
new Cookie().set("access_token", data["access_token"], { path: "/", maxAge: undefined }); new Cookie().set("refresh_token", data["refresh_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("access_token");
} }
export async function readProfiles(recovering = true) { export async function readProfiles(recovering = true) {
if (!checkLoggedIn()) return; if (!checkLoggedIn()) return;
const res = await fetch("/api/users/me", { const res = await fetch("/api/users/me", {
headers: { "Authorization": `Bearer ${getAtk()}` } headers: {"Authorization": `Bearer ${getAtk()}`}
}); });
if (res.status !== 200) { if (res.status !== 200) {
if (recovering) { if (recovering) {
// Auto retry after refresh access token // Auto retry after refresh access token
await refreshAtk(); await refreshAtk();
return await readProfiles(false); return await readProfiles(false);
} else { } else {
clearUserinfo(); clearUserinfo();
window.location.reload(); window.location.reload();
}
} }
}
const data = await res.json(); const data = await res.json();
setUserinfo({ setUserinfo({
isLoggedIn: true, isLoggedIn: true,
displayName: data["nick"], displayName: data["name"],
profiles: null, profiles: data,
meta: data });
});
} }
export function clearUserinfo() { export function clearUserinfo() {
new Cookie().remove("access_token", { path: "/", maxAge: undefined }); new Cookie().remove("access_token", {path: "/", maxAge: undefined});
new Cookie().remove("refresh_token", { path: "/", maxAge: undefined }); new Cookie().remove("refresh_token", {path: "/", maxAge: undefined});
setUserinfo(defaultUserinfo); setUserinfo(defaultUserinfo);
} }
export function UserinfoProvider(props: any) { export function UserinfoProvider(props: any) {
return ( return (
<UserinfoContext.Provider value={userinfo}> <UserinfoContext.Provider value={userinfo}>
{props.children} {props.children}
</UserinfoContext.Provider> </UserinfoContext.Provider>
); );
} }
export function useUserinfo() { export function useUserinfo() {
return useContext(UserinfoContext); return useContext(UserinfoContext);
} }

View File

@ -6,8 +6,8 @@ export default defineConfig({
plugins: [devtools({ autoname: true }), solid()], plugins: [devtools({ autoname: true }), solid()],
server: { server: {
proxy: { proxy: {
"/api": "http://localhost:8444", "/api": "http://localhost:8445",
"/.well-known": "http://localhost:8444" "/.well-known": "http://localhost:8445"
} }
} }
}); });