diff --git a/.idea/workspace.xml b/.idea/workspace.xml index f857090..72a6cdd 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,9 +4,71 @@ - @@ -168,7 +230,8 @@ - true diff --git a/go.mod b/go.mod index 6a499cc..3d5a56e 100644 --- a/go.mod +++ b/go.mod @@ -10,9 +10,7 @@ require ( github.com/go-playground/validator/v10 v10.17.0 github.com/gofiber/contrib/websocket v1.3.0 github.com/gofiber/fiber/v2 v2.52.4 - github.com/gofiber/template/html/v2 v2.1.1 github.com/golang-jwt/jwt/v5 v5.2.0 - github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 github.com/google/uuid v1.6.0 github.com/hashicorp/consul/api v1.29.1 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible @@ -57,8 +55,6 @@ 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/gofiber/template v1.8.3 // indirect - github.com/gofiber/utils v1.1.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect diff --git a/go.sum b/go.sum index 4522a2e..a1c5a95 100644 --- a/go.sum +++ b/go.sum @@ -100,12 +100,6 @@ github.com/gofiber/contrib/websocket v1.3.0/go.mod h1:xguaOzn2ZZ759LavtosEP+rcxI github.com/gofiber/fiber/v2 v2.36.0/go.mod h1:tgCr+lierLwLoVHHO/jn3Niannv34WRkQETU8wiL9fQ= github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM= github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= -github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= -github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= -github.com/gofiber/template/html/v2 v2.1.1 h1:QEy3O3EBkvwDthy5bXVGUseOyO6ldJoiDxlF4+MJiV8= -github.com/gofiber/template/html/v2 v2.1.1/go.mod h1:2G0GHHOUx70C1LDncoBpe4T6maQbNa4x1CVNFW0wju0= -github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM= -github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang-jwt/jwt/v4 v4.4.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= @@ -135,8 +129,6 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2 h1:yEt5djSYb4iNtmV9iJGVday+i4e9u6Mrn5iP64HH5QM= -github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= diff --git a/pkg/internal/embed.go b/pkg/internal/embed.go deleted file mode 100644 index 7a0136b..0000000 --- a/pkg/internal/embed.go +++ /dev/null @@ -1,6 +0,0 @@ -package pkg - -import "embed" - -//go:embed all:views/* -var FS embed.FS diff --git a/pkg/internal/grpc/server.go b/pkg/internal/grpc/server.go index c4163a9..f143759 100644 --- a/pkg/internal/grpc/server.go +++ b/pkg/internal/grpc/server.go @@ -17,27 +17,31 @@ type Server struct { proto.UnimplementedFriendshipsServer proto.UnimplementedRealmsServer health.UnimplementedHealthServer + + srv *grpc.Server } -var S *grpc.Server +func NewServer() *Server { + server := &Server{ + srv: grpc.NewServer(), + } -func NewGRPC() { - S = grpc.NewServer() + proto.RegisterAuthServer(server.srv, &Server{}) + proto.RegisterNotifyServer(server.srv, &Server{}) + proto.RegisterFriendshipsServer(server.srv, &Server{}) + proto.RegisterRealmsServer(server.srv, &Server{}) + health.RegisterHealthServer(server.srv, &Server{}) - proto.RegisterAuthServer(S, &Server{}) - proto.RegisterNotifyServer(S, &Server{}) - proto.RegisterFriendshipsServer(S, &Server{}) - proto.RegisterRealmsServer(S, &Server{}) - health.RegisterHealthServer(S, &Server{}) + reflection.Register(server.srv) - reflection.Register(S) + return server } -func ListenGRPC() error { +func (v *Server) Listen() error { listener, err := net.Listen("tcp", viper.GetString("grpc_bind")) if err != nil { return err } - return S.Serve(listener) + return v.srv.Serve(listener) } diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 9bcb2e7..d4d9201 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -85,5 +85,9 @@ func MapAPIs(app *fiber.App) { } return c.Next() }).Get("/ws", websocket.New(listenWebsocket)) + + api.All("/*", func(c *fiber.Ctx) error { + return fiber.ErrNotFound + }) } } diff --git a/pkg/internal/server/server.go b/pkg/internal/server/server.go index 1f14ef0..870d027 100644 --- a/pkg/internal/server/server.go +++ b/pkg/internal/server/server.go @@ -1,32 +1,31 @@ package server import ( + "git.solsynth.dev/hydrogen/passport/pkg/internal/server/admin" "git.solsynth.dev/hydrogen/passport/pkg/internal/server/api" "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" + "github.com/gofiber/fiber/v2/middleware/filesystem" "net/http" + "path/filepath" "strings" - "git.solsynth.dev/hydrogen/passport/pkg/internal" "git.solsynth.dev/hydrogen/passport/pkg/internal/i18n" - "git.solsynth.dev/hydrogen/passport/pkg/internal/server/admin" - "git.solsynth.dev/hydrogen/passport/pkg/internal/server/ui" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v2/middleware/favicon" "github.com/gofiber/fiber/v2/middleware/idempotency" "github.com/gofiber/fiber/v2/middleware/logger" - "github.com/gofiber/template/html/v2" jsoniter "github.com/json-iterator/go" "github.com/rs/zerolog/log" "github.com/spf13/viper" ) -var A *fiber.App +type HTTPApp struct { + app *fiber.App +} -func NewServer() { - templates := html.NewFileSystem(http.FS(pkg.FS), ".gohtml") - - A = fiber.New(fiber.Config{ +func NewServer() *HTTPApp { + app := fiber.New(fiber.Config{ DisableStartupMessage: true, EnableIPValidation: true, ServerHeader: "Hydrogen.Passport", @@ -35,12 +34,10 @@ func NewServer() { JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal, JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal, EnablePrintRoutes: viper.GetBool("debug.print_routes"), - Views: templates, - ViewsLayout: "views/index", }) - A.Use(idempotency.New()) - A.Use(cors.New(cors.Config{ + app.Use(idempotency.New()) + app.Use(cors.New(cors.Config{ AllowCredentials: true, AllowMethods: strings.Join([]string{ fiber.MethodGet, @@ -56,27 +53,34 @@ func NewServer() { }, })) - A.Use(logger.New(logger.Config{ + app.Use(logger.New(logger.Config{ Format: "${status} | ${latency} | ${method} ${path}\n", Output: log.Logger, })) - A.Use(exts.AuthMiddleware) - A.Use(i18n.I18nMiddleware) + app.Use(exts.AuthMiddleware) + app.Use(i18n.I18nMiddleware) - A.Use(favicon.New(favicon.Config{ - FileSystem: http.FS(pkg.FS), - File: "views/favicon.png", - URL: "/favicon.png", + api.MapAPIs(app) + admin.MapAdminEndpoints(app) + + app.Use(filesystem.New(filesystem.Config{ + Root: http.Dir(viper.GetString("frontend_app")), + Index: "index.html", + NotFoundFile: "index.html", + MaxAge: 3600, })) - api.MapAPIs(A) - admin.MapAdminEndpoints(A) - ui.MapUserInterface(A) + app.Use(favicon.New(favicon.Config{ + File: filepath.Join(viper.GetString("frontend_app"), "favicon.png"), + URL: "/favicon.png", + })) + + return &HTTPApp{app} } -func Listen() { - if err := A.Listen(viper.GetString("bind")); err != nil { +func (v *HTTPApp) Listen() { + if err := v.app.Listen(viper.GetString("bind")); err != nil { log.Fatal().Err(err).Msg("An error occurred when starting server...") } } diff --git a/pkg/internal/server/ui/accounts.go b/pkg/internal/server/ui/accounts.go deleted file mode 100644 index e4ec045..0000000 --- a/pkg/internal/server/ui/accounts.go +++ /dev/null @@ -1,55 +0,0 @@ -package ui - -import ( - "fmt" - "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" - "html/template" - "time" - - "git.solsynth.dev/hydrogen/passport/pkg/internal/database" - "git.solsynth.dev/hydrogen/passport/pkg/internal/models" - "github.com/gofiber/fiber/v2" - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" - "github.com/sujit-baniya/flash" -) - -func selfUserinfoPage(c *fiber.Ctx) error { - if err := exts.EnsureAuthenticated(c); err != nil { - return DoAuthRedirect(c) - } - user := c.Locals("user").(models.Account) - - var data models.Account - if err := database.C. - Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}). - Preload("Profile"). - Preload("PersonalPage"). - Preload("Contacts"). - First(&data).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - birthday := "Unknown" - if data.Profile.Birthday != nil { - birthday = data.Profile.Birthday.Format(time.RFC822) - } - - doc := parser. - NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock). - Parse([]byte(data.PersonalPage.Content)) - - renderer := html.NewRenderer(html.RendererOptions{Flags: html.CommonFlags | html.HrefTargetBlank}) - - return c.Render("views/users/me", fiber.Map{ - "info": flash.Get(c)["message"], - "uid": fmt.Sprintf("%08d", data.ID), - "joined_at": data.CreatedAt.Format(time.RFC822), - "birthday_at": birthday, - "personal_page": template.HTML(markdown.Render(doc, renderer)), - "userinfo": data, - "avatar": data.GetAvatar(), - "banner": data.GetBanner(), - }, "views/layouts/user-center") -} diff --git a/pkg/internal/server/ui/index.go b/pkg/internal/server/ui/index.go deleted file mode 100644 index 9b1027e..0000000 --- a/pkg/internal/server/ui/index.go +++ /dev/null @@ -1,34 +0,0 @@ -package ui - -import ( - "fmt" - - "github.com/gofiber/fiber/v2" -) - -func DoAuthRedirect(c *fiber.Ctx) error { - uri := c.Request().URI().FullURI() - return c.Redirect(fmt.Sprintf("/sign-in?redirect_uri=%s", string(uri))) -} - -func MapUserInterface(app *fiber.App) { - pages := app.Group("/").Name("Pages") - - pages.Get("/", func(c *fiber.Ctx) error { - return c.Redirect("/users/me") - }) - - pages.Get("/sign-up", signupPage) - pages.Get("/sign-in", signinPage) - pages.Get("/mfa", mfaRequestPage) - pages.Get("/mfa/apply", mfaApplyPage) - pages.Get("/authorize", authorizePage) - - pages.Post("/sign-up", signupAction) - pages.Post("/sign-in", signinAction) - pages.Post("/mfa", mfaRequestAction) - pages.Post("/mfa/apply", mfaApplyAction) - pages.Post("/authorize", authorizeAction) - - pages.Get("/users/me", selfUserinfoPage) -} diff --git a/pkg/internal/server/ui/mfa.go b/pkg/internal/server/ui/mfa.go deleted file mode 100644 index 4fd8e24..0000000 --- a/pkg/internal/server/ui/mfa.go +++ /dev/null @@ -1,194 +0,0 @@ -package ui - -import ( - "fmt" - "git.solsynth.dev/hydrogen/passport/pkg/internal/models" - "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" - "git.solsynth.dev/hydrogen/passport/pkg/internal/services" - "github.com/gofiber/fiber/v2" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/samber/lo" - "github.com/sujit-baniya/flash" -) - -func mfaRequestPage(c *fiber.Ctx) error { - ticketId := c.QueryInt("ticket", 0) - - ticket, err := services.GetTicket(uint(ticketId)) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": "you must provide ticket id to perform multi-factor authenticate", - }).Redirect("/sign-in") - } - user, err := services.GetAccount(ticket.AccountID) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": "ticket related user just weirdly disappear", - }).Redirect("/sign-in") - } - factors, err := services.ListUserFactor(user.ID) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("unable to get your factors: %v", err.Error()), - }).Redirect("/sign-in") - } - - factors = lo.Filter(factors, func(item models.AuthFactor, index int) bool { - return item.Type != models.PasswordAuthFactor - }) - - localizer := c.Locals("localizer").(*i18n.Localizer) - - next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"}) - title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaTitle"}) - caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaCaption"}) - - return c.Render("views/mfa", fiber.Map{ - "info": flash.Get(c)["message"], - "redirect_uri": flash.Get(c)["redirect_uri"], - "ticket_id": ticket.ID, - "factors": lo.Map(factors, func(item models.AuthFactor, index int) fiber.Map { - return fiber.Map{ - "name": services.GetFactorName(item.Type, localizer), - "id": item.ID, - } - }), - "i18n": fiber.Map{ - "next": next, - "title": title, - "caption": caption, - }, - }, "views/layouts/auth") -} - -func mfaRequestAction(c *fiber.Ctx) error { - var data struct { - TicketID uint `form:"ticket_id" validate:"required"` - FactorID uint `form:"factor_id" validate:"required"` - } - - redirectBackUri := "/sign-in" - err := exts.BindAndValidate(c, &data) - - if data.TicketID > 0 { - redirectBackUri = fmt.Sprintf("/mfa?ticket=%d", data.TicketID) - } - - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": err.Error(), - }).Redirect(redirectBackUri) - } - - factor, err := services.GetFactor(data.FactorID) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("factor was not found: %v", err.Error()), - }).Redirect(redirectBackUri) - } - - _, err = services.GetFactorCode(factor) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("unable to get factor code: %v", err.Error()), - }).Redirect(redirectBackUri) - } - - return flash.WithData(c, fiber.Map{ - "redirect_uri": exts.GetRedirectUri(c), - }).Redirect(fmt.Sprintf("/mfa/apply?ticket=%d&factor=%d", data.TicketID, factor.ID)) -} - -func mfaApplyPage(c *fiber.Ctx) error { - ticketId := c.QueryInt("ticket", 0) - factorId := c.QueryInt("factor", 0) - - ticket, err := services.GetTicket(uint(ticketId)) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("unable to find your ticket: %v", err.Error()), - }).Redirect("/sign-in") - } - factor, err := services.GetFactor(uint(factorId)) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("unable to find your factors: %v", err.Error()), - }).Redirect("/sign-in") - } - - localizer := c.Locals("localizer").(*i18n.Localizer) - - next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"}) - password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"}) - title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaTitle"}) - caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "mfaCaption"}) - - return c.Render("views/mfa-apply", fiber.Map{ - "info": flash.Get(c)["message"], - "label": services.GetFactorName(factor.Type, localizer), - "ticket_id": ticket.ID, - "factor_id": factor.ID, - "i18n": fiber.Map{ - "next": next, - "password": password, - "title": title, - "caption": caption, - }, - }, "views/layouts/auth") -} - -func mfaApplyAction(c *fiber.Ctx) error { - var data struct { - TicketID uint `form:"ticket_id" validate:"required"` - FactorID uint `form:"factor_id" validate:"required"` - Code string `form:"code" validate:"required"` - } - - redirectBackUri := "/sign-in" - err := exts.BindAndValidate(c, &data) - - if data.TicketID > 0 { - redirectBackUri = fmt.Sprintf("/mfa/apply?ticket=%d&factor=%d", data.TicketID, data.FactorID) - } - - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": err.Error(), - }).Redirect(redirectBackUri) - } - - ticket, err := services.GetTicket(data.TicketID) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("unable to find your ticket: %v", err.Error()), - }).Redirect("/sign-in") - } - factor, err := services.GetFactor(data.FactorID) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("factor was not found: %v", err.Error()), - }).Redirect(redirectBackUri) - } - - ticket, err = services.ActiveTicketWithMFA(ticket, factor, data.Code) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("invalid multi-factor authenticate code: %v", err.Error()), - }).Redirect(redirectBackUri) - } else if ticket.IsAvailable() != nil { - return flash.WithInfo(c, fiber.Map{ - "message": "ticket weirdly still unavailable after multi-factor authenticate", - }).Redirect("/sign-in") - } - - access, refresh, err := services.ExchangeToken(*ticket.GrantToken) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("failed to exchange token: %v", err.Error()), - }).Redirect("/sign-in") - } else { - exts.SetAuthCookies(c, access, refresh) - } - - return c.Redirect(lo.FromPtr(exts.GetRedirectUri(c, "/users/me"))) -} diff --git a/pkg/internal/server/ui/oauth.go b/pkg/internal/server/ui/oauth.go deleted file mode 100644 index 946a17c..0000000 --- a/pkg/internal/server/ui/oauth.go +++ /dev/null @@ -1,166 +0,0 @@ -package ui - -import ( - "fmt" - "git.solsynth.dev/hydrogen/passport/pkg/internal/database" - "git.solsynth.dev/hydrogen/passport/pkg/internal/models" - "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" - "git.solsynth.dev/hydrogen/passport/pkg/internal/services" - "github.com/gofiber/fiber/v2" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/samber/lo" - "github.com/sujit-baniya/flash" - "html/template" - "strings" - "time" -) - -func authorizePage(c *fiber.Ctx) error { - localizer := c.Locals("localizer").(*i18n.Localizer) - - if err := exts.EnsureAuthenticated(c); err != nil { - return DoAuthRedirect(c) - } - user := c.Locals("user").(models.Account) - - id := c.Query("client_id") - redirect := c.Query("redirect_uri") - - var message string - if len(id) <= 0 || len(redirect) <= 0 { - message = "invalid request, missing query parameters" - } - - var client models.ThirdClient - if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil { - message = fmt.Sprintf("unable to find client: %v", err) - } else if !client.IsDraft && !lo.Contains(client.Callbacks, strings.Split(redirect, "?")[0]) { - message = "invalid callback url" - } - - var ticket models.AuthTicket - if err := database.C.Where(&models.AuthTicket{ - AccountID: user.ID, - ClientID: &client.ID, - }).Where("last_grant_at IS NULL").First(&ticket).Error; err == nil { - if !(ticket.ExpiredAt != nil && ticket.ExpiredAt.Unix() < time.Now().Unix()) { - ticket, err = services.RegenSession(ticket) - if c.Query("response_type") == "code" { - return c.Redirect(fmt.Sprintf( - "%s?code=%s&state=%s", - redirect, - *ticket.GrantToken, - c.Query("state"), - )) - } else if c.Query("response_type") == "token" { - if access, refresh, err := services.GetToken(ticket); err == nil { - return c.Redirect(fmt.Sprintf("%s?access_token=%s&refresh_token=%s&state=%s", - redirect, - access, - refresh, c.Query("state"), - )) - } - } - } - } - - decline, _ := localizer.LocalizeMessage(&i18n.Message{ID: "decline"}) - approve, _ := localizer.LocalizeMessage(&i18n.Message{ID: "approve"}) - title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "authorizeTitle"}) - caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "authorizeCaption"}) - - qs := "/authorize?" + string(c.Request().URI().QueryString()) - - return c.Render("views/authorize", fiber.Map{ - "info": lo.Ternary[any](len(message) > 0, message, flash.Get(c)["message"]), - "client": client, - "scopes": strings.Split(c.Query("scope"), " "), - "action_url": template.URL(qs), - "i18n": fiber.Map{ - "approve": approve, - "decline": decline, - "title": title, - "caption": caption, - }, - }, "views/layouts/auth") -} - -func authorizeAction(c *fiber.Ctx) error { - if err := exts.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - id := c.Query("client_id") - response := c.Query("response_type") - redirect := c.Query("redirect_uri") - scope := c.Query("scope") - - if err := exts.EnsureAuthenticated(c); err != nil { - return DoAuthRedirect(c) - } - - redirectBackUri := "/authorize?" + string(c.Request().URI().QueryString()) - - if len(scope) <= 0 { - return flash.WithInfo(c, fiber.Map{ - "message": "invalid request parameters", - }).Redirect(redirectBackUri) - } - - var client models.ThirdClient - if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil { - return fiber.NewError(fiber.StatusNotFound, err.Error()) - } - - switch response { - case "code": - // OAuth Authorization Mode - ticket, err := services.NewOauthTicket( - user, - client, - strings.Split(scope, " "), - []string{"passport", client.Alias}, - c.IP(), - c.Get(fiber.HeaderUserAgent), - ) - - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } else { - services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent)) - return c.Redirect(fmt.Sprintf( - "%s?code=%s&state=%s", - redirect, - *ticket.GrantToken, - c.Query("state"), - )) - } - case "token": - // OAuth Implicit Mode - ticket, err := services.NewOauthTicket( - user, - client, - strings.Split(scope, " "), - []string{"passport", client.Alias}, - c.IP(), - c.Get(fiber.HeaderUserAgent), - ) - - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } else if access, refresh, err := services.GetToken(ticket); err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } else { - services.AddEvent(user, "oauth.connect", client.Alias, c.IP(), c.Get(fiber.HeaderUserAgent)) - return c.Redirect(fmt.Sprintf("%s?access_token=%s&refresh_token=%s&state=%s", - redirect, - access, - refresh, c.Query("state"), - )) - } - default: - return flash.WithInfo(c, fiber.Map{ - "message": "unsupported response type", - }).Redirect(redirectBackUri) - } -} diff --git a/pkg/internal/server/ui/signin.go b/pkg/internal/server/ui/signin.go deleted file mode 100644 index df87970..0000000 --- a/pkg/internal/server/ui/signin.go +++ /dev/null @@ -1,93 +0,0 @@ -package ui - -import ( - "fmt" - "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" - "git.solsynth.dev/hydrogen/passport/pkg/internal/services" - "github.com/gofiber/fiber/v2" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/samber/lo" - "github.com/sujit-baniya/flash" -) - -func signinPage(c *fiber.Ctx) error { - localizer := c.Locals("localizer").(*i18n.Localizer) - - next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"}) - username, _ := localizer.LocalizeMessage(&i18n.Message{ID: "username"}) - password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"}) - signup, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"}) - title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"}) - caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinCaption"}) - requiredNotify, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinRequired"}) - - var info any - if flash.Get(c)["message"] != nil { - info = flash.Get(c)["message"] - } else { - info = requiredNotify - } - - return c.Render("views/signin", fiber.Map{ - "info": info, - "i18n": fiber.Map{ - "next": next, - "username": username, - "password": password, - "signup": signup, - "title": title, - "caption": caption, - }, - }, "views/layouts/auth") -} - -func signinAction(c *fiber.Ctx) error { - var data struct { - Username string `form:"username" validate:"required"` - Password string `form:"password" validate:"required"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": err.Error(), - }).Redirect("/sign-in") - } - - user, err := services.LookupAccount(data.Username) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("account was not found: %v", err.Error()), - }).Redirect("/sign-in") - } - - ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent)) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("unable setup ticket: %v", err.Error()), - }).Redirect("/sign-in") - } - - ticket, err = services.ActiveTicketWithPassword(ticket, data.Password) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("invalid password: %v", err.Error()), - }).Redirect("/sign-in") - } - - if ticket.IsAvailable() != nil { - return flash.WithData(c, fiber.Map{ - "redirect_uri": exts.GetRedirectUri(c), - }).Redirect(fmt.Sprintf("/mfa?ticket=%d", ticket.ID)) - } - - access, refresh, err := services.ExchangeToken(*ticket.GrantToken) - if err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("failed to exchange token: %v", err.Error()), - }).Redirect("/sign-in") - } else { - exts.SetAuthCookies(c, access, refresh) - } - - return c.Redirect(lo.FromPtr(exts.GetRedirectUri(c, "/users/me"))) -} diff --git a/pkg/internal/server/ui/signup.go b/pkg/internal/server/ui/signup.go deleted file mode 100644 index 28867a2..0000000 --- a/pkg/internal/server/ui/signup.go +++ /dev/null @@ -1,87 +0,0 @@ -package ui - -import ( - "fmt" - "git.solsynth.dev/hydrogen/passport/pkg/internal/database" - "git.solsynth.dev/hydrogen/passport/pkg/internal/models" - "git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts" - "git.solsynth.dev/hydrogen/passport/pkg/internal/services" - "github.com/gofiber/fiber/v2" - "github.com/nicksnyder/go-i18n/v2/i18n" - "github.com/samber/lo" - "github.com/spf13/viper" - "github.com/sujit-baniya/flash" -) - -func signupPage(c *fiber.Ctx) error { - localizer := c.Locals("localizer").(*i18n.Localizer) - - next, _ := localizer.LocalizeMessage(&i18n.Message{ID: "next"}) - email, _ := localizer.LocalizeMessage(&i18n.Message{ID: "email"}) - nickname, _ := localizer.LocalizeMessage(&i18n.Message{ID: "nickname"}) - username, _ := localizer.LocalizeMessage(&i18n.Message{ID: "username"}) - password, _ := localizer.LocalizeMessage(&i18n.Message{ID: "password"}) - magicToken, _ := localizer.LocalizeMessage(&i18n.Message{ID: "magicToken"}) - signin, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signinTitle"}) - title, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupTitle"}) - caption, _ := localizer.LocalizeMessage(&i18n.Message{ID: "signupCaption"}) - - return c.Render("views/signup", fiber.Map{ - "info": flash.Get(c)["message"], - "use_magic_token": viper.GetBool("use_registration_magic_token"), - "i18n": fiber.Map{ - "next": next, - "email": email, - "username": username, - "nickname": nickname, - "password": password, - "magic_token": magicToken, - "signin": signin, - "title": title, - "caption": caption, - }, - }, "views/layouts/auth") -} - -func signupAction(c *fiber.Ctx) error { - var data struct { - Name string `form:"name" validate:"required,lowercase,alphanum,min=4,max=16"` - Nick string `form:"nick" validate:"required,min=4,max=24"` - Email string `form:"email" validate:"required,email"` - Password string `form:"password" validate:"required,min=4,max=32"` - MagicToken string `form:"magic_token"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": err.Error(), - }).Redirect("/sign-up") - } else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 { - return flash.WithInfo(c, fiber.Map{ - "message": "magic token was required", - }).Redirect("/sign-up") - } else if viper.GetBool("use_registration_magic_token") { - if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": fmt.Sprintf("magic token was invalid: %v", err.Error()), - }).Redirect("/sign-up") - } else { - database.C.Delete(&tk) - } - } - - if _, err := services.CreateAccount( - data.Name, - data.Nick, - data.Email, - data.Password, - ); err != nil { - return flash.WithInfo(c, fiber.Map{ - "message": err.Error(), - }).Redirect("/sign-up") - } else { - return flash.WithInfo(c, fiber.Map{ - "message": "account has been created. now you can sign in!", - }).Redirect(lo.FromPtr(exts.GetRedirectUri(c, "/sign-in"))) - } -} diff --git a/pkg/internal/views/authorize.gohtml b/pkg/internal/views/authorize.gohtml deleted file mode 100644 index c98e6a4..0000000 --- a/pkg/internal/views/authorize.gohtml +++ /dev/null @@ -1,56 +0,0 @@ -
- - -

{{.i18n.title}} {{.client.Name}}

-

{{.i18n.caption}}

-
- -
-
- -
-
-
Description
-
{{.client.Description}}
-
- -
-
Requested scopes
-
    - {{range $_, $element := .scopes}} -
  • - {{$element}} -
  • - {{end}} -
-
- -
- - -
-
-
- - - - diff --git a/pkg/internal/views/favicon.png b/pkg/internal/views/favicon.png deleted file mode 100644 index f044b34..0000000 Binary files a/pkg/internal/views/favicon.png and /dev/null differ diff --git a/pkg/internal/views/index.gohtml b/pkg/internal/views/index.gohtml deleted file mode 100644 index c0c265f..0000000 --- a/pkg/internal/views/index.gohtml +++ /dev/null @@ -1,10 +0,0 @@ - - - -{{template "views/partials/header"}} - - -{{embed}} - - - \ No newline at end of file diff --git a/pkg/internal/views/layouts/auth.gohtml b/pkg/internal/views/layouts/auth.gohtml deleted file mode 100644 index 94815ea..0000000 --- a/pkg/internal/views/layouts/auth.gohtml +++ /dev/null @@ -1,115 +0,0 @@ - - - -{{template "views/partials/header"}} - - -
-
- {{if ne .info nil}} - - {{end}} - -
- {{embed}} -
-
-
- - - - - diff --git a/pkg/internal/views/layouts/user-center.gohtml b/pkg/internal/views/layouts/user-center.gohtml deleted file mode 100644 index 4828bd7..0000000 --- a/pkg/internal/views/layouts/user-center.gohtml +++ /dev/null @@ -1,128 +0,0 @@ - - - -{{template "views/partials/header"}} - - -
-
- {{if ne .info nil}} - - {{end}} - -
- {{embed}} -
-
-
- - - - - diff --git a/pkg/internal/views/mfa-apply.gohtml b/pkg/internal/views/mfa-apply.gohtml deleted file mode 100644 index 187ce82..0000000 --- a/pkg/internal/views/mfa-apply.gohtml +++ /dev/null @@ -1,43 +0,0 @@ -
- - -

{{.i18n.title}}

-

{{.i18n.caption}}

-
- -
-
- -
- - - -
{{.label}}
- -
- - -
- -
- -
-
-
- - diff --git a/pkg/internal/views/mfa.gohtml b/pkg/internal/views/mfa.gohtml deleted file mode 100644 index 8372c07..0000000 --- a/pkg/internal/views/mfa.gohtml +++ /dev/null @@ -1,59 +0,0 @@ -
- - -

{{.i18n.title}}

-

{{.i18n.caption}}

-
- -
-
- -
- - {{if ne .redirect_uri nil}} - - {{end}} - -
- {{range $_, $element := .factors}} -
-
- - -
-
- {{end}} -
- -
- -
-
-
- - diff --git a/pkg/internal/views/partials/header.gohtml b/pkg/internal/views/partials/header.gohtml deleted file mode 100644 index 09202f3..0000000 --- a/pkg/internal/views/partials/header.gohtml +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - Solarpass - - - diff --git a/pkg/internal/views/signin.gohtml b/pkg/internal/views/signin.gohtml deleted file mode 100644 index beef18e..0000000 --- a/pkg/internal/views/signin.gohtml +++ /dev/null @@ -1,27 +0,0 @@ -
- - -

{{.i18n.title}}

-

{{.i18n.caption}}

-
- -
-
- -
-
- - -
- -
- - -
- -
- {{.i18n.signup}} - -
-
-
diff --git a/pkg/internal/views/signup.gohtml b/pkg/internal/views/signup.gohtml deleted file mode 100644 index 2f88f97..0000000 --- a/pkg/internal/views/signup.gohtml +++ /dev/null @@ -1,47 +0,0 @@ -
- - -

{{.i18n.title}}

-

{{.i18n.caption}}

-
- -
-
- -
-
-
- - -
- -
- - -
-
- - -
- - -
- -
- - -
- - {{if eq .use_magic_token true}} -
- - -
- {{end}} - -
- {{.i18n.signin}} - -
-
-
diff --git a/pkg/internal/views/users/me.gohtml b/pkg/internal/views/users/me.gohtml deleted file mode 100644 index 63c8be1..0000000 --- a/pkg/internal/views/users/me.gohtml +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - -
- {{if ne .userinfo.Avatar nil}} - Avatar - {{else}} -
- account_circle -
- {{end}} - -
-

{{.userinfo.Nick}}

-
@{{.userinfo.Name}}
-
- {{if gt (len .userinfo.Description) 0}} -
{{.userinfo.Description}}
- {{else}} -
No description yet.
- {{end}} -
#{{.uid}}
-
- -
-
- {{.personal_page}} -
-
- - diff --git a/pkg/main.go b/pkg/main.go index e6026f5..636fc1f 100644 --- a/pkg/main.go +++ b/pkg/main.go @@ -58,12 +58,10 @@ func main() { } // Server - server.NewServer() - go server.Listen() + go server.NewServer().Listen() // Grpc Server - grpc.NewGRPC() - go grpc.ListenGRPC() + go grpc.NewServer().Listen() // Configure timed tasks quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger))) diff --git a/settings.toml b/settings.toml index 7c60c2a..5344981 100644 --- a/settings.toml +++ b/settings.toml @@ -1,5 +1,7 @@ id = "passport01" +frontend_app = "web/dist" + bind = "0.0.0.0:8444" grpc_bind = "0.0.0.0:7444" domain = "localhost" diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs new file mode 100755 index 0000000..11cf3e4 --- /dev/null +++ b/web/.eslintrc.cjs @@ -0,0 +1,18 @@ +/* eslint-env node */ +require("@rushstack/eslint-patch/modern-module-resolution") + +module.exports = { + root: true, + extends: [ + "plugin:vue/vue3-essential", + "eslint:recommended", + "@vue/eslint-config-typescript", + "@vue/eslint-config-prettier/skip-formatting", + ], + parserOptions: { + ecmaVersion: "latest", + }, + rules: { + "vue/multi-word-component-names": "off", + } +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100755 index 0000000..8ee54e8 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/web/.prettierrc.json b/web/.prettierrc.json new file mode 100755 index 0000000..6404b10 --- /dev/null +++ b/web/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "tabWidth": 2, + "singleQuote": false, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/web/README.md b/web/README.md new file mode 100755 index 0000000..bdb10ae --- /dev/null +++ b/web/README.md @@ -0,0 +1,39 @@ +# views + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vitejs.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm run lint +``` diff --git a/web/bun.lockb b/web/bun.lockb new file mode 100755 index 0000000..9a92439 Binary files /dev/null and b/web/bun.lockb differ diff --git a/web/env.d.ts b/web/env.d.ts new file mode 100755 index 0000000..11f02fe --- /dev/null +++ b/web/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/web/index.html b/web/index.html new file mode 100755 index 0000000..c61a7b4 --- /dev/null +++ b/web/index.html @@ -0,0 +1,13 @@ + + + + + + + Solarpass + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100755 index 0000000..098325b --- /dev/null +++ b/web/package.json @@ -0,0 +1,45 @@ +{ + "name": "passport-web", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "build-only": "vite build", + "type-check": "vue-tsc --build --force", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/" + }, + "dependencies": { + "@fontsource/roboto": "^5.0.13", + "@mdi/font": "^7.4.47", + "@unocss/reset": "^0.58.9", + "dompurify": "^3.1.5", + "marked": "^12.0.2", + "pinia": "^2.1.7", + "universal-cookie": "^7.1.4", + "unocss": "^0.58.9", + "vue": "^3.4.30", + "vue-router": "^4.4.0", + "vuetify": "^3.6.10" + }, + "devDependencies": { + "@rushstack/eslint-patch": "^1.10.3", + "@tsconfig/node20": "^20.1.4", + "@types/dompurify": "^3.0.5", + "@types/node": "^20.14.8", + "@vitejs/plugin-vue": "^5.0.5", + "@vitejs/plugin-vue-jsx": "^3.1.0", + "@vue/eslint-config-prettier": "^8.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/tsconfig": "^0.5.1", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.26.0", + "npm-run-all2": "^6.2.0", + "prettier": "^3.3.2", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "vue-tsc": "^2.0.22" + } +} diff --git a/web/public/favicon.png b/web/public/favicon.png new file mode 100755 index 0000000..2ca2d07 Binary files /dev/null and b/web/public/favicon.png differ diff --git a/web/src/assets/utils.css b/web/src/assets/utils.css new file mode 100755 index 0000000..fd368b0 --- /dev/null +++ b/web/src/assets/utils.css @@ -0,0 +1,14 @@ +html, +body, +#app, +.v-application { + font-family: "Roboto Sans", ui-sans-serif, system-ui, sans-serif; +} + +.no-scrollbar { + scrollbar-width: none; +} + +.no-scrollbar::-webkit-scrollbar { + width: 0; +} diff --git a/web/src/components/Copyright.vue b/web/src/components/Copyright.vue new file mode 100755 index 0000000..f3f6baa --- /dev/null +++ b/web/src/components/Copyright.vue @@ -0,0 +1,6 @@ + diff --git a/web/src/components/NotificationList.vue b/web/src/components/NotificationList.vue new file mode 100755 index 0000000..7f3cfd8 --- /dev/null +++ b/web/src/components/NotificationList.vue @@ -0,0 +1,70 @@ + + + diff --git a/web/src/components/UserMenu.vue b/web/src/components/UserMenu.vue new file mode 100755 index 0000000..10a1c28 --- /dev/null +++ b/web/src/components/UserMenu.vue @@ -0,0 +1,43 @@ + + + diff --git a/web/src/components/auth/AccountLocator.vue b/web/src/components/auth/AccountLocator.vue new file mode 100755 index 0000000..2a45c90 --- /dev/null +++ b/web/src/components/auth/AccountLocator.vue @@ -0,0 +1,61 @@ + + + diff --git a/web/src/components/auth/CallbackNotify.vue b/web/src/components/auth/CallbackNotify.vue new file mode 100755 index 0000000..d63c6d6 --- /dev/null +++ b/web/src/components/auth/CallbackNotify.vue @@ -0,0 +1,16 @@ + + + diff --git a/web/src/components/auth/FactorApplicator.vue b/web/src/components/auth/FactorApplicator.vue new file mode 100755 index 0000000..8d92c0e --- /dev/null +++ b/web/src/components/auth/FactorApplicator.vue @@ -0,0 +1,129 @@ + + + diff --git a/web/src/components/auth/FactorPicker.vue b/web/src/components/auth/FactorPicker.vue new file mode 100755 index 0000000..b107398 --- /dev/null +++ b/web/src/components/auth/FactorPicker.vue @@ -0,0 +1,75 @@ + + + diff --git a/web/src/index.vue b/web/src/index.vue new file mode 100755 index 0000000..4f21c35 --- /dev/null +++ b/web/src/index.vue @@ -0,0 +1,5 @@ + diff --git a/web/src/layouts/master.vue b/web/src/layouts/master.vue new file mode 100755 index 0000000..4b1d5c9 --- /dev/null +++ b/web/src/layouts/master.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/web/src/layouts/user-center.vue b/web/src/layouts/user-center.vue new file mode 100755 index 0000000..ddb0690 --- /dev/null +++ b/web/src/layouts/user-center.vue @@ -0,0 +1,22 @@ + + \ No newline at end of file diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100755 index 0000000..b3665d3 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,54 @@ +import "virtual:uno.css" + +import "./assets/utils.css" + +import { createApp } from "vue" +import { createPinia } from "pinia" + +import "vuetify/styles" +import { createVuetify } from "vuetify" +import { md3 } from "vuetify/blueprints" +import * as components from "vuetify/components" +import * as labsComponents from "vuetify/labs/components" +import * as directives from "vuetify/directives" + +import "@mdi/font/css/materialdesignicons.min.css" +import "@fontsource/roboto/latin.css" +import "@unocss/reset/tailwind.css" + +import index from "./index.vue" +import router from "./router" + +const app = createApp(index) + +app.use( + createVuetify({ + directives, + components: { + ...components, + ...labsComponents, + }, + blueprint: md3, + theme: { + defaultTheme: "original", + themes: { + original: { + colors: { + primary: "#4a5099", + secondary: "#2196f3", + accent: "#009688", + error: "#f44336", + warning: "#ff9800", + info: "#03a9f4", + success: "#4caf50", + }, + }, + }, + }, + }), +) + +app.use(createPinia()) +app.use(router) + +app.mount("#app") diff --git a/web/src/router/index.ts b/web/src/router/index.ts new file mode 100755 index 0000000..e9eea4b --- /dev/null +++ b/web/src/router/index.ts @@ -0,0 +1,96 @@ +import { createRouter, createWebHistory } from "vue-router" +import { useUserinfo } from "@/stores/userinfo" +import MasterLayout from "@/layouts/master.vue" +import UserCenterLayout from "@/layouts/user-center.vue" + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + component: MasterLayout, + children: [ + { + path: "/", + component: UserCenterLayout, + children: [ + { + path: "/", + name: "dashboard", + component: () => import("@/views/dashboard.vue"), + meta: { title: "Your account" }, + }, + { + path: "/me/personalize", + name: "personalize", + component: () => import("@/views/personalize.vue"), + meta: { title: "Your personality" }, + }, + { + path: "/me/personal-page", + name: "personal-page", + component: () => import("@/views/personal-page.vue"), + meta: { title: "Your personal page" }, + }, + { + path: "/me/security", + name: "security", + component: () => import("@/views/security.vue"), + meta: { title: "Your security" }, + }, + ], + }, + ], + }, + { + path: "/auth", + children: [ + { + path: "sign-in", + name: "auth.sign-in", + component: () => import("@/views/auth/sign-in.vue"), + meta: { public: true, title: "Sign in" }, + }, + { + path: "sign-up", + name: "auth.sign-up", + component: () => import("@/views/auth/sign-up.vue"), + meta: { public: true, title: "Sign up" }, + }, + { + path: "o/connect", + name: "openid.connect", + component: () => import("@/views/auth/connect.vue"), + }, + + { + path: "/me/confirm", + name: "callback.confirm", + component: () => import("@/views/confirm.vue"), + meta: { public: true, title: "Confirm registration" }, + }, + ], + }, + ], +}) + +router.beforeEach(async (to, from, next) => { + const id = useUserinfo() + if (!id.isReady) { + await id.readProfiles() + } + + if (to.meta.title) { + document.title = `Solarpass | ${to.meta.title}` + } else { + document.title = "Solarpass" + } + + if (!to.meta.public && !id.userinfo.isLoggedIn) { + next({ name: "auth.sign-in", query: { redirect_uri: to.fullPath } }) + } else { + next() + } +}) + +export default router diff --git a/web/src/scripts/request.ts b/web/src/scripts/request.ts new file mode 100755 index 0000000..3384bc4 --- /dev/null +++ b/web/src/scripts/request.ts @@ -0,0 +1,3 @@ +export async function request(input: string, init?: RequestInit) { + return await fetch(input, init) +} diff --git a/web/src/stores/notifications.ts b/web/src/stores/notifications.ts new file mode 100755 index 0000000..974b64c --- /dev/null +++ b/web/src/stores/notifications.ts @@ -0,0 +1,64 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { checkLoggedIn, getAtk } from "@/stores/userinfo"; +import { request } from "@/scripts/request"; + +export const useNotifications = defineStore("notifications", () => { + let socket: WebSocket; + + const loading = ref(false); + + const notifications = ref([]); + const total = ref(0) + + async function list() { + loading.value = true; + const res = await request( + "/api/notifications?" + + new URLSearchParams({ + take: (25).toString(), + offset: (0).toString() + }), + { + headers: { Authorization: `Bearer ${getAtk()}` } + } + ); + if (res.status === 200) { + const data = await res.json(); + notifications.value = data["data"]; + total.value = data["count"]; + } + loading.value = false; + } + + function remove(idx: number) { + notifications.value.splice(idx, 1) + total.value--; + } + + async function connect() { + if (!(checkLoggedIn())) return; + + const uri = `ws://${window.location.host}/api/notifications/listen`; + + socket = new WebSocket(uri + `?tk=${getAtk() as string}`); + + socket.addEventListener("open", (event) => { + console.log("[NOTIFICATIONS] The listen websocket has been established... ", event.type); + }); + socket.addEventListener("close", (event) => { + console.warn("[NOTIFICATIONS] The listen websocket is disconnected... ", event.reason, event.code); + }); + socket.addEventListener("message", (event) => { + const data = JSON.parse(event.data); + notifications.value.push(data); + total.value++; + }); + } + + function disconnect() { + socket.close(); + } + + return { loading, notifications, total, list, remove, connect, disconnect }; +}); \ No newline at end of file diff --git a/web/src/stores/userinfo.ts b/web/src/stores/userinfo.ts new file mode 100755 index 0000000..642e3f1 --- /dev/null +++ b/web/src/stores/userinfo.ts @@ -0,0 +1,54 @@ +import Cookie from "universal-cookie" +import { defineStore } from "pinia" +import { ref } from "vue" +import { request } from "@/scripts/request" + +export interface Userinfo { + isLoggedIn: boolean + displayName: string + data: any +} + +const defaultUserinfo: Userinfo = { + isLoggedIn: false, + displayName: "Citizen", + data: null, +} + +export function getAtk(): string { + return new Cookie().get("__hydrogen_atk") +} + +export function checkLoggedIn(): boolean { + return new Cookie().get("__hydrogen_rtk") +} + +export const useUserinfo = defineStore("userinfo", () => { + const userinfo = ref(defaultUserinfo) + const isReady = ref(false) + + async function readProfiles() { + if (!checkLoggedIn()) { + isReady.value = true + } + + const res = await request("/api/users/me", { + headers: { Authorization: `Bearer ${getAtk()}` }, + }) + + if (res.status !== 200) { + return + } + + const data = await res.json() + + isReady.value = true + userinfo.value = { + isLoggedIn: true, + displayName: data["nick"], + data: data, + } + } + + return { userinfo, isReady, readProfiles } +}) diff --git a/web/src/views/auth/claims.ts b/web/src/views/auth/claims.ts new file mode 100755 index 0000000..6ca79e5 --- /dev/null +++ b/web/src/views/auth/claims.ts @@ -0,0 +1,13 @@ +export interface ClaimType { + icon: string + name: string + description: string +} + +export const claims: { [id: string]: ClaimType } = { + openid: { + icon: "mdi-identifier", + name: "Open Identity", + description: "Allow them to read your personal information.", + }, +} diff --git a/web/src/views/auth/connect.vue b/web/src/views/auth/connect.vue new file mode 100755 index 0000000..e7285e3 --- /dev/null +++ b/web/src/views/auth/connect.vue @@ -0,0 +1,192 @@ + + + + + diff --git a/web/src/views/auth/sign-in.vue b/web/src/views/auth/sign-in.vue new file mode 100755 index 0000000..dfa9fe8 --- /dev/null +++ b/web/src/views/auth/sign-in.vue @@ -0,0 +1,76 @@ + + + + + diff --git a/web/src/views/auth/sign-up.vue b/web/src/views/auth/sign-up.vue new file mode 100755 index 0000000..a206dc3 --- /dev/null +++ b/web/src/views/auth/sign-up.vue @@ -0,0 +1,162 @@ + + + + + diff --git a/web/src/views/confirm.vue b/web/src/views/confirm.vue new file mode 100755 index 0000000..c16b984 --- /dev/null +++ b/web/src/views/confirm.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/web/src/views/dashboard.vue b/web/src/views/dashboard.vue new file mode 100755 index 0000000..750e1c4 --- /dev/null +++ b/web/src/views/dashboard.vue @@ -0,0 +1,77 @@ + + + + + + + diff --git a/web/src/views/personal-page.vue b/web/src/views/personal-page.vue new file mode 100755 index 0000000..782b1df --- /dev/null +++ b/web/src/views/personal-page.vue @@ -0,0 +1,71 @@ + + + diff --git a/web/src/views/personalize.vue b/web/src/views/personalize.vue new file mode 100755 index 0000000..220a185 --- /dev/null +++ b/web/src/views/personalize.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/web/src/views/security.vue b/web/src/views/security.vue new file mode 100755 index 0000000..979527f --- /dev/null +++ b/web/src/views/security.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json new file mode 100755 index 0000000..e14c754 --- /dev/null +++ b/web/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100755 index 0000000..66b5e57 --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + } + ] +} diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json new file mode 100755 index 0000000..2c669ee --- /dev/null +++ b/web/tsconfig.node.json @@ -0,0 +1,13 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"], + "compilerOptions": { + "composite": true, + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/web/uno.config.ts b/web/uno.config.ts new file mode 100755 index 0000000..2d323f7 --- /dev/null +++ b/web/uno.config.ts @@ -0,0 +1,5 @@ +import { defineConfig, presetAttributify, presetTypography, presetUno } from "unocss" + +export default defineConfig({ + presets: [presetAttributify(), presetTypography(), presetUno({ preflight: false })], +}) diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100755 index 0000000..41b2b40 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,27 @@ +import { fileURLToPath, URL } from "node:url"; + +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import vueJsx from "@vitejs/plugin-vue-jsx"; +import unocss from "unocss/vite"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue(), vueJsx(), unocss()], + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)) + } + }, + server: { + proxy: { + "/api/ws": { + target: "ws://localhost:8444", + ws: true + }, + + "/api": "http://localhost:8444", + "/.well-known": "http://localhost:8444" + } + } +});