diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..c0eeaef --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dataSources/723637bc-6ce3-4bbe-afb3-d88730c75a1b.xml b/.idea/dataSources/723637bc-6ce3-4bbe-afb3-d88730c75a1b.xml index 308becf..79223f4 100644 --- a/.idea/dataSources/723637bc-6ce3-4bbe-afb3-d88730c75a1b.xml +++ b/.idea/dataSources/723637bc-6ce3-4bbe-afb3-d88730c75a1b.xml @@ -5024,6 +5024,7 @@ true posixrules 16445 + 1257512postgres diff --git a/.idea/workspace.xml b/.idea/workspace.xml index f857090..9dde2a4 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,9 +4,13 @@ - - - - - - - @@ -168,7 +166,14 @@ - 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/database/migrator.go b/pkg/internal/database/migrator.go index febeaf2..61592eb 100644 --- a/pkg/internal/database/migrator.go +++ b/pkg/internal/database/migrator.go @@ -9,7 +9,6 @@ var AutoMaintainRange = []any{ &models.Account{}, &models.AuthFactor{}, &models.AccountProfile{}, - &models.AccountPage{}, &models.AccountContact{}, &models.AccountFriendship{}, &models.Badge{}, 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/models/accounts.go b/pkg/internal/models/accounts.go index 49bff98..df188a8 100644 --- a/pkg/internal/models/accounts.go +++ b/pkg/internal/models/accounts.go @@ -21,7 +21,6 @@ type Account struct { PermNodes datatypes.JSONMap `json:"perm_nodes"` Profile AccountProfile `json:"profile"` - PersonalPage AccountPage `json:"personal_page"` Badges []Badge `json:"badges"` Contacts []AccountContact `json:"contacts"` RealmIdentities []RealmMember `json:"realm_identities"` diff --git a/pkg/internal/models/profiles.go b/pkg/internal/models/profiles.go index 1800953..1aa5452 100644 --- a/pkg/internal/models/profiles.go +++ b/pkg/internal/models/profiles.go @@ -1,7 +1,6 @@ package models import ( - "gorm.io/datatypes" "time" ) @@ -14,18 +13,3 @@ type AccountProfile struct { Birthday *time.Time `json:"birthday"` AccountID uint `json:"account_id"` } - -type AccountPage struct { - BaseModel - - Content string `json:"content"` - Script string `json:"script"` - Style string `json:"style"` - Links datatypes.JSONSlice[AccountPageLinks] `json:"links"` - AccountID uint `json:"account_id"` -} - -type AccountPageLinks struct { - Label string `json:"label"` - Url string `json:"url"` -} diff --git a/pkg/internal/models/tokens.go b/pkg/internal/models/tokens.go index 9c432a1..760d7b4 100644 --- a/pkg/internal/models/tokens.go +++ b/pkg/internal/models/tokens.go @@ -14,6 +14,6 @@ type MagicToken struct { Code string `json:"code"` Type int8 `json:"type"` - AssignTo *uint `json:"assign_to"` + AccountID *uint `json:"account_id"` ExpiredAt *time.Time `json:"expired_at"` } diff --git a/pkg/internal/server/api/accounts_api.go b/pkg/internal/server/api/accounts_api.go index 8fbcc84..d2531aa 100644 --- a/pkg/internal/server/api/accounts_api.go +++ b/pkg/internal/server/api/accounts_api.go @@ -123,7 +123,7 @@ func editUserinfo(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) } -func killSession(c *fiber.Ctx) error { +func killTicket(c *fiber.Ctx) error { if err := exts.EnsureAuthenticated(c); err != nil { return err } diff --git a/pkg/internal/server/api/auth_api.go b/pkg/internal/server/api/auth_api.go index 4c8b24c..dd6f546 100644 --- a/pkg/internal/server/api/auth_api.go +++ b/pkg/internal/server/api/auth_api.go @@ -23,6 +23,8 @@ func doAuthenticate(c *fiber.Ctx) error { user, err := services.LookupAccount(data.Username) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error())) + } else if user.ConfirmedAt == nil { + return fiber.NewError(fiber.StatusForbidden, "account was not confirmed") } ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent)) @@ -36,7 +38,7 @@ func doAuthenticate(c *fiber.Ctx) error { } return c.JSON(fiber.Map{ - "is_finished": ticket.IsAvailable(), + "is_finished": ticket.IsAvailable() == nil, "ticket": ticket, }) } @@ -68,7 +70,7 @@ func doMultiFactorAuthenticate(c *fiber.Ctx) error { } return c.JSON(fiber.Map{ - "is_finished": ticket.IsAvailable(), + "is_finished": ticket.IsAvailable() == nil, "ticket": ticket, }) } diff --git a/pkg/internal/server/api/factors_api.go b/pkg/internal/server/api/factors_api.go index 98a7c46..99bf731 100644 --- a/pkg/internal/server/api/factors_api.go +++ b/pkg/internal/server/api/factors_api.go @@ -1,10 +1,29 @@ package api import ( + "fmt" "git.solsynth.dev/hydrogen/passport/pkg/internal/services" "github.com/gofiber/fiber/v2" ) +func getAvailableFactors(c *fiber.Ctx) error { + ticketId := c.QueryInt("ticketId", 0) + if ticketId <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "must provide ticket id as a query parameter") + } + + ticket, err := services.GetTicket(uint(ticketId)) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket was not found: %v", err)) + } + factors, err := services.ListUserFactor(ticket.AccountID) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(factors) +} + func requestFactorToken(c *fiber.Ctx) error { id, _ := c.ParamsInt("factorId", 0) diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go index 9bcb2e7..cb828b3 100644 --- a/pkg/internal/server/api/index.go +++ b/pkg/internal/server/api/index.go @@ -26,12 +26,10 @@ func MapAPIs(app *fiber.App) { me.Put("/banner", setBanner) me.Get("/", getUserinfo) - me.Get("/page", getOwnPersonalPage) me.Put("/", editUserinfo) - me.Put("/page", editPersonalPage) me.Get("/events", getEvents) me.Get("/tickets", getTickets) - me.Delete("/tickets/:ticketId", killSession) + me.Delete("/tickets/:ticketId", killTicket) me.Post("/confirm", doRegisterConfirm) @@ -49,14 +47,22 @@ func MapAPIs(app *fiber.App) { directory := api.Group("/users/:alias").Name("User Directory") { directory.Get("/", getOtherUserinfo) - directory.Get("/page", getPersonalPage) } api.Post("/users", doRegister) - api.Post("/auth", doAuthenticate) - api.Post("/auth/token", getToken) - api.Post("/auth/factors/:factorId", requestFactorToken) + auth := api.Group("/auth").Name("Auth") + { + auth.Post("/", doAuthenticate) + auth.Post("/mfa", doMultiFactorAuthenticate) + auth.Post("/token", getToken) + + auth.Get("/factors", getAvailableFactors) + auth.Post("/factors/:factorId", requestFactorToken) + + auth.Get("/o/authorize", tryAuthorizeThirdClient) + auth.Post("/o/authorize", authorizeThirdClient) + } realms := api.Group("/realms").Name("Realms API") { @@ -85,5 +91,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/ui/oauth.go b/pkg/internal/server/api/oauth_api.go old mode 100644 new mode 100755 similarity index 51% rename from pkg/internal/server/ui/oauth.go rename to pkg/internal/server/api/oauth_api.go index 946a17c..4872d97 --- a/pkg/internal/server/ui/oauth.go +++ b/pkg/internal/server/api/oauth_api.go @@ -1,111 +1,76 @@ -package ui +package api 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" + + "git.solsynth.dev/hydrogen/passport/pkg/internal/database" + "git.solsynth.dev/hydrogen/passport/pkg/internal/models" + "git.solsynth.dev/hydrogen/passport/pkg/internal/services" + "github.com/gofiber/fiber/v2" + "github.com/samber/lo" ) -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) - +func tryAuthorizeThirdClient(c *fiber.Ctx) error { 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" + return fiber.NewError(fiber.StatusBadRequest, "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) + return fiber.NewError(fiber.StatusNotFound, err.Error()) } else if !client.IsDraft && !lo.Contains(client.Callbacks, strings.Split(redirect, "?")[0]) { - message = "invalid callback url" + return fiber.NewError(fiber.StatusBadRequest, "invalid callback url") } + if err := exts.EnsureAuthenticated(c); err != nil { + return err + } + user := c.Locals("user").(models.Account) + 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()) { + if ticket.ExpiredAt != nil && ticket.ExpiredAt.Unix() < time.Now().Unix() { + return c.JSON(fiber.Map{ + "client": client, + "ticket": nil, + }) + } else { 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"), - )) - } - } } + + return c.JSON(fiber.Map{ + "client": client, + "ticket": ticket, + }) } - 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") + return c.JSON(fiber.Map{ + "client": client, + "ticket": nil, + }) } -func authorizeAction(c *fiber.Ctx) error { - if err := exts.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) +func authorizeThirdClient(c *fiber.Ctx) error { id := c.Query("client_id") response := c.Query("response_type") redirect := c.Query("redirect_uri") scope := c.Query("scope") + if len(scope) <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "invalid request params") + } 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) + return err } + user := c.Locals("user").(models.Account) var client models.ThirdClient if err := database.C.Where(&models.ThirdClient{Alias: id}).First(&client).Error; err != nil { @@ -128,12 +93,10 @@ func authorizeAction(c *fiber.Ctx) error { 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"), - )) + return c.JSON(fiber.Map{ + "ticket": ticket, + "redirect_uri": redirect, + }) } case "token": // OAuth Implicit Mode @@ -152,15 +115,14 @@ func authorizeAction(c *fiber.Ctx) error { 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"), - )) + return c.JSON(fiber.Map{ + "access_token": access, + "refresh_token": refresh, + "redirect_uri": redirect, + "ticket": ticket, + }) } default: - return flash.WithInfo(c, fiber.Map{ - "message": "unsupported response type", - }).Redirect(redirectBackUri) + return fiber.NewError(fiber.StatusBadRequest, "unsupported response type") } } diff --git a/pkg/internal/server/api/page_api.go b/pkg/internal/server/api/page_api.go deleted file mode 100644 index 77c7c43..0000000 --- a/pkg/internal/server/api/page_api.go +++ /dev/null @@ -1,76 +0,0 @@ -package api - -import ( - "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" - "github.com/gofiber/fiber/v2" -) - -func getPersonalPage(c *fiber.Ctx) error { - alias := c.Params("alias") - - var account models.Account - if err := database.C. - Where(&models.Account{Name: alias}). - First(&account).Error; err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - var page models.AccountPage - if err := database.C. - Where(&models.AccountPage{AccountID: account.ID}). - First(&page).Error; err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(page) -} - -func getOwnPersonalPage(c *fiber.Ctx) error { - if err := exts.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var page models.AccountPage - if err := database.C. - Where(&models.AccountPage{AccountID: user.ID}). - FirstOrCreate(&page, &models.AccountPage{AccountID: user.ID}).Error; err != nil { - return fiber.NewError(fiber.StatusBadRequest, err.Error()) - } - - return c.JSON(page) -} - -func editPersonalPage(c *fiber.Ctx) error { - if err := exts.EnsureAuthenticated(c); err != nil { - return err - } - user := c.Locals("user").(models.Account) - - var data struct { - Content string `json:"content"` - Links []models.AccountPageLinks `json:"links"` - } - - if err := exts.BindAndValidate(c, &data); err != nil { - return err - } - - var page models.AccountPage - if err := database.C. - Where(&models.AccountPage{AccountID: user.ID}). - FirstOrInit(&page).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - page.Content = data.Content - page.Links = data.Links - - if err := database.C.Save(&page).Error; err != nil { - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - return c.SendStatus(fiber.StatusOK) -} 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/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/services/accounts.go b/pkg/internal/services/accounts.go index a84d9f9..a4b23a0 100644 --- a/pkg/internal/services/accounts.go +++ b/pkg/internal/services/accounts.go @@ -2,6 +2,7 @@ package services import ( "fmt" + "github.com/rs/zerolog/log" "github.com/spf13/viper" "gorm.io/datatypes" "time" @@ -93,7 +94,7 @@ func ConfirmAccount(code string) error { var user models.Account if err := database.C.Where(&models.Account{ - BaseModel: models.BaseModel{ID: *token.AssignTo}, + BaseModel: models.BaseModel{ID: *token.AccountID}, }).First(&user).Error; err != nil { return err } @@ -121,3 +122,49 @@ func ConfirmAccount(code string) error { return nil }) } + +func DeleteAccount(id uint) error { + tx := database.C.Begin() + + for _, model := range []any{ + &models.Badge{}, + &models.RealmMember{}, + &models.AccountContact{}, + &models.AuthFactor{}, + &models.AuthTicket{}, + &models.MagicToken{}, + &models.ThirdClient{}, + &models.Notification{}, + &models.NotificationSubscriber{}, + &models.AccountFriendship{}, + } { + if err := tx.Delete(model, "account_id = ?", id).Error; err != nil { + tx.Rollback() + return err + } + } + + if err := tx.Delete(&models.Account{}, "id = ?", id).Error; err != nil { + tx.Rollback() + return err + } + + return tx.Commit().Error +} + +func RecycleUnConfirmAccount() { + var hitList []models.Account + if err := database.C.Where("confirmed_at IS NULL").Find(&hitList).Error; err != nil { + log.Error().Err(err).Msg("An error occurred while recycling accounts...") + return + } + + if len(hitList) > 0 { + log.Info().Int("count", len(hitList)).Msg("Going to recycle those un-confirmed accounts...") + for _, entry := range hitList { + if err := DeleteAccount(entry.ID); err != nil { + log.Error().Err(err).Msg("An error occurred while recycling accounts...") + } + } + } +} diff --git a/pkg/internal/services/ticket.go b/pkg/internal/services/ticket.go index 7039e0c..b9ecf68 100644 --- a/pkg/internal/services/ticket.go +++ b/pkg/internal/services/ticket.go @@ -12,22 +12,13 @@ import ( ) func DetectRisk(user models.Account, ip, ua string) bool { - var availableFactor int64 - if err := database.C. - Where(models.AuthFactor{AccountID: user.ID}). - Where("type != ?", models.PasswordAuthFactor). - Model(models.AuthFactor{}). - Where(&availableFactor); err != nil || availableFactor <= 0 { - return false - } - - var secureFactor int64 + var clue int64 if err := database.C. Where(models.AuthTicket{AccountID: user.ID, IpAddress: ip}). Where("available_at IS NOT NULL"). Model(models.AuthTicket{}). - Count(&secureFactor).Error; err == nil { - if secureFactor >= 1 { + Count(&clue).Error; err == nil { + if clue >= 1 { return false } } @@ -81,7 +72,7 @@ func NewOauthTicket( AccessToken: lo.ToPtr(uuid.NewString()), RefreshToken: lo.ToPtr(uuid.NewString()), AvailableAt: lo.ToPtr(time.Now()), - ExpiredAt: lo.ToPtr(time.Now()), + ExpiredAt: lo.ToPtr(time.Now().Add(7 * 24 * time.Hour)), ClientID: &client.ID, AccountID: user.ID, } diff --git a/pkg/internal/services/tokens.go b/pkg/internal/services/tokens.go index f297444..9dd9419 100644 --- a/pkg/internal/services/tokens.go +++ b/pkg/internal/services/tokens.go @@ -47,7 +47,7 @@ func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expired token := models.MagicToken{ Code: strings.Replace(uuid.NewString(), "-", "", -1), Type: mode, - AssignTo: &uid, + AccountID: &uid, ExpiredAt: expiredAt, } @@ -59,13 +59,13 @@ func NewMagicToken(mode models.MagicTokenType, assignTo *models.Account, expired } func NotifyMagicToken(token models.MagicToken) error { - if token.AssignTo == nil { + if token.AccountID == nil { return fmt.Errorf("could notify a non-assign magic token") } var user models.Account if err := database.C.Where(&models.Account{ - BaseModel: models.BaseModel{ID: *token.AssignTo}, + BaseModel: models.BaseModel{ID: *token.AccountID}, }).Preload("Contacts").First(&user).Error; err != nil { return err } 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..dae1fd4 100644 --- a/pkg/main.go +++ b/pkg/main.go @@ -58,18 +58,17 @@ 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))) quartz.AddFunc("@every 60m", services.DoAutoSignoff) quartz.AddFunc("@every 60m", services.DoAutoDatabaseCleanup) quartz.AddFunc("@every 60s", services.RecycleAuthContext) + quartz.AddFunc("@every 60m", services.RecycleUnConfirmAccount) quartz.AddFunc("@every 5m", services.KexCleanup) quartz.Start() diff --git a/settings.toml b/settings.toml index 7c60c2a..8e6d066 100644 --- a/settings.toml +++ b/settings.toml @@ -1,4 +1,7 @@ id = "passport01" +name = "Solarpass" + +frontend_app = "web/dist" bind = "0.0.0.0:8444" grpc_bind = "0.0.0.0:7444" 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..873ab83 --- /dev/null +++ b/web/src/components/Copyright.vue @@ -0,0 +1,6 @@ + diff --git a/web/src/components/GoUseSolian.vue b/web/src/components/GoUseSolian.vue new file mode 100644 index 0000000..834cfc9 --- /dev/null +++ b/web/src/components/GoUseSolian.vue @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/web/src/components/NotificationList.vue b/web/src/components/NotificationList.vue new file mode 100755 index 0000000..2c5b79d --- /dev/null +++ b/web/src/components/NotificationList.vue @@ -0,0 +1,68 @@ + + + diff --git a/web/src/components/UserMenu.vue b/web/src/components/UserMenu.vue new file mode 100755 index 0000000..0786ed8 --- /dev/null +++ b/web/src/components/UserMenu.vue @@ -0,0 +1,53 @@ + + + diff --git a/web/src/components/auth/Authenticate.vue b/web/src/components/auth/Authenticate.vue new file mode 100755 index 0000000..22fc05a --- /dev/null +++ b/web/src/components/auth/Authenticate.vue @@ -0,0 +1,65 @@ + + + diff --git a/web/src/components/auth/AuthenticateCompleted.vue b/web/src/components/auth/AuthenticateCompleted.vue new file mode 100644 index 0000000..4ac6109 --- /dev/null +++ b/web/src/components/auth/AuthenticateCompleted.vue @@ -0,0 +1,68 @@ + + + \ No newline at end of file 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..26155f0 --- /dev/null +++ b/web/src/components/auth/FactorApplicator.vue @@ -0,0 +1,93 @@ + + + diff --git a/web/src/components/auth/FactorPicker.vue b/web/src/components/auth/FactorPicker.vue new file mode 100755 index 0000000..dde94fd --- /dev/null +++ b/web/src/components/auth/FactorPicker.vue @@ -0,0 +1,88 @@ + + + diff --git a/web/src/components/navigation/AppBar.vue b/web/src/components/navigation/AppBar.vue new file mode 100644 index 0000000..a369aeb --- /dev/null +++ b/web/src/components/navigation/AppBar.vue @@ -0,0 +1,49 @@ + + + + + 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..6c8ef89 --- /dev/null +++ b/web/src/layouts/master.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/web/src/layouts/user-center.vue b/web/src/layouts/user-center.vue new file mode 100755 index 0000000..baa61ef --- /dev/null +++ b/web/src/layouts/user-center.vue @@ -0,0 +1,30 @@ + + + + + \ 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..388f706 --- /dev/null +++ b/web/src/router/index.ts @@ -0,0 +1,87 @@ +import { createRouter, createWebHistory } from "vue-router" +import { useUserinfo } from "@/stores/userinfo" +import UserCenterLayout from "@/layouts/user-center.vue" + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: "/", + redirect: { name: "dashboard" }, + meta: { public: true }, + }, + { + path: "/users", + component: UserCenterLayout, + children: [ + { + path: "/me", + 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/security", + name: "security", + component: () => import("@/views/security.vue"), + meta: { title: "Your security" }, + }, + ], + }, + { + path: "/", + 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: "authorize", + name: "oauth.authorize", + component: () => import("@/views/auth/authorize.vue"), + }, + ], + }, + { + path: "/users/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..784f582 --- /dev/null +++ b/web/src/stores/notifications.ts @@ -0,0 +1,66 @@ +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/ws` + + 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) + if (data["w"] == "notifications.new") { + notifications.value.push(data["p"]) + 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..048b03f --- /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 +} + +export 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/authorize.vue b/web/src/views/auth/authorize.vue new file mode 100755 index 0000000..f583145 --- /dev/null +++ b/web/src/views/auth/authorize.vue @@ -0,0 +1,196 @@ + + + + + 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/sign-in.vue b/web/src/views/auth/sign-in.vue new file mode 100755 index 0000000..47c39c3 --- /dev/null +++ b/web/src/views/auth/sign-in.vue @@ -0,0 +1,67 @@ + + + + + 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..734232a --- /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/personalize.vue b/web/src/views/personalize.vue new file mode 100755 index 0000000..b902e4c --- /dev/null +++ b/web/src/views/personalize.vue @@ -0,0 +1,157 @@ + + + + + diff --git a/web/src/views/security.vue b/web/src/views/security.vue new file mode 100755 index 0000000..fa75c55 --- /dev/null +++ b/web/src/views/security.vue @@ -0,0 +1,197 @@ + + + + + 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" + } + } +});