diff --git a/go.mod b/go.mod index 2263f11..033af90 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,16 @@ require ( code.smartsheep.studio/hydrogen/passport v0.0.0-20240201075828-dbc09bd7af8a github.com/go-playground/validator/v10 v10.17.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/rs/zerolog v1.31.0 github.com/samber/lo v1.39.0 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/gorm v1.25.6 ) @@ -21,9 +27,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.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/google/uuid v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // 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/jinzhu/inflection v1.0.0 // 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/leodido/go-urn v1.2.4 // 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/tcplisten v1.0.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/net v0.20.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.16.0 // indirect golang.org/x/text v0.14.0 // indirect @@ -67,6 +68,5 @@ require ( google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gorm.io/datatypes v1.2.0 // indirect gorm.io/driver/mysql v1.5.2 // indirect ) diff --git a/go.sum b/go.sum index 4f00e02..aeb3b53 100644 --- a/go.sum +++ b/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= 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.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/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= 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-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.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/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= diff --git a/pkg/cmd/main.go b/pkg/cmd/main.go index f573670..9684153 100644 --- a/pkg/cmd/main.go +++ b/pkg/cmd/main.go @@ -42,11 +42,11 @@ func main() { go server.Listen() // 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) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - log.Info().Msgf("Passport v%s is quitting...", interactive.AppVersion) + log.Info().Msgf("Interactive v%s is quitting...", interactive.AppVersion) } diff --git a/pkg/server/auth_api.go b/pkg/server/auth_api.go index 5e604cd..69ce4f3 100644 --- a/pkg/server/auth_api.go +++ b/pkg/server/auth_api.go @@ -11,19 +11,24 @@ import ( "golang.org/x/oauth2" ) -var cfg = oauth2.Config{ - RedirectURL: fmt.Sprintf("https://%s/api/auth/callback", viper.GetString("domain")), - ClientID: viper.GetString("passport.client_id"), - ClientSecret: viper.GetString("passport.client_secret"), - Scopes: []string{"openid"}, - 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, - }, +var cfg oauth2.Config + +func buildOauth2Config() { + cfg = oauth2.Config{ + RedirectURL: fmt.Sprintf("https://%s/auth/callback", viper.GetString("domain")), + ClientID: viper.GetString("passport.client_id"), + ClientSecret: viper.GetString("passport.client_secret"), + Scopes: []string{"openid"}, + 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 { + buildOauth2Config() url := cfg.AuthCodeURL(uuid.NewString()) return c.JSON(fiber.Map{ @@ -32,6 +37,7 @@ func doLogin(c *fiber.Ctx) error { } func doPostLogin(c *fiber.Ctx) error { + buildOauth2Config() code := c.Query("code") token, err := cfg.Exchange(context.Background(), code) diff --git a/pkg/server/startup.go b/pkg/server/startup.go index 5e3d690..f81ace2 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -56,6 +56,8 @@ func NewServer() { api := A.Group("/api").Name("API") { + api.Get("/users/me", auth, getUserinfo) + api.Get("/auth", doLogin) api.Get("/auth/callback", doPostLogin) api.Post("/auth/refresh", doRefreshToken) diff --git a/pkg/server/users_api.go b/pkg/server/users_api.go new file mode 100644 index 0000000..eabeee5 --- /dev/null +++ b/pkg/server/users_api.go @@ -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) +} diff --git a/pkg/services/auth.go b/pkg/services/auth.go index 4db7c0d..c843dc4 100644 --- a/pkg/services/auth.go +++ b/pkg/services/auth.go @@ -13,24 +13,26 @@ import ( ) type PassportUserinfo struct { - Sub uint `json:"sub"` - Email string `json:"email"` - Picture string `json:"picture"` - PreferredUsernames string `json:"preferred_usernames"` + Sub string `json:"sub"` + Email string `json:"email"` + Picture string `json:"picture"` + PreferredUsername string `json:"preferred_username"` } func LinkAccount(userinfo PassportUserinfo) (models.Account, error) { + id, _ := strconv.Atoi(userinfo.Sub) + var account models.Account if err := database.C.Where(&models.Account{ - ExternalID: userinfo.Sub, + ExternalID: uint(id), }).First(&account).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { account = models.Account{ - Name: userinfo.PreferredUsernames, + Name: userinfo.PreferredUsername, Avatar: userinfo.Picture, EmailAddress: userinfo.Email, PowerLevel: 0, - ExternalID: userinfo.Sub, + ExternalID: uint(id), } return account, database.C.Save(&account).Error } diff --git a/pkg/view/src/index.tsx b/pkg/view/src/index.tsx index 39f65ad..5f1d767 100644 --- a/pkg/view/src/index.tsx +++ b/pkg/view/src/index.tsx @@ -19,13 +19,8 @@ render(() => ( import("./pages/dashboard.tsx"))} /> - import("./pages/security.tsx"))} /> - import("./pages/personalise.tsx"))} /> - import("./pages/auth/login.tsx"))} /> - import("./pages/auth/register.tsx"))} /> - import("./pages/auth/connect.tsx"))} /> - import("./pages/auth/callback.tsx"))} /> - import("./pages/users/confirm.tsx"))} /> + import("./pages/auth/callout.tsx"))} /> + import("./pages/auth/callback.tsx"))} /> diff --git a/pkg/view/src/layouts/RootLayout.tsx b/pkg/view/src/layouts/RootLayout.tsx index 475c644..53f4efa 100644 --- a/pkg/view/src/layouts/RootLayout.tsx +++ b/pkg/view/src/layouts/RootLayout.tsx @@ -21,7 +21,7 @@ export default function RootLayout(props: any) { }, [ready, userinfo]); 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 (!e?.defaultPrevented) e?.preventDefault(); diff --git a/pkg/view/src/layouts/shared/Navbar.tsx b/pkg/view/src/layouts/shared/Navbar.tsx index 686a46e..fb38ed4 100644 --- a/pkg/view/src/layouts/shared/Navbar.tsx +++ b/pkg/view/src/layouts/shared/Navbar.tsx @@ -74,7 +74,7 @@ export default function Navbar() { - {wellKnown?.name ?? "Goatpass"} + {wellKnown?.name ?? "Interactive"} diff --git a/pkg/view/src/pages/auth/callback.tsx b/pkg/view/src/pages/auth/callback.tsx new file mode 100644 index 0000000..abe7e1f --- /dev/null +++ b/pkg/view/src/pages/auth/callback.tsx @@ -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(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 ( +
+
+
+ + +
+
+
+ +
+ {status()} +
+
+ +
}> +
+ +
+ +
+
+ + ); +} \ No newline at end of file diff --git a/pkg/view/src/pages/auth/callout.tsx b/pkg/view/src/pages/auth/callout.tsx new file mode 100644 index 0000000..e3b6192 --- /dev/null +++ b/pkg/view/src/pages/auth/callout.tsx @@ -0,0 +1,55 @@ +import { createSignal, Show } from "solid-js"; + +export default function AuthCallout() { + const [error, setError] = createSignal(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 ( +
+
+
+ + +
+
+
+ +
+ {status()} +
+
+ +
}> +
+ +
+ +
+
+ + ); +} \ No newline at end of file diff --git a/pkg/view/src/pages/dashboard.tsx b/pkg/view/src/pages/dashboard.tsx index de7cab7..c460663 100644 --- a/pkg/view/src/pages/dashboard.tsx +++ b/pkg/view/src/pages/dashboard.tsx @@ -1,5 +1,5 @@ -import { getAtk, readProfiles, useUserinfo } from "../stores/userinfo.tsx"; -import { createSignal, For, Show } from "solid-js"; +import { useUserinfo } from "../stores/userinfo.tsx"; +import { createSignal, Show } from "solid-js"; export default function DashboardPage() { 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 (
@@ -39,19 +26,6 @@ export default function DashboardPage() {
- - - -
-
-

Notifications

-
- - - - - - - -
You're done! There are no notifications unread for you.
-
- 0}> - - - - {item => - - - - } - - -
-

{item.subject}

-

{item.content}

- -
-
-
-
-
-
); } \ No newline at end of file diff --git a/pkg/view/src/stores/userinfo.tsx b/pkg/view/src/stores/userinfo.tsx index fd4d10f..7bf0c8f 100644 --- a/pkg/view/src/stores/userinfo.tsx +++ b/pkg/view/src/stores/userinfo.tsx @@ -1,95 +1,91 @@ import Cookie from "universal-cookie"; -import { createContext, useContext } from "solid-js"; -import { createStore } from "solid-js/store"; +import {createContext, useContext} from "solid-js"; +import {createStore} from "solid-js/store"; export interface Userinfo { - isLoggedIn: boolean, - displayName: string, - profiles: any, - meta: any + isLoggedIn: boolean, + displayName: string, + profiles: any, } const UserinfoContext = createContext(); const defaultUserinfo: Userinfo = { - isLoggedIn: false, - displayName: "Citizen", - profiles: null, - meta: null + isLoggedIn: false, + displayName: "Citizen", + profiles: null, }; const [userinfo, setUserinfo] = createStore(structuredClone(defaultUserinfo)); export function getAtk(): string { - return new Cookie().get("access_token"); + return new Cookie().get("access_token"); } 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", { - 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 }); - } + const res = await fetch("/api/auth/refresh", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({ + refresh_token: rtk, + }) + }); + 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 { - return new Cookie().get("access_token"); + return new Cookie().get("access_token"); } export async function readProfiles(recovering = true) { - if (!checkLoggedIn()) return; + if (!checkLoggedIn()) return; - const res = await fetch("/api/users/me", { - headers: { "Authorization": `Bearer ${getAtk()}` } - }); + const res = await fetch("/api/users/me", { + headers: {"Authorization": `Bearer ${getAtk()}`} + }); - if (res.status !== 200) { - if (recovering) { - // Auto retry after refresh access token - await refreshAtk(); - return await readProfiles(false); - } else { - clearUserinfo(); - window.location.reload(); + if (res.status !== 200) { + if (recovering) { + // Auto retry after refresh access token + await refreshAtk(); + return await readProfiles(false); + } else { + clearUserinfo(); + window.location.reload(); + } } - } - const data = await res.json(); + const data = await res.json(); - setUserinfo({ - isLoggedIn: true, - displayName: data["nick"], - profiles: null, - meta: data - }); + setUserinfo({ + isLoggedIn: true, + displayName: data["name"], + profiles: data, + }); } export function clearUserinfo() { - new Cookie().remove("access_token", { path: "/", maxAge: undefined }); - new Cookie().remove("refresh_token", { path: "/", maxAge: undefined }); - setUserinfo(defaultUserinfo); + new Cookie().remove("access_token", {path: "/", maxAge: undefined}); + new Cookie().remove("refresh_token", {path: "/", maxAge: undefined}); + setUserinfo(defaultUserinfo); } export function UserinfoProvider(props: any) { - return ( - - {props.children} - - ); + return ( + + {props.children} + + ); } export function useUserinfo() { - return useContext(UserinfoContext); + return useContext(UserinfoContext); } \ No newline at end of file diff --git a/pkg/view/vite.config.ts b/pkg/view/vite.config.ts index e8abad0..dec5cb7 100644 --- a/pkg/view/vite.config.ts +++ b/pkg/view/vite.config.ts @@ -6,8 +6,8 @@ export default defineConfig({ plugins: [devtools({ autoname: true }), solid()], server: { proxy: { - "/api": "http://localhost:8444", - "/.well-known": "http://localhost:8444" + "/api": "http://localhost:8445", + "/.well-known": "http://localhost:8445" } } });