🎨 Update project structure
This commit is contained in:
179
pkg/internal/server/accounts_api.go
Normal file
179
pkg/internal/server/accounts_api.go
Normal file
@ -0,0 +1,179 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func getUserinfo(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data models.Account
|
||||
if err := database.C.
|
||||
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
|
||||
Preload("Profile").
|
||||
Preload("Contacts").
|
||||
Preload("Badges").
|
||||
First(&data).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
var resp fiber.Map
|
||||
raw, _ := jsoniter.Marshal(data)
|
||||
jsoniter.Unmarshal(raw, &resp)
|
||||
|
||||
resp["sub"] = strconv.Itoa(int(data.ID))
|
||||
resp["family_name"] = data.Profile.FirstName
|
||||
resp["given_name"] = data.Profile.LastName
|
||||
resp["name"] = data.Name
|
||||
resp["email"] = data.GetPrimaryEmail().Content
|
||||
resp["preferred_username"] = data.Nick
|
||||
|
||||
if data.Avatar != nil {
|
||||
resp["picture"] = *data.GetAvatar()
|
||||
}
|
||||
|
||||
return c.JSON(resp)
|
||||
}
|
||||
|
||||
func getEvents(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
var count int64
|
||||
var events []models.ActionEvent
|
||||
if err := database.C.
|
||||
Where(&models.ActionEvent{AccountID: user.ID}).
|
||||
Model(&models.ActionEvent{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := database.C.
|
||||
Order("created_at desc").
|
||||
Where(&models.ActionEvent{AccountID: user.ID}).
|
||||
Limit(take).
|
||||
Offset(offset).
|
||||
Find(&events).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": events,
|
||||
})
|
||||
}
|
||||
|
||||
func editUserinfo(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Nick string `json:"nick" validate:"required,min=4,max=24"`
|
||||
Description string `json:"description"`
|
||||
FirstName string `json:"first_name"`
|
||||
LastName string `json:"last_name"`
|
||||
Birthday time.Time `json:"birthday"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.
|
||||
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
|
||||
Preload("Profile").
|
||||
First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
account.Nick = data.Nick
|
||||
account.Description = data.Description
|
||||
account.Profile.FirstName = data.FirstName
|
||||
account.Profile.LastName = data.LastName
|
||||
account.Profile.Birthday = &data.Birthday
|
||||
|
||||
if err := database.C.Save(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else if err := database.C.Save(&account.Profile).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
services.InvalidAuthCacheWithUser(account.ID)
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func killSession(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("ticketId", 0)
|
||||
|
||||
if err := database.C.Delete(&models.AuthTicket{}, &models.AuthTicket{
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
AccountID: user.ID,
|
||||
}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func doRegister(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Name string `json:"name" validate:"required,lowercase,alphanum,min=4,max=16"`
|
||||
Nick string `json:"nick" validate:"required,min=4,max=24"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
Password string `json:"password" validate:"required,min=4,max=32"`
|
||||
MagicToken string `json:"magic_token"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if viper.GetBool("use_registration_magic_token") && len(data.MagicToken) <= 0 {
|
||||
return fmt.Errorf("missing magic token in request")
|
||||
} else if viper.GetBool("use_registration_magic_token") {
|
||||
if tk, err := services.ValidateMagicToken(data.MagicToken, models.RegistrationMagicToken); err != nil {
|
||||
return err
|
||||
} else {
|
||||
database.C.Delete(&tk)
|
||||
}
|
||||
}
|
||||
|
||||
if user, err := services.CreateAccount(
|
||||
data.Name,
|
||||
data.Nick,
|
||||
data.Email,
|
||||
data.Password,
|
||||
); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(user)
|
||||
}
|
||||
}
|
||||
|
||||
func doRegisterConfirm(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := services.ConfirmAccount(data.Code); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
63
pkg/internal/server/admin/badges_api.go
Normal file
63
pkg/internal/server/admin/badges_api.go
Normal file
@ -0,0 +1,63 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func grantBadge(c *fiber.Ctx) error {
|
||||
if err := utils.CheckPermissions(c, "AdminGrantBadges", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
AccountID uint `json:"account_id"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var account models.Account
|
||||
if account, err = services.GetAccount(data.AccountID); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("target account was not found: %v", err))
|
||||
}
|
||||
|
||||
badge := models.Badge{
|
||||
Type: data.Type,
|
||||
Metadata: data.Metadata,
|
||||
}
|
||||
|
||||
if err := services.GrantBadge(account, badge); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(badge)
|
||||
}
|
||||
}
|
||||
|
||||
func revokeBadge(c *fiber.Ctx) error {
|
||||
if err := utils.CheckPermissions(c, "AdminRevokeBadges", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
id, _ := c.ParamsInt("badgeId", 0)
|
||||
|
||||
var badge models.Badge
|
||||
if err := database.C.Where("id = ?", id).First(&badge).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("target badge was not found: %v", err))
|
||||
}
|
||||
|
||||
if err := services.RevokeBadge(badge); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(badge)
|
||||
}
|
||||
}
|
13
pkg/internal/server/admin/index.go
Normal file
13
pkg/internal/server/admin/index.go
Normal file
@ -0,0 +1,13 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func MapAdminEndpoints(A *fiber.App, authMiddleware fiber.Handler) {
|
||||
admin := A.Group("/api/admin").Use(authMiddleware)
|
||||
{
|
||||
admin.Post("/badges", grantBadge)
|
||||
admin.Delete("/badges/:badgeId", revokeBadge)
|
||||
}
|
||||
}
|
146
pkg/internal/server/auth_api.go
Normal file
146
pkg/internal/server/auth_api.go
Normal file
@ -0,0 +1,146 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
)
|
||||
|
||||
func doAuthenticate(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := services.LookupAccount(data.Username)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
|
||||
}
|
||||
|
||||
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid password: %v", err.Error()))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": ticket.IsAvailable(),
|
||||
"ticket": ticket,
|
||||
})
|
||||
}
|
||||
|
||||
func doMultiFactorAuthenticate(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
TicketID uint `json:"ticket_id" validate:"required"`
|
||||
FactorID uint `json:"factor_id" validate:"required"`
|
||||
Code string `json:"code" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ticket, err := services.GetTicket(data.TicketID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ticket was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
factor, err := services.GetFactor(data.FactorID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("factor was not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
ticket, err = services.ActiveTicketWithMFA(ticket, factor, data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid code: %v", err.Error()))
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"is_finished": ticket.IsAvailable(),
|
||||
"ticket": ticket,
|
||||
})
|
||||
}
|
||||
|
||||
func getToken(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
Code string `json:"code" form:"code"`
|
||||
RefreshToken string `json:"refresh_token" form:"refresh_token"`
|
||||
ClientID string `json:"client_id" form:"client_id"`
|
||||
ClientSecret string `json:"client_secret" form:"client_secret"`
|
||||
Username string `json:"username" form:"username"`
|
||||
Password string `json:"password" form:"password"`
|
||||
RedirectUri string `json:"redirect_uri" form:"redirect_uri"`
|
||||
GrantType string `json:"grant_type" form:"grant_type"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var access, refresh string
|
||||
switch data.GrantType {
|
||||
case "refresh_token":
|
||||
// Refresh Token
|
||||
access, refresh, err = services.RefreshToken(data.RefreshToken)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "authorization_code":
|
||||
// Authorization Code Mode
|
||||
access, refresh, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "password":
|
||||
// Password Mode
|
||||
user, err := services.LookupAccount(data.Username)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("account was not found: %v", err.Error()))
|
||||
}
|
||||
ticket, err := services.NewTicket(user, c.IP(), c.Get(fiber.HeaderUserAgent))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable setup ticket: %v", err.Error()))
|
||||
}
|
||||
ticket, err = services.ActiveTicketWithPassword(ticket, data.Password)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("invalid password: %v", err.Error()))
|
||||
} else if err := ticket.IsAvailable(); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("risk detected: %v (ticketId=%d)", err, ticket.ID))
|
||||
}
|
||||
access, refresh, err = services.ExchangeOauthToken(data.ClientID, data.ClientSecret, data.RedirectUri, *ticket.GrantToken)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
case "grant_token":
|
||||
// Internal Usage
|
||||
access, refresh, err = services.ExchangeToken(data.Code)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
default:
|
||||
return fiber.NewError(fiber.StatusBadRequest, "unsupported exchange token type")
|
||||
}
|
||||
|
||||
services.SetJwtCookieSet(c, access, refresh)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"id_token": access,
|
||||
"access_token": access,
|
||||
"refresh_token": refresh,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": (30 * time.Minute).Seconds(),
|
||||
})
|
||||
}
|
55
pkg/internal/server/auth_middleware.go
Normal file
55
pkg/internal/server/auth_middleware.go
Normal file
@ -0,0 +1,55 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func authMiddleware(c *fiber.Ctx) error {
|
||||
var token string
|
||||
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
|
||||
token = cookie
|
||||
}
|
||||
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
|
||||
tk := strings.Replace(header, "Bearer", "", 1)
|
||||
token = strings.TrimSpace(tk)
|
||||
}
|
||||
if query := c.Query("tk"); len(query) > 0 {
|
||||
token = strings.TrimSpace(query)
|
||||
}
|
||||
|
||||
c.Locals("token", token)
|
||||
|
||||
if err := authFunc(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func authFunc(c *fiber.Ctx, overrides ...string) error {
|
||||
var token string
|
||||
if len(overrides) > 0 {
|
||||
token = overrides[0]
|
||||
} else {
|
||||
if tk, ok := c.Locals("token").(string); !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized)
|
||||
} else {
|
||||
token = tk
|
||||
}
|
||||
}
|
||||
|
||||
rtk := c.Cookies(services.CookieRefreshKey)
|
||||
if ctx, perms, atk, rtk, err := services.Authenticate(token, rtk, 0); err == nil {
|
||||
if atk != token {
|
||||
services.SetJwtCookieSet(c, atk, rtk)
|
||||
}
|
||||
c.Locals("permissions", perms)
|
||||
c.Locals("principal", ctx.Account)
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
73
pkg/internal/server/avatar_api.go
Normal file
73
pkg/internal/server/avatar_api.go
Normal file
@ -0,0 +1,73 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func setAvatar(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
AttachmentID uint `json:"attachment" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := grpc.Attachments.CheckAttachmentExists(context.Background(), &pcpb.AttachmentLookupRequest{
|
||||
Id: lo.ToPtr(uint64(data.AttachmentID)),
|
||||
Usage: lo.ToPtr("p.avatar"),
|
||||
}); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("avatar was not found in repository: %v", err))
|
||||
}
|
||||
|
||||
user.Avatar = &data.AttachmentID
|
||||
|
||||
if err := database.C.Save(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.InvalidAuthCacheWithUser(user.ID)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func setBanner(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
AttachmentID uint `json:"attachment" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := grpc.Attachments.CheckAttachmentExists(context.Background(), &pcpb.AttachmentLookupRequest{
|
||||
Id: lo.ToPtr(uint64(data.AttachmentID)),
|
||||
Usage: lo.ToPtr("p.banner"),
|
||||
}); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("banner was not found in repository: %v", err))
|
||||
}
|
||||
|
||||
user.Banner = &data.AttachmentID
|
||||
|
||||
if err := database.C.Save(&user).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
services.InvalidAuthCacheWithUser(user.ID)
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
23
pkg/internal/server/factors_api.go
Normal file
23
pkg/internal/server/factors_api.go
Normal file
@ -0,0 +1,23 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func requestFactorToken(c *fiber.Ctx) error {
|
||||
id, _ := c.ParamsInt("factorId", 0)
|
||||
|
||||
factor, err := services.GetFactor(uint(id))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if sent, err := services.GetFactorCode(factor); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if !sent {
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
123
pkg/internal/server/friendships_api.go
Normal file
123
pkg/internal/server/friendships_api.go
Normal file
@ -0,0 +1,123 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listFriendship(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
status := c.QueryInt("status", -1)
|
||||
|
||||
var err error
|
||||
var friends []models.AccountFriendship
|
||||
if status < 0 {
|
||||
if friends, err = services.ListAllFriend(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
} else {
|
||||
if friends, err = services.ListFriend(user, models.FriendshipStatus(status)); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(friends)
|
||||
}
|
||||
|
||||
func getFriendship(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
relatedId, _ := c.ParamsInt("relatedId", 0)
|
||||
|
||||
related, err := services.GetAccount(uint(relatedId))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if friend, err := services.GetFriendWithTwoSides(user.ID, related.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(friend)
|
||||
}
|
||||
}
|
||||
|
||||
func makeFriendship(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
relatedName := c.Query("related")
|
||||
relatedId, _ := c.ParamsInt("relatedId", 0)
|
||||
|
||||
var err error
|
||||
var related models.Account
|
||||
if relatedId > 0 {
|
||||
related, err = services.GetAccount(uint(relatedId))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
} else if len(relatedName) > 0 {
|
||||
related, err = services.LookupAccount(relatedName)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "must one of username or user id")
|
||||
}
|
||||
|
||||
friend, err := services.NewFriend(user, related, models.FriendshipPending)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(friend)
|
||||
}
|
||||
}
|
||||
|
||||
func editFriendship(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
relatedId, _ := c.ParamsInt("relatedId", 0)
|
||||
|
||||
var data struct {
|
||||
Status uint8 `json:"status"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
related, err := services.GetAccount(uint(relatedId))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
friendship, err := services.GetFriendWithTwoSides(user.ID, related.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
originalStatus := friendship.Status
|
||||
friendship.Status = models.FriendshipStatus(data.Status)
|
||||
|
||||
if friendship, err := services.EditFriendWithCheck(friendship, user, originalStatus); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(friendship)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteFriendship(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
relatedId, _ := c.ParamsInt("relatedId", 0)
|
||||
|
||||
related, err := services.GetAccount(uint(relatedId))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
friendship, err := services.GetFriendWithTwoSides(user.ID, related.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.DeleteFriend(friendship); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(friendship)
|
||||
}
|
||||
}
|
111
pkg/internal/server/notifications_api.go
Normal file
111
pkg/internal/server/notifications_api.go
Normal file
@ -0,0 +1,111 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getNotifications(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
tx := database.C.Where(&models.Notification{RecipientID: user.ID}).Model(&models.Notification{})
|
||||
|
||||
var count int64
|
||||
var notifications []models.Notification
|
||||
if err := tx.
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := tx.
|
||||
Limit(take).
|
||||
Offset(offset).
|
||||
Find(¬ifications).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": notifications,
|
||||
})
|
||||
}
|
||||
|
||||
func markNotificationRead(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("notificationId", 0)
|
||||
|
||||
var notify models.Notification
|
||||
if err := database.C.Where(&models.Notification{
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
RecipientID: user.ID,
|
||||
}).First(¬ify).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := database.C.Delete(¬ify).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func markNotificationReadBatch(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
MessageIDs []uint `json:"messages"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := database.C.Model(&models.Notification{}).
|
||||
Where("recipient_id = ? AND id IN ?", user.ID, data.MessageIDs).
|
||||
Delete(&models.Notification{}).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func addNotifySubscriber(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Provider string `json:"provider" validate:"required"`
|
||||
DeviceToken string `json:"device_token" validate:"required"`
|
||||
DeviceID string `json:"device_id" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err := database.C.Where(&models.NotificationSubscriber{
|
||||
DeviceID: data.DeviceID,
|
||||
DeviceToken: data.DeviceToken,
|
||||
AccountID: user.ID,
|
||||
}).Model(&models.NotificationSubscriber{}).Count(&count).Error; err != nil || count > 0 {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
subscriber, err := services.AddNotifySubscriber(
|
||||
user,
|
||||
data.Provider,
|
||||
data.DeviceID,
|
||||
data.DeviceToken,
|
||||
c.Get(fiber.HeaderUserAgent),
|
||||
)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(subscriber)
|
||||
}
|
60
pkg/internal/server/notify_api.go
Normal file
60
pkg/internal/server/notify_api.go
Normal file
@ -0,0 +1,60 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func notifyUser(c *fiber.Ctx) error {
|
||||
var data struct {
|
||||
ClientID string `json:"client_id" validate:"required"`
|
||||
ClientSecret string `json:"client_secret" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Subject string `json:"subject" validate:"required,max=1024"`
|
||||
Content string `json:"content" validate:"required,max=4096"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Links []models.NotificationLink `json:"links"`
|
||||
IsForcePush bool `json:"is_force_push"`
|
||||
IsRealtime bool `json:"is_realtime"`
|
||||
UserID uint `json:"user_id" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
client, err := services.GetThirdClientWithSecret(data.ClientID, data.ClientSecret)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, err.Error())
|
||||
}
|
||||
|
||||
var user models.Account
|
||||
if user, err = services.GetAccount(data.UserID); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
notification := models.Notification{
|
||||
Type: data.Type,
|
||||
Subject: data.Subject,
|
||||
Content: data.Content,
|
||||
Links: data.Links,
|
||||
IsRealtime: data.IsRealtime,
|
||||
IsForcePush: data.IsForcePush,
|
||||
RecipientID: user.ID,
|
||||
SenderID: &client.ID,
|
||||
}
|
||||
|
||||
if data.IsRealtime {
|
||||
if err := services.PushNotification(notification); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := services.NewNotification(notification); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
70
pkg/internal/server/page_api.go
Normal file
70
pkg/internal/server/page_api.go
Normal file
@ -0,0 +1,70 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"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 {
|
||||
user := c.Locals("principal").(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 {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Content string `json:"content"`
|
||||
Links []models.AccountPageLinks `json:"links"`
|
||||
}
|
||||
|
||||
if err := utils.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)
|
||||
}
|
121
pkg/internal/server/realm_members_api.go
Normal file
121
pkg/internal/server/realm_members_api.go
Normal file
@ -0,0 +1,121 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listRealmMembers(c *fiber.Ctx) error {
|
||||
alias := c.Params("realm")
|
||||
|
||||
if realm, err := services.GetRealmWithAlias(alias); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if members, err := services.ListRealmMember(realm.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(members)
|
||||
}
|
||||
}
|
||||
|
||||
func getMyRealmMember(c *fiber.Ctx) error {
|
||||
alias := c.Params("realm")
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
if realm, err := services.GetRealmWithAlias(alias); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if member, err := services.GetRealmMember(user.ID, realm.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(member)
|
||||
}
|
||||
}
|
||||
|
||||
func addRealmMember(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("realm")
|
||||
|
||||
var data struct {
|
||||
Target string `json:"target" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
realm, err := services.GetRealmWithAlias(alias)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
Name: data.Target,
|
||||
}).First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.AddRealmMember(user, account, realm); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func removeRealmMember(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("realm")
|
||||
|
||||
var data struct {
|
||||
Target string `json:"target" validate:"required"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
realm, err := services.GetRealmWithAlias(alias)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
Name: data.Target,
|
||||
}).First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.RemoveRealmMember(user, account, realm); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func leaveRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("realm")
|
||||
|
||||
realm, err := services.GetRealmWithAlias(alias)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if user.ID == realm.AccountID {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you cannot leave your own realm")
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
BaseModel: models.BaseModel{ID: user.ID},
|
||||
}).First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.RemoveRealmMember(user, account, realm); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
135
pkg/internal/server/realms_api.go
Normal file
135
pkg/internal/server/realms_api.go
Normal file
@ -0,0 +1,135 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getRealm(c *fiber.Ctx) error {
|
||||
alias := c.Params("realm")
|
||||
if realm, err := services.GetRealmWithAlias(alias); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(realm)
|
||||
}
|
||||
}
|
||||
|
||||
func listCommunityRealm(c *fiber.Ctx) error {
|
||||
realms, err := services.ListCommunityRealm()
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(realms)
|
||||
}
|
||||
|
||||
func listOwnedRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
if realms, err := services.ListOwnedRealm(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(realms)
|
||||
}
|
||||
}
|
||||
|
||||
func listAvailableRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
if realms, err := services.ListAvailableRealm(user); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(realms)
|
||||
}
|
||||
}
|
||||
|
||||
func createRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
if err := utils.CheckPermissions(c, "CreateRealms", true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,lowercase,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsCommunity bool `json:"is_community"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
realm, err := services.NewRealm(models.Realm{
|
||||
Alias: data.Alias,
|
||||
Name: data.Name,
|
||||
Description: data.Description,
|
||||
IsPublic: data.IsPublic,
|
||||
IsCommunity: data.IsCommunity,
|
||||
AccountID: user.ID,
|
||||
}, user)
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
return c.JSON(realm)
|
||||
}
|
||||
|
||||
func editRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("realmId", 0)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,lowercase,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsCommunity bool `json:"is_community"`
|
||||
}
|
||||
|
||||
if err := utils.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var realm models.Realm
|
||||
if err := database.C.Where(&models.Realm{
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
AccountID: user.ID,
|
||||
}).First(&realm).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
realm.Alias = data.Alias
|
||||
realm.Name = data.Name
|
||||
realm.Description = data.Description
|
||||
realm.IsPublic = data.IsPublic
|
||||
realm.IsCommunity = data.IsCommunity
|
||||
|
||||
realm, err := services.EditRealm(realm)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(realm)
|
||||
}
|
||||
|
||||
func deleteRealm(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("realmId", 0)
|
||||
|
||||
var realm models.Realm
|
||||
if err := database.C.Where(&models.Realm{
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
AccountID: user.ID,
|
||||
}).First(&realm).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.DeleteRealm(realm); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
36
pkg/internal/server/security_api.go
Normal file
36
pkg/internal/server/security_api.go
Normal file
@ -0,0 +1,36 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getTickets(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
var count int64
|
||||
var tickets []models.AuthTicket
|
||||
if err := database.C.
|
||||
Where(&models.AuthTicket{AccountID: user.ID}).
|
||||
Model(&models.AuthTicket{}).
|
||||
Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if err := database.C.
|
||||
Order("created_at desc").
|
||||
Where(&models.AuthTicket{AccountID: user.ID}).
|
||||
Limit(take).
|
||||
Offset(offset).
|
||||
Find(&tickets).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": tickets,
|
||||
})
|
||||
}
|
156
pkg/internal/server/startup.go
Normal file
156
pkg/internal/server/startup.go
Normal file
@ -0,0 +1,156 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/i18n"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/server/admin"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/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
|
||||
|
||||
func NewServer() {
|
||||
templates := html.NewFileSystem(http.FS(pkg.FS), ".gohtml")
|
||||
|
||||
A = fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
EnableIPValidation: true,
|
||||
ServerHeader: "Hydrogen.Passport",
|
||||
AppName: "Hydrogen.Passport",
|
||||
ProxyHeader: fiber.HeaderXForwardedFor,
|
||||
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{
|
||||
AllowCredentials: true,
|
||||
AllowMethods: strings.Join([]string{
|
||||
fiber.MethodGet,
|
||||
fiber.MethodPost,
|
||||
fiber.MethodHead,
|
||||
fiber.MethodOptions,
|
||||
fiber.MethodPut,
|
||||
fiber.MethodDelete,
|
||||
fiber.MethodPatch,
|
||||
}, ","),
|
||||
AllowOriginsFunc: func(origin string) bool {
|
||||
return true
|
||||
},
|
||||
}))
|
||||
|
||||
A.Use(logger.New(logger.Config{
|
||||
Format: "${status} | ${latency} | ${method} ${path}\n",
|
||||
Output: log.Logger,
|
||||
}))
|
||||
|
||||
A.Use(i18n.I18nMiddleware)
|
||||
|
||||
A.Get("/.well-known", getMetadata)
|
||||
A.Get("/.well-known/openid-configuration", getOidcConfiguration)
|
||||
|
||||
api := A.Group("/api").Name("API")
|
||||
{
|
||||
notify := api.Group("/notifications").Name("Notifications API")
|
||||
{
|
||||
notify.Get("/", authMiddleware, getNotifications)
|
||||
notify.Post("/subscribe", authMiddleware, addNotifySubscriber)
|
||||
notify.Put("/batch/read", authMiddleware, markNotificationReadBatch)
|
||||
notify.Put("/:notificationId/read", authMiddleware, markNotificationRead)
|
||||
}
|
||||
|
||||
me := api.Group("/users/me").Name("Myself Operations")
|
||||
{
|
||||
|
||||
me.Put("/avatar", authMiddleware, setAvatar)
|
||||
me.Put("/banner", authMiddleware, setBanner)
|
||||
|
||||
me.Get("/", authMiddleware, getUserinfo)
|
||||
me.Get("/page", authMiddleware, getOwnPersonalPage)
|
||||
me.Put("/", authMiddleware, editUserinfo)
|
||||
me.Put("/page", authMiddleware, editPersonalPage)
|
||||
me.Get("/events", authMiddleware, getEvents)
|
||||
me.Get("/tickets", authMiddleware, getTickets)
|
||||
me.Delete("/tickets/:ticketId", authMiddleware, killSession)
|
||||
|
||||
me.Post("/confirm", doRegisterConfirm)
|
||||
|
||||
friends := me.Group("/friends").Name("Friends")
|
||||
{
|
||||
friends.Get("/", authMiddleware, listFriendship)
|
||||
friends.Get("/:relatedId", authMiddleware, getFriendship)
|
||||
friends.Post("/", authMiddleware, makeFriendship)
|
||||
friends.Post("/:relatedId", authMiddleware, makeFriendship)
|
||||
friends.Put("/:relatedId", authMiddleware, editFriendship)
|
||||
friends.Delete("/:relatedId", authMiddleware, deleteFriendship)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
realms := api.Group("/realms").Name("Realms API")
|
||||
{
|
||||
realms.Get("/", listCommunityRealm)
|
||||
realms.Get("/me", authMiddleware, listOwnedRealm)
|
||||
realms.Get("/me/available", authMiddleware, listAvailableRealm)
|
||||
realms.Get("/:realm", getRealm)
|
||||
realms.Get("/:realm/members", listRealmMembers)
|
||||
realms.Get("/:realm/members/me", authMiddleware, getMyRealmMember)
|
||||
realms.Post("/", authMiddleware, createRealm)
|
||||
realms.Put("/:realmId", authMiddleware, editRealm)
|
||||
realms.Delete("/:realmId", authMiddleware, deleteRealm)
|
||||
realms.Post("/:realm/members", authMiddleware, addRealmMember)
|
||||
realms.Delete("/:realm/members", authMiddleware, removeRealmMember)
|
||||
realms.Delete("/:realm/members/me", authMiddleware, leaveRealm)
|
||||
}
|
||||
|
||||
developers := api.Group("/dev").Name("Developers API")
|
||||
{
|
||||
developers.Post("/notify", notifyUser)
|
||||
}
|
||||
|
||||
api.Get("/ws", authMiddleware, websocket.New(listenWebsocket))
|
||||
}
|
||||
|
||||
A.Use(favicon.New(favicon.Config{
|
||||
FileSystem: http.FS(pkg.FS),
|
||||
File: "views/favicon.png",
|
||||
URL: "/favicon.png",
|
||||
}))
|
||||
|
||||
admin.MapAdminEndpoints(A, authMiddleware)
|
||||
ui.MapUserInterface(A, authFunc)
|
||||
}
|
||||
|
||||
func Listen() {
|
||||
if err := A.Listen(viper.GetString("bind")); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when starting server...")
|
||||
}
|
||||
}
|
51
pkg/internal/server/ui/accounts.go
Normal file
51
pkg/internal/server/ui/accounts.go
Normal file
@ -0,0 +1,51 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/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 {
|
||||
user := c.Locals("principal").(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")
|
||||
}
|
47
pkg/internal/server/ui/index.go
Normal file
47
pkg/internal/server/ui/index.go
Normal file
@ -0,0 +1,47 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func MapUserInterface(A *fiber.App, authFunc utils.AuthFunc) {
|
||||
authCheckWare := func(c *fiber.Ctx) error {
|
||||
var token string
|
||||
if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 {
|
||||
token = cookie
|
||||
}
|
||||
|
||||
c.Locals("token", token)
|
||||
|
||||
if err := authFunc(c); err != nil {
|
||||
uri := c.Request().URI().FullURI()
|
||||
return c.Redirect(fmt.Sprintf("/sign-in?redirect_uri=%s", string(uri)))
|
||||
} else {
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
pages := A.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", authCheckWare, authorizePage)
|
||||
|
||||
pages.Post("/sign-up", signupAction)
|
||||
pages.Post("/sign-in", signinAction)
|
||||
pages.Post("/mfa", mfaRequestAction)
|
||||
pages.Post("/mfa/apply", mfaApplyAction)
|
||||
pages.Post("/authorize", authCheckWare, authorizeAction)
|
||||
|
||||
pages.Get("/users/me", authCheckWare, selfUserinfoPage)
|
||||
}
|
194
pkg/internal/server/ui/mfa.go
Normal file
194
pkg/internal/server/ui/mfa.go
Normal file
@ -0,0 +1,194 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"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 := utils.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": utils.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 := utils.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 {
|
||||
services.SetJwtCookieSet(c, access, refresh)
|
||||
}
|
||||
|
||||
return c.Redirect(lo.FromPtr(utils.GetRedirectUri(c, "/users/me")))
|
||||
}
|
154
pkg/internal/server/ui/oauth.go
Normal file
154
pkg/internal/server/ui/oauth.go
Normal file
@ -0,0 +1,154 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/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)
|
||||
user := c.Locals("principal").(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 {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id := c.Query("client_id")
|
||||
response := c.Query("response_type")
|
||||
redirect := c.Query("redirect_uri")
|
||||
scope := c.Query("scope")
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
93
pkg/internal/server/ui/signin.go
Normal file
93
pkg/internal/server/ui/signin.go
Normal file
@ -0,0 +1,93 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"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 := utils.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": utils.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 {
|
||||
services.SetJwtCookieSet(c, access, refresh)
|
||||
}
|
||||
|
||||
return c.Redirect(lo.FromPtr(utils.GetRedirectUri(c, "/users/me")))
|
||||
}
|
87
pkg/internal/server/ui/signup.go
Normal file
87
pkg/internal/server/ui/signup.go
Normal file
@ -0,0 +1,87 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/utils"
|
||||
"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 := utils.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(utils.GetRedirectUri(c, "/sign-in")))
|
||||
}
|
||||
}
|
23
pkg/internal/server/userinfo_api.go
Normal file
23
pkg/internal/server/userinfo_api.go
Normal file
@ -0,0 +1,23 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getOtherUserinfo(c *fiber.Ctx) error {
|
||||
alias := c.Params("alias")
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.
|
||||
Where(&models.Account{Name: alias}).
|
||||
Omit("tickets", "challenges", "factors", "events", "clients", "notifications", "notify_subscribers").
|
||||
Preload("Profile").
|
||||
Preload("Badges").
|
||||
First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(account)
|
||||
}
|
34
pkg/internal/server/well_known_api.go
Normal file
34
pkg/internal/server/well_known_api.go
Normal file
@ -0,0 +1,34 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func getMetadata(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"name": viper.GetString("name"),
|
||||
"domain": viper.GetString("domain"),
|
||||
"open_registration": !viper.GetBool("use_registration_magic_token"),
|
||||
})
|
||||
}
|
||||
|
||||
func getOidcConfiguration(c *fiber.Ctx) error {
|
||||
domain := viper.GetString("domain")
|
||||
basepath := fmt.Sprintf("https://%s", domain)
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"issuer": basepath,
|
||||
"authorization_endpoint": fmt.Sprintf("%s/authorize", basepath),
|
||||
"token_endpoint": fmt.Sprintf("%s/api/auth/token", basepath),
|
||||
"userinfo_endpoint": fmt.Sprintf("%s/api/users/me", basepath),
|
||||
"response_types_supported": []string{"code", "token"},
|
||||
"grant_types_supported": []string{"authorization_code", "implicit", "refresh_token"},
|
||||
"subject_types_supported": []string{"public"},
|
||||
"token_endpoint_auth_methods_supported": []string{"client_secret_post"},
|
||||
"id_token_signing_alg_values_supported": []string{"HS512"},
|
||||
"token_endpoint_auth_signing_alg_values_supported": []string{"HS512"},
|
||||
})
|
||||
}
|
83
pkg/internal/server/ws.go
Normal file
83
pkg/internal/server/ws.go
Normal file
@ -0,0 +1,83 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/passport/pkg/services"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func listenWebsocket(c *websocket.Conn) {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
// Push connection
|
||||
services.ClientRegister(user, c)
|
||||
log.Debug().Uint("user", user.ID).Msg("New websocket connection established...")
|
||||
|
||||
// Event loop
|
||||
var task models.UnifiedCommand
|
||||
|
||||
var messageType int
|
||||
var payload []byte
|
||||
var packet []byte
|
||||
var err error
|
||||
|
||||
for {
|
||||
if messageType, packet, err = c.ReadMessage(); err != nil {
|
||||
break
|
||||
} else if err := jsoniter.Unmarshal(packet, &task); err != nil {
|
||||
_ = c.WriteMessage(messageType, models.UnifiedCommand{
|
||||
Action: "error",
|
||||
Message: "unable to unmarshal your command, requires json request",
|
||||
}.Marshal())
|
||||
continue
|
||||
} else {
|
||||
payload, _ = jsoniter.Marshal(task.Payload)
|
||||
}
|
||||
|
||||
var message *models.UnifiedCommand
|
||||
switch task.Action {
|
||||
case "kex.request":
|
||||
var req struct {
|
||||
RequestID string `json:"request_id"`
|
||||
KeypairID string `json:"keypair_id"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
OwnerID uint `json:"owner_id"`
|
||||
Deadline int64 `json:"deadline"`
|
||||
}
|
||||
_ = jsoniter.Unmarshal(payload, &req)
|
||||
if len(req.RequestID) <= 0 || len(req.KeypairID) <= 0 || req.OwnerID <= 0 {
|
||||
message = lo.ToPtr(models.UnifiedCommandFromError(fmt.Errorf("invalid request")))
|
||||
}
|
||||
services.KexRequest(c, req.RequestID, req.KeypairID, req.Algorithm, req.OwnerID, req.Deadline)
|
||||
case "kex.provide":
|
||||
var req struct {
|
||||
RequestID string `json:"request_id"`
|
||||
KeypairID string `json:"keypair_id"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
PublicKey []byte `json:"public_key"`
|
||||
}
|
||||
_ = jsoniter.Unmarshal(payload, &req)
|
||||
if len(req.RequestID) <= 0 || len(req.KeypairID) <= 0 {
|
||||
message = lo.ToPtr(models.UnifiedCommandFromError(fmt.Errorf("invalid request")))
|
||||
}
|
||||
services.KexProvide(user.ID, req.RequestID, req.KeypairID, packet)
|
||||
default:
|
||||
message = lo.ToPtr(models.UnifiedCommandFromError(fmt.Errorf("unknown action")))
|
||||
}
|
||||
|
||||
if message != nil {
|
||||
if err = c.WriteMessage(messageType, message.Marshal()); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pop connection
|
||||
services.ClientUnregister(user, c)
|
||||
log.Debug().Uint("user", user.ID).Msg("A websocket connection disconnected...")
|
||||
}
|
Reference in New Issue
Block a user