diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index d299f91..26ece40 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,12 +4,14 @@
-
+
+
+
+
+
-
-
-
-
+
+
@@ -143,7 +145,8 @@
-
+
+
true
diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go
index 195d50f..75148e7 100644
--- a/pkg/database/migrator.go
+++ b/pkg/database/migrator.go
@@ -12,6 +12,8 @@ var DatabaseAutoActionRange = []any{
&models.AccountPage{},
&models.AccountContact{},
&models.AccountFriendship{},
+ &models.Realm{},
+ &models.RealmMember{},
&models.AuthTicket{},
&models.MagicToken{},
&models.ThirdClient{},
diff --git a/pkg/models/realms.go b/pkg/models/realms.go
new file mode 100644
index 0000000..bbb5b32
--- /dev/null
+++ b/pkg/models/realms.go
@@ -0,0 +1,23 @@
+package models
+
+type Realm struct {
+ BaseModel
+
+ Alias string `json:"alias" gorm:"uniqueIndex"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Members []RealmMember `json:"members"`
+ IsPublic bool `json:"is_public"`
+ IsCommunity bool `json:"is_community"`
+ AccountID uint `json:"account_id"`
+}
+
+type RealmMember struct {
+ BaseModel
+
+ RealmID uint `json:"realm_id"`
+ AccountID uint `json:"account_id"`
+ Realm Realm `json:"realm"`
+ Account Account `json:"account"`
+ PowerLevel int `json:"power_level"`
+}
diff --git a/pkg/server/realm_members_api.go b/pkg/server/realm_members_api.go
new file mode 100644
index 0000000..7eef6fc
--- /dev/null
+++ b/pkg/server/realm_members_api.go
@@ -0,0 +1,107 @@
+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 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)
+ }
+}
diff --git a/pkg/server/realms_api.go b/pkg/server/realms_api.go
new file mode 100644
index 0000000..821d4bb
--- /dev/null
+++ b/pkg/server/realms_api.go
@@ -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.ListRealmWithUser(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.ListRealmIsAvailable(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 user.PowerLevel < 10 {
+ return fiber.NewError(fiber.StatusForbidden, "require power level 10 to create realms")
+ }
+
+ 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,
+ })
+
+ 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)
+}
diff --git a/pkg/server/startup.go b/pkg/server/startup.go
index 9775b20..1b9a766 100644
--- a/pkg/server/startup.go
+++ b/pkg/server/startup.go
@@ -117,6 +117,21 @@ func NewServer() {
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.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)
diff --git a/pkg/services/realms.go b/pkg/services/realms.go
new file mode 100644
index 0000000..81874ef
--- /dev/null
+++ b/pkg/services/realms.go
@@ -0,0 +1,137 @@
+package services
+
+import (
+ "fmt"
+ "git.solsynth.dev/hydrogen/passport/pkg/database"
+ "git.solsynth.dev/hydrogen/passport/pkg/models"
+ "github.com/samber/lo"
+)
+
+func ListCommunityRealm() ([]models.Realm, error) {
+ var realms []models.Realm
+ if err := database.C.Where(&models.Realm{
+ IsCommunity: true,
+ }).Find(&realms).Error; err != nil {
+ return realms, err
+ }
+
+ return realms, nil
+}
+
+func ListRealmWithUser(user models.Account) ([]models.Realm, error) {
+ var realms []models.Realm
+ if err := database.C.Where(&models.Realm{AccountID: user.ID}).Find(&realms).Error; err != nil {
+ return realms, err
+ }
+
+ return realms, nil
+}
+
+func ListRealmIsAvailable(user models.Account) ([]models.Realm, error) {
+ var realms []models.Realm
+ var members []models.RealmMember
+ if err := database.C.Where(&models.RealmMember{
+ AccountID: user.ID,
+ }).Find(&members).Error; err != nil {
+ return realms, err
+ }
+
+ idx := lo.Map(members, func(item models.RealmMember, index int) uint {
+ return item.RealmID
+ })
+
+ if err := database.C.Where("id IN ?", idx).Find(&realms).Error; err != nil {
+ return realms, err
+ }
+
+ return realms, nil
+}
+
+func GetRealmWithAlias(alias string) (models.Realm, error) {
+ var realm models.Realm
+ if err := database.C.Where(&models.Realm{
+ Alias: alias,
+ }).First(&realm).Error; err != nil {
+ return realm, err
+ }
+ return realm, nil
+}
+
+func NewRealm(realm models.Realm) (models.Realm, error) {
+ err := database.C.Save(&realm).Error
+ return realm, err
+}
+
+func ListRealmMember(realmId uint) ([]models.RealmMember, error) {
+ var members []models.RealmMember
+
+ if err := database.C.
+ Where(&models.RealmMember{RealmID: realmId}).
+ Preload("Account").
+ Find(&members).Error; err != nil {
+ return members, err
+ }
+
+ return members, nil
+}
+
+func GetRealmMember(userId uint, realmId uint) (models.RealmMember, error) {
+ var member models.RealmMember
+ if err := database.C.Where(&models.RealmMember{
+ AccountID: userId,
+ RealmID: realmId,
+ }).Find(&member).Error; err != nil {
+ return member, err
+ }
+ return member, nil
+}
+
+func AddRealmMember(user models.Account, affected models.Account, target models.Realm) error {
+ if !target.IsPublic && !target.IsCommunity {
+ if member, err := GetRealmMember(user.ID, target.ID); err != nil {
+ return fmt.Errorf("only realm member can add people: %v", err)
+ } else if member.PowerLevel < 50 {
+ return fmt.Errorf("only realm moderator can add people")
+ }
+ friendship, err := GetFriendWithTwoSides(affected.ID, user.ID)
+ if err != nil || friendship.Status != models.FriendshipActive {
+ return fmt.Errorf("you only can add your friends to your realm")
+ }
+ }
+
+ member := models.RealmMember{
+ RealmID: target.ID,
+ AccountID: affected.ID,
+ }
+ err := database.C.Save(&member).Error
+ return err
+}
+
+func RemoveRealmMember(user models.Account, affected models.Account, target models.Realm) error {
+ if user.ID != affected.ID {
+ if member, err := GetRealmMember(user.ID, target.ID); err != nil {
+ return fmt.Errorf("only realm member can remove other member: %v", err)
+ } else if member.PowerLevel < 50 {
+ return fmt.Errorf("only realm moderator can invite people")
+ }
+ }
+
+ var member models.RealmMember
+ if err := database.C.Where(&models.RealmMember{
+ RealmID: target.ID,
+ AccountID: affected.ID,
+ }).First(&member).Error; err != nil {
+ return err
+ }
+
+ return database.C.Delete(&member).Error
+}
+
+func EditRealm(realm models.Realm) (models.Realm, error) {
+ err := database.C.Save(&realm).Error
+ return realm, err
+}
+
+func DeleteRealm(realm models.Realm) error {
+ return database.C.Delete(&realm).Error
+}