♻️ Improve code structure and much easier to read

🐛 Fix auth middleware
This commit is contained in:
2024-06-22 13:04:21 +08:00
parent c37a55b88b
commit 7007cda8f2
34 changed files with 451 additions and 337 deletions

View File

@ -0,0 +1,190 @@
package api
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"strconv"
"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"
jsoniter "github.com/json-iterator/go"
"github.com/spf13/viper"
)
func getUserinfo(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
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("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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 := exts.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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 := exts.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 := exts.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)
}

View File

@ -0,0 +1,146 @@
package api
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"time"
"github.com/gofiber/fiber/v2"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
)
func doAuthenticate(c *fiber.Ctx) error {
var data struct {
Username string `json:"username"`
Password string `json:"password" validate:"required"`
}
if err := exts.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 := exts.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 := exts.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")
}
exts.SetAuthCookies(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(),
})
}

View File

@ -0,0 +1,90 @@
package api
import (
"context"
"fmt"
pcpb "git.solsynth.dev/hydrogen/paperclip/pkg/grpc/proto"
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/gap"
"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/samber/lo"
)
func setAvatar(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
var data struct {
AttachmentID uint `json:"attachment" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
pc, err := gap.DiscoverPaperclip()
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "attachments services was not available")
}
if _, err := pcpb.NewAttachmentsClient(pc).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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
AttachmentID uint `json:"attachment" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
pc, err := gap.DiscoverPaperclip()
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "attachments services was not available")
}
if _, err := pcpb.NewAttachmentsClient(pc).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)
}

View File

@ -0,0 +1,23 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/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)
}
}

View File

@ -0,0 +1,138 @@
package api
import (
"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"
)
func listFriendship(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
relatedId, _ := c.ParamsInt("relatedId", 0)
var data struct {
Status uint8 `json:"status"`
}
if err := exts.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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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)
}
}

View File

@ -0,0 +1,90 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"github.com/gofiber/contrib/websocket"
"github.com/gofiber/fiber/v2"
)
func MapAPIs(app *fiber.App) {
app.Get("/.well-known", getMetadata)
app.Get("/.well-known/openid-configuration", getOidcConfiguration)
api := app.Group("/api").Name("API")
{
notify := api.Group("/notifications").Name("Notifications API")
{
notify.Get("/", getNotifications)
notify.Post("/subscribe", addNotifySubscriber)
notify.Put("/batch/read", markNotificationReadBatch)
notify.Put("/:notificationId/read", markNotificationRead)
}
me := api.Group("/users/me").Name("Myself Operations")
{
me.Put("/avatar", setAvatar)
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.Post("/confirm", doRegisterConfirm)
friends := me.Group("/friends").Name("Friends")
{
friends.Get("/", listFriendship)
friends.Get("/:relatedId", getFriendship)
friends.Post("/", makeFriendship)
friends.Post("/:relatedId", makeFriendship)
friends.Put("/:relatedId", editFriendship)
friends.Delete("/:relatedId", 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", listOwnedRealm)
realms.Get("/me/available", listAvailableRealm)
realms.Get("/:realm", getRealm)
realms.Get("/:realm/members", listRealmMembers)
realms.Get("/:realm/members/me", getMyRealmMember)
realms.Post("/", createRealm)
realms.Put("/:realmId", editRealm)
realms.Delete("/:realmId", deleteRealm)
realms.Post("/:realm/members", addRealmMember)
realms.Delete("/:realm/members", removeRealmMember)
realms.Delete("/:realm/members/me", leaveRealm)
}
developers := api.Group("/dev").Name("Developers API")
{
developers.Post("/notify", notifyUser)
}
api.Use(func(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
return c.Next()
}).Get("/ws", websocket.New(listenWebsocket))
}
}

View File

@ -0,0 +1,128 @@
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"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func getNotifications(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
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(&notifications).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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
id, _ := c.ParamsInt("notificationId", 0)
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
var notify models.Notification
if err := database.C.Where(&models.Notification{
BaseModel: models.BaseModel{ID: uint(id)},
RecipientID: user.ID,
}).First(&notify).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if err := database.C.Delete(&notify).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}
func markNotificationReadBatch(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
var data struct {
MessageIDs []uint `json:"messages"`
}
if err := exts.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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 := exts.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)
}

View File

@ -0,0 +1,60 @@
package api
import (
"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"
)
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 := exts.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)
}

View File

@ -0,0 +1,76 @@
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)
}

View File

@ -0,0 +1,133 @@
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"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"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")
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
alias := c.Params("realm")
var data struct {
Target string `json:"target" validate:"required"`
}
if err := exts.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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
alias := c.Params("realm")
var data struct {
Target string `json:"target" validate:"required"`
}
if err := exts.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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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)
}
}

View File

@ -0,0 +1,147 @@
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"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
"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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 {
if err := exts.EnsureGrantedPerm(c, "CreateRealms", true); err != nil {
return err
}
user := c.Locals("user").(models.Account)
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 := exts.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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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 := exts.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 {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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)
}

View File

@ -0,0 +1,40 @@
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 getTickets(c *fiber.Ctx) error {
if err := exts.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(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,
})
}

View File

@ -0,0 +1,23 @@
package api
import (
"git.solsynth.dev/hydrogen/passport/pkg/internal/database"
"git.solsynth.dev/hydrogen/passport/pkg/internal/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)
}

View File

@ -0,0 +1,34 @@
package api
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"},
})
}

View File

@ -0,0 +1,82 @@
package api
import (
"fmt"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/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("user").(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...")
}