diff --git a/go.mod b/go.mod index c97a277..a119841 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22 toolchain go1.22.1 require ( - git.solsynth.dev/hydrogen/identity v0.0.0-20240406034845-44d2ec9c4ace + git.solsynth.dev/hydrogen/passport v0.0.0-20240504085931-7c418a3cd32f github.com/go-playground/validator/v10 v10.17.0 github.com/gofiber/contrib/websocket v1.3.0 github.com/gofiber/fiber/v2 v2.52.4 @@ -59,7 +59,7 @@ require ( github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect github.com/jxskiss/base62 v1.1.0 // indirect - github.com/klauspost/compress v1.17.7 // indirect + github.com/klauspost/compress v1.17.8 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lithammer/shortuuid/v4 v4.0.0 // indirect diff --git a/go.sum b/go.sum index e7b1340..6fb4d05 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.solsynth.dev/hydrogen/identity v0.0.0-20240406034845-44d2ec9c4ace h1:bXbBjM56vA3BxfyuD0IrlJabpVx5bLi4qCv3/RsPa1c= -git.solsynth.dev/hydrogen/identity v0.0.0-20240406034845-44d2ec9c4ace/go.mod h1:GxcduEpQWQ2mO37A9uRtseS680uMLi957GDywRBAJHg= +git.solsynth.dev/hydrogen/passport v0.0.0-20240504085931-7c418a3cd32f h1:sKrQrKZc5C+dwefRsnc0uAGttzpSUWXUBoFaCXLkaTo= +git.solsynth.dev/hydrogen/passport v0.0.0-20240504085931-7c418a3cd32f/go.mod h1:3JRFPtf0dXRk2UQ1yVIgIspNfytM2yLBeBePJChgLZE= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= @@ -119,8 +119,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw= github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc= -github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= -github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= +github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/pkg/database/migrator.go b/pkg/database/migrator.go index 876f5ec..535265f 100644 --- a/pkg/database/migrator.go +++ b/pkg/database/migrator.go @@ -7,6 +7,7 @@ import ( var DatabaseAutoActionRange = []any{ &models.Account{}, + &models.Realm{}, &models.Channel{}, &models.ChannelMember{}, &models.Call{}, diff --git a/pkg/grpc/client.go b/pkg/grpc/client.go index c882dc8..3a11c0f 100644 --- a/pkg/grpc/client.go +++ b/pkg/grpc/client.go @@ -1,13 +1,14 @@ package grpc import ( - idpb "git.solsynth.dev/hydrogen/identity/pkg/grpc/proto" + idpb "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" "google.golang.org/grpc/credentials/insecure" "github.com/spf13/viper" "google.golang.org/grpc" ) +var Realms idpb.RealmsClient var Friendships idpb.FriendshipsClient var Notify idpb.NotifyClient var Auth idpb.AuthClient @@ -17,6 +18,7 @@ func ConnectPassport() error { if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil { return err } else { + Realms = idpb.NewRealmsClient(conn) Friendships = idpb.NewFriendshipsClient(conn) Notify = idpb.NewNotifyClient(conn) Auth = idpb.NewAuthClient(conn) diff --git a/pkg/models/channels.go b/pkg/models/channels.go index ee6dff3..400af2d 100644 --- a/pkg/models/channels.go +++ b/pkg/models/channels.go @@ -10,7 +10,7 @@ const ( type Channel struct { BaseModel - Alias string `json:"alias" gorm:"uniqueIndex"` + Alias string `json:"alias"` Name string `json:"name"` Description string `json:"description"` Members []ChannelMember `json:"members"` @@ -19,7 +19,7 @@ type Channel struct { Type ChannelType `json:"type"` Account Account `json:"account"` AccountID uint `json:"account_id"` - RealmID uint `json:"realm_id"` + RealmID *uint `json:"realm_id"` } type NotifyLevel = int8 diff --git a/pkg/models/realms.go b/pkg/models/realms.go new file mode 100644 index 0000000..01269af --- /dev/null +++ b/pkg/models/realms.go @@ -0,0 +1,15 @@ +package models + +// Realm profiles basically fetched from Hydrogen.Passport +// But cache at here for better usage and database relations +type Realm struct { + BaseModel + + Alias string `json:"alias"` + Name string `json:"name"` + Description string `json:"description"` + Channels []Channel `json:"channels"` + IsPublic bool `json:"is_public"` + IsCommunity bool `json:"is_community"` + ExternalID uint `json:"external_id"` +} diff --git a/pkg/server/auth.go b/pkg/server/auth.go index 19d80af..b6597c0 100644 --- a/pkg/server/auth.go +++ b/pkg/server/auth.go @@ -3,14 +3,13 @@ package server import ( "strings" - "git.solsynth.dev/hydrogen/messaging/pkg/security" "git.solsynth.dev/hydrogen/messaging/pkg/services" "github.com/gofiber/fiber/v2" ) func authMiddleware(c *fiber.Ctx) error { var token string - if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 { + if cookie := c.Cookies(services.CookieAccessKey); len(cookie) > 0 { token = cookie } if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 { @@ -42,10 +41,10 @@ func authFunc(c *fiber.Ctx, overrides ...string) error { } } - rtk := c.Cookies(security.CookieRefreshKey) + rtk := c.Cookies(services.CookieRefreshKey) if user, atk, rtk, err := services.Authenticate(token, rtk); err == nil { if atk != token { - security.SetJwtCookieSet(c, atk, rtk) + services.SetJwtCookieSet(c, atk, rtk) } c.Locals("principal", user) return nil diff --git a/pkg/server/channel_members_api.go b/pkg/server/channel_members_api.go index 0efb3cf..9c17264 100644 --- a/pkg/server/channel_members_api.go +++ b/pkg/server/channel_members_api.go @@ -1,6 +1,7 @@ package server import ( + "fmt" "git.solsynth.dev/hydrogen/messaging/pkg/database" "git.solsynth.dev/hydrogen/messaging/pkg/models" "git.solsynth.dev/hydrogen/messaging/pkg/services" @@ -22,7 +23,7 @@ func listChannelMembers(c *fiber.Ctx) error { } } -func inviteChannel(c *fiber.Ctx) error { +func addChannelMember(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) alias := c.Params("channel") @@ -49,14 +50,14 @@ func inviteChannel(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusNotFound, err.Error()) } - if err := services.InviteChannelMember(account, channel); err != nil { + if err := services.AddChannelMemberWithCheck(account, channel); err != nil { return fiber.NewError(fiber.StatusBadRequest, err.Error()) } else { return c.SendStatus(fiber.StatusOK) } } -func kickChannel(c *fiber.Ctx) error { +func removeChannelMember(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) alias := c.Params("channel") @@ -124,6 +125,34 @@ func editChannelMembership(c *fiber.Ctx) error { } } +func joinChannel(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) + alias := c.Params("channel") + + var channel models.Channel + if err := database.C.Where(&models.Channel{ + Alias: alias, + }).First(&channel).Error; err != nil { + return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if _, _, err := services.GetAvailableChannel(channel.ID, user); err == nil { + return fiber.NewError(fiber.StatusBadRequest, "you already joined the channel") + } else if channel.RealmID == nil { + return fiber.NewError(fiber.StatusBadRequest, "you was impossible to join a channel without related realm") + } + + if realm, err := services.GetRealm(channel.ID); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("invalid channel, related realm was not found: %v", err)) + } else if _, err := services.GetRealmMember(realm.ID, user.ExternalID); err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you are not a part of the realm: %v", err)) + } + + if err := services.AddChannelMember(user, channel); err != nil { + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } else { + return c.SendStatus(fiber.StatusOK) + } +} + func leaveChannel(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) alias := c.Params("channel") diff --git a/pkg/server/messages_api.go b/pkg/server/messages_api.go index 71d7b8b..4a4653d 100644 --- a/pkg/server/messages_api.go +++ b/pkg/server/messages_api.go @@ -9,6 +9,7 @@ import ( ) func listMessage(c *fiber.Ctx) error { + user := c.Locals("principal").(models.Account) take := c.QueryInt("take", 0) offset := c.QueryInt("offset", 0) alias := c.Params("channel") @@ -16,6 +17,8 @@ func listMessage(c *fiber.Ctx) error { channel, err := services.GetChannelWithAlias(alias) if err != nil { return fiber.NewError(fiber.StatusNotFound, err.Error()) + } else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil { + return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err)) } count := services.CountMessage(channel) @@ -30,7 +33,7 @@ func listMessage(c *fiber.Ctx) error { }) } -func newTextMessage(c *fiber.Ctx) error { +func newMessage(c *fiber.Ctx) error { user := c.Locals("principal").(models.Account) alias := c.Params("channel") diff --git a/pkg/server/startup.go b/pkg/server/startup.go index de7f6ce..cf328cf 100644 --- a/pkg/server/startup.go +++ b/pkg/server/startup.go @@ -88,12 +88,13 @@ func NewServer() { channels.Get("/:channel/members", listChannelMembers) channels.Put("/:channel/members", authMiddleware, editChannelMembership) - channels.Post("/:channel/invite", authMiddleware, inviteChannel) - channels.Post("/:channel/kick", authMiddleware, kickChannel) - channels.Post("/:channel/leave", authMiddleware, leaveChannel) + channels.Post("/:channel/members", authMiddleware, addChannelMember) + channels.Post("/:channel/members/me", authMiddleware, joinChannel) + channels.Delete("/:channel/members", authMiddleware, removeChannelMember) + channels.Delete("/:channel/members/me", authMiddleware, leaveChannel) - channels.Get("/:channel/messages", listMessage) - channels.Post("/:channel/messages", authMiddleware, newTextMessage) + channels.Get("/:channel/messages", authMiddleware, listMessage) + channels.Post("/:channel/messages", authMiddleware, newMessage) channels.Put("/:channel/messages/:messageId", authMiddleware, editMessage) channels.Delete("/:channel/messages/:messageId", authMiddleware, deleteMessage) diff --git a/pkg/services/accounts.go b/pkg/services/accounts.go index 2db5c87..5a2e6f1 100644 --- a/pkg/services/accounts.go +++ b/pkg/services/accounts.go @@ -5,9 +5,9 @@ import ( "git.solsynth.dev/hydrogen/messaging/pkg/database" "time" - "git.solsynth.dev/hydrogen/identity/pkg/grpc/proto" "git.solsynth.dev/hydrogen/messaging/pkg/grpc" "git.solsynth.dev/hydrogen/messaging/pkg/models" + "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" "github.com/spf13/viper" ) diff --git a/pkg/services/auth.go b/pkg/services/auth.go index a79f1df..1620aba 100644 --- a/pkg/services/auth.go +++ b/pkg/services/auth.go @@ -7,10 +7,10 @@ import ( "reflect" "time" - "git.solsynth.dev/hydrogen/identity/pkg/grpc/proto" "git.solsynth.dev/hydrogen/messaging/pkg/database" "git.solsynth.dev/hydrogen/messaging/pkg/grpc" "git.solsynth.dev/hydrogen/messaging/pkg/models" + "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" "gorm.io/gorm" ) diff --git a/pkg/services/channel_members.go b/pkg/services/channel_members.go index 3617504..7165e8e 100644 --- a/pkg/services/channel_members.go +++ b/pkg/services/channel_members.go @@ -19,7 +19,7 @@ func ListChannelMember(channelId uint) ([]models.ChannelMember, error) { return members, nil } -func InviteChannelMember(user models.Account, target models.Channel) error { +func AddChannelMemberWithCheck(user models.Account, target models.Channel) error { if _, err := GetAccountFriend(user.ID, target.AccountID, 1); err != nil { return fmt.Errorf("you only can invite your friends to your channel") } diff --git a/pkg/security/encryptor.go b/pkg/services/encryptor.go similarity index 94% rename from pkg/security/encryptor.go rename to pkg/services/encryptor.go index 6cebde0..8700731 100644 --- a/pkg/security/encryptor.go +++ b/pkg/services/encryptor.go @@ -1,4 +1,4 @@ -package security +package services import "golang.org/x/crypto/bcrypt" diff --git a/pkg/security/jwt.go b/pkg/services/jwt.go similarity index 99% rename from pkg/security/jwt.go rename to pkg/services/jwt.go index 450a16e..77b5b22 100644 --- a/pkg/security/jwt.go +++ b/pkg/services/jwt.go @@ -1,4 +1,4 @@ -package security +package services import ( "fmt" diff --git a/pkg/services/realms.go b/pkg/services/realms.go new file mode 100644 index 0000000..1a39999 --- /dev/null +++ b/pkg/services/realms.go @@ -0,0 +1,82 @@ +package services + +import ( + "context" + "errors" + "fmt" + "git.solsynth.dev/hydrogen/messaging/pkg/database" + "git.solsynth.dev/hydrogen/messaging/pkg/grpc" + "git.solsynth.dev/hydrogen/messaging/pkg/models" + "git.solsynth.dev/hydrogen/passport/pkg/grpc/proto" + "github.com/samber/lo" + "gorm.io/gorm" +) + +func GetRealm(id uint) (models.Realm, error) { + var realm models.Realm + response, err := grpc.Realms.GetRealm(context.Background(), &proto.RealmLookupRequest{ + Id: lo.ToPtr(uint64(id)), + }) + if err != nil { + return realm, err + } + return LinkRealm(response) +} + +func GetRealmWithAlias(alias string) (models.Realm, error) { + var realm models.Realm + response, err := grpc.Realms.GetRealm(context.Background(), &proto.RealmLookupRequest{ + Alias: &alias, + }) + if err != nil { + return realm, err + } + return LinkRealm(response) +} + +func GetRealmMember(realmId uint, userId uint) (*proto.RealmMemberResponse, error) { + response, err := grpc.Realms.GetRealmMember(context.Background(), &proto.RealmMemberLookupRequest{ + RealmId: uint64(realmId), + UserId: lo.ToPtr(uint64(userId)), + }) + if err != nil { + return nil, err + } else { + return response, nil + } +} + +func ListRealmMember(realmId uint) ([]*proto.RealmMemberResponse, error) { + response, err := grpc.Realms.ListRealmMember(context.Background(), &proto.RealmMemberLookupRequest{ + RealmId: uint64(realmId), + }) + if err != nil { + return nil, err + } else { + return response.Data, nil + } +} + +func LinkRealm(info *proto.RealmResponse) (models.Realm, error) { + var realm models.Realm + if info == nil { + return realm, fmt.Errorf("remote realm info was not found") + } + if err := database.C.Where(&models.Realm{ + ExternalID: uint(info.Id), + }).First(&realm).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + realm = models.Realm{ + Alias: info.Alias, + Name: info.Name, + Description: info.Description, + IsPublic: info.IsPublic, + IsCommunity: info.IsCommunity, + ExternalID: uint(info.Id), + } + return realm, database.C.Save(&realm).Error + } + return realm, err + } + return realm, nil +}