✨ Login
This commit is contained in:
parent
434773976f
commit
19e1775476
12
go.mod
12
go.mod
@ -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
4
go.sum
@ -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=
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
20
pkg/server/users_api.go
Normal 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)
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -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>
|
||||||
|
64
pkg/view/src/pages/auth/callback.tsx
Normal file
64
pkg/view/src/pages/auth/callback.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
55
pkg/view/src/pages/auth/callout.tsx
Normal file
55
pkg/view/src/pages/auth/callout.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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);
|
||||||
}
|
}
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user