diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index d6000e7..4a0ec98 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,19 +4,15 @@
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
-
@@ -163,7 +159,6 @@
-
@@ -188,7 +183,8 @@
-
+
+
true
diff --git a/pkg/internal/server/api/bots_api.go b/pkg/internal/server/api/bots_api.go
new file mode 100644
index 0000000..fdeb860
--- /dev/null
+++ b/pkg/internal/server/api/bots_api.go
@@ -0,0 +1,90 @@
+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"
+ "github.com/samber/lo"
+ "gorm.io/datatypes"
+ "strings"
+ "time"
+)
+
+func listBots(c *fiber.Ctx) error {
+ if err := exts.EnsureAuthenticated(c); err != nil {
+ return err
+ }
+ user := c.Locals("user").(models.Account)
+
+ var bots []models.Account
+ if err := database.C.Where("automated_id = ?", user.AutomatedID).Find(&bots).Error; err != nil {
+ return fiber.NewError(fiber.StatusInternalServerError, err.Error())
+ }
+
+ return c.JSON(bots)
+}
+
+func createBot(c *fiber.Ctx) error {
+ if err := exts.EnsureAuthenticated(c); err != nil {
+ return err
+ }
+ user := c.Locals("user").(models.Account)
+
+ cnt, _ := services.GetBotCount(user)
+ if err := exts.EnsureGrantedPerm(c, "CreateBots", cnt+1); err != nil {
+ return err
+ }
+
+ var data struct {
+ Name string `json:"name" validate:"required,lowercase,alphanum,min=4,max=16"`
+ Nick string `json:"nick" validate:"required"`
+ Description string `json:"description"`
+ }
+
+ if err := exts.BindAndValidate(c, &data); err != nil {
+ return err
+ } else {
+ data.Name = strings.TrimSpace(data.Name)
+ data.Nick = strings.TrimSpace(data.Nick)
+ }
+
+ if !services.ValidateAccountName(data.Nick, 4, 24) {
+ return fiber.NewError(fiber.StatusBadRequest, "invalid bot nick, length requires 4 to 24")
+ }
+
+ bot, err := services.NewBot(user, models.Account{
+ Name: data.Name,
+ Nick: data.Nick,
+ Description: data.Description,
+ ConfirmedAt: lo.ToPtr(time.Now()),
+ PermNodes: datatypes.JSONMap{},
+ })
+
+ if err != nil {
+ return fiber.NewError(fiber.StatusBadRequest, err.Error())
+ } else {
+ return c.JSON(bot)
+ }
+}
+
+func deleteBot(c *fiber.Ctx) error {
+ if err := exts.EnsureAuthenticated(c); err != nil {
+ return err
+ }
+ user := c.Locals("user").(models.Account)
+
+ id, _ := c.ParamsInt("id", 0)
+
+ var bot models.Account
+ if err := database.C.Where("id = ? AND automated_id = ?", id, user.ID).First(&bot).Error; err != nil {
+ return fiber.NewError(fiber.StatusNotFound, err.Error())
+ }
+
+ if err := services.DeleteAccount(bot.ID); err != nil {
+ return fiber.NewError(fiber.StatusInternalServerError, err.Error())
+ }
+
+ return c.JSON(bot)
+}
diff --git a/pkg/internal/server/api/index.go b/pkg/internal/server/api/index.go
index 021f73c..a73b1de 100644
--- a/pkg/internal/server/api/index.go
+++ b/pkg/internal/server/api/index.go
@@ -101,6 +101,13 @@ func MapAPIs(app *fiber.App, baseURL string) {
{
developers.Post("/notify", notifyUser)
+ bots := developers.Group("/bots").Name("Bots")
+ {
+ bots.Get("/", listBots)
+ bots.Post("/", createBot)
+ bots.Delete("/:id", deleteBot)
+ }
+
keys := developers.Group("/keys").Name("Keys")
{
keys.Get("/", listBotKeys)
diff --git a/pkg/internal/server/api/notify_api.go b/pkg/internal/server/api/notify_api.go
index b4989c5..c6ad390 100644
--- a/pkg/internal/server/api/notify_api.go
+++ b/pkg/internal/server/api/notify_api.go
@@ -1,6 +1,7 @@
package api
import (
+ "fmt"
"git.solsynth.dev/hydrogen/passport/pkg/internal/models"
"git.solsynth.dev/hydrogen/passport/pkg/internal/server/exts"
"git.solsynth.dev/hydrogen/passport/pkg/internal/services"
@@ -8,32 +9,36 @@ import (
)
func notifyUser(c *fiber.Ctx) error {
+ if err := exts.EnsureGrantedPerm(c, "DevNotifyUser", true); err != nil {
+ return err
+ }
+ user := c.Locals("user").(models.Account)
+
var data struct {
- ClientID string `json:"client_id" validate:"required"`
- ClientSecret string `json:"client_secret" validate:"required"`
- Topic string `json:"type" validate:"required"`
- Title string `json:"subject" validate:"required,max=1024"`
- Subtitle *string `json:"subtitle" validate:"max=1024"`
- Body string `json:"content" validate:"required,max=4096"`
- Metadata map[string]any `json:"metadata"`
- Avatar *string `json:"avatar"`
- Picture *string `json:"picture"`
- IsForcePush bool `json:"is_force_push"`
- IsRealtime bool `json:"is_realtime"`
- UserID uint `json:"user_id" validate:"required"`
+ ClientID string `json:"client_id" validate:"required"`
+ Topic string `json:"type" validate:"required"`
+ Title string `json:"subject" validate:"required,max=1024"`
+ Subtitle *string `json:"subtitle" validate:"max=1024"`
+ Body string `json:"content" validate:"required,max=4096"`
+ Metadata map[string]any `json:"metadata"`
+ Avatar *string `json:"avatar"`
+ Picture *string `json:"picture"`
+ 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)
+ client, err := services.GetThirdClientWithUser(data.ClientID, user.ID)
if err != nil {
- return fiber.NewError(fiber.StatusForbidden, err.Error())
+ return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get client: %v", err))
}
- var user models.Account
- if user, err = services.GetAccount(data.UserID); err != nil {
+ var target models.Account
+ if target, err = services.GetAccount(data.UserID); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
@@ -47,7 +52,7 @@ func notifyUser(c *fiber.Ctx) error {
Picture: data.Picture,
IsRealtime: data.IsRealtime,
IsForcePush: data.IsForcePush,
- AccountID: user.ID,
+ AccountID: target.ID,
SenderID: &client.ID,
}
diff --git a/pkg/internal/services/bot_token.go b/pkg/internal/services/bot_token.go
index c07cdcc..edf5ab4 100644
--- a/pkg/internal/services/bot_token.go
+++ b/pkg/internal/services/bot_token.go
@@ -42,7 +42,7 @@ func RollApiKey(key models.ApiKey) (models.ApiKey, error) {
return key, err
}
- ticket, err := RotateTicket(ticket)
+ ticket, err := RotateTicket(ticket, true)
if err != nil {
return key, err
} else {
diff --git a/pkg/internal/services/bots.go b/pkg/internal/services/bots.go
new file mode 100644
index 0000000..695a208
--- /dev/null
+++ b/pkg/internal/services/bots.go
@@ -0,0 +1,24 @@
+package services
+
+import (
+ "git.solsynth.dev/hydrogen/passport/pkg/internal/database"
+ "git.solsynth.dev/hydrogen/passport/pkg/internal/models"
+)
+
+func GetBotCount(user models.Account) (int64, error) {
+ var count int64
+ if err := database.C.Where("automated_id = ?", user.ID).Count(&count).Error; err != nil {
+ return 0, err
+ }
+ return count, nil
+}
+
+func NewBot(user models.Account, bot models.Account) (models.Account, error) {
+ bot.AutomatedBy = &user
+ bot.AutomatedID = &user.ID
+
+ if err := database.C.Save(&bot).Error; err != nil {
+ return bot, err
+ }
+ return bot, nil
+}
diff --git a/pkg/internal/services/clients.go b/pkg/internal/services/clients.go
index 8cf38cd..a97282a 100644
--- a/pkg/internal/services/clients.go
+++ b/pkg/internal/services/clients.go
@@ -18,6 +18,18 @@ func GetThirdClient(id string) (models.ThirdClient, error) {
return client, nil
}
+func GetThirdClientWithUser(id string, userId uint) (models.ThirdClient, error) {
+ var client models.ThirdClient
+ if err := database.C.Where(&models.ThirdClient{
+ Alias: id,
+ AccountID: &userId,
+ }).First(&client).Error; err != nil {
+ return client, err
+ }
+
+ return client, nil
+}
+
func GetThirdClientWithSecret(id, secret string) (models.ThirdClient, error) {
client, err := GetThirdClient(id)
if err != nil {
diff --git a/pkg/internal/services/ticket.go b/pkg/internal/services/ticket.go
index 5ad2a32..f52bc14 100644
--- a/pkg/internal/services/ticket.go
+++ b/pkg/internal/services/ticket.go
@@ -147,10 +147,13 @@ func ActiveTicketWithMFA(ticket models.AuthTicket, factor models.AuthFactor, cod
return ticket, nil
}
-func RotateTicket(ticket models.AuthTicket) (models.AuthTicket, error) {
+func RotateTicket(ticket models.AuthTicket, fullyRestart ...bool) (models.AuthTicket, error) {
ticket.GrantToken = lo.ToPtr(uuid.NewString())
ticket.AccessToken = lo.ToPtr(uuid.NewString())
ticket.RefreshToken = lo.ToPtr(uuid.NewString())
+ if len(fullyRestart) > 0 && fullyRestart[0] {
+ ticket.LastGrantAt = nil
+ }
err := database.C.Save(&ticket).Error
return ticket, err
}