mirror of
https://github.com/aykhans/slash-e.git
synced 2025-09-05 16:54:18 +00:00
refactor: update api version
This commit is contained in:
@@ -1 +0,0 @@
|
||||
> The v1 API has been deprecated. Please use the v2 API instead.
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
35
api/v1/acl_config.go
Normal file
35
api/v1/acl_config.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package v1
|
||||
|
||||
import "strings"
|
||||
|
||||
var allowedMethodsWhenUnauthorized = map[string]bool{
|
||||
"/slash.api.v1.WorkspaceService/GetWorkspaceProfile": true,
|
||||
"/slash.api.v1.WorkspaceService/GetWorkspaceSetting": true,
|
||||
"/slash.api.v1.AuthService/SignIn": true,
|
||||
"/slash.api.v1.AuthService/SignUp": true,
|
||||
"/slash.api.v1.AuthService/SignOut": true,
|
||||
"/memos.api.v1.AuthService/GetAuthStatus": true,
|
||||
"/slash.api.v1.ShortcutService/GetShortcutByName": true,
|
||||
"/slash.api.v1.ShortcutService/GetShortcut": true,
|
||||
"/slash.api.v1.CollectionService/GetCollectionByName": true,
|
||||
}
|
||||
|
||||
// isUnauthorizeAllowedMethod returns true if the method is allowed to be called when the user is not authorized.
|
||||
func isUnauthorizeAllowedMethod(methodName string) bool {
|
||||
if strings.HasPrefix(methodName, "/grpc.reflection") {
|
||||
return true
|
||||
}
|
||||
return allowedMethodsWhenUnauthorized[methodName]
|
||||
}
|
||||
|
||||
var allowedMethodsOnlyForAdmin = map[string]bool{
|
||||
"/slash.api.v1.UserService/CreateUser": true,
|
||||
"/slash.api.v1.UserService/DeleteUser": true,
|
||||
"/slash.api.v1.WorkspaceService/UpdateWorkspaceSetting": true,
|
||||
"/slash.api.v1.SubscriptionService/UpdateSubscription": true,
|
||||
}
|
||||
|
||||
// isOnlyForAdminAllowedMethod returns true if the method is allowed to be called only by admin.
|
||||
func isOnlyForAdminAllowedMethod(methodName string) bool {
|
||||
return allowedMethodsOnlyForAdmin[methodName]
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
package v1
|
||||
|
||||
type ActivityShorcutCreatePayload struct {
|
||||
ShortcutID int32 `json:"shortcutId"`
|
||||
}
|
||||
|
||||
type ActivityShorcutViewPayload struct {
|
||||
ShortcutID int32 `json:"shortcutId"`
|
||||
IP string `json:"ip"`
|
||||
Referer string `json:"referer"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
}
|
@@ -1,131 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/mssola/useragent"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"github.com/yourselfhosted/slash/internal/util"
|
||||
"github.com/yourselfhosted/slash/server/metric"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
type ReferenceInfo struct {
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type DeviceInfo struct {
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type BrowserInfo struct {
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type AnalysisData struct {
|
||||
ReferenceData []ReferenceInfo `json:"referenceData"`
|
||||
DeviceData []DeviceInfo `json:"deviceData"`
|
||||
BrowserData []BrowserInfo `json:"browserData"`
|
||||
}
|
||||
|
||||
func (s *APIV1Service) registerAnalyticsRoutes(g *echo.Group) {
|
||||
g.GET("/shortcut/:shortcutId/analytics", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
shortcutID, err := util.ConvertStringToInt32(c.Param("shortcutId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("shortcut id is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
|
||||
}
|
||||
activities, err := s.Store.ListActivities(ctx, &store.FindActivity{
|
||||
Type: store.ActivityShortcutView,
|
||||
PayloadShortcutID: &shortcutID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get activities, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
referenceMap := make(map[string]int)
|
||||
deviceMap := make(map[string]int)
|
||||
browserMap := make(map[string]int)
|
||||
for _, activity := range activities {
|
||||
payload := &ActivityShorcutViewPayload{}
|
||||
if err := json.Unmarshal([]byte(activity.Payload), payload); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to unmarshal payload, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
if _, ok := referenceMap[payload.Referer]; !ok {
|
||||
referenceMap[payload.Referer] = 0
|
||||
}
|
||||
referenceMap[payload.Referer]++
|
||||
|
||||
ua := useragent.New(payload.UserAgent)
|
||||
deviceName := ua.OSInfo().Name
|
||||
browserName, _ := ua.Browser()
|
||||
|
||||
if _, ok := deviceMap[deviceName]; !ok {
|
||||
deviceMap[deviceName] = 0
|
||||
}
|
||||
deviceMap[deviceName]++
|
||||
|
||||
if _, ok := browserMap[browserName]; !ok {
|
||||
browserMap[browserName] = 0
|
||||
}
|
||||
browserMap[browserName]++
|
||||
}
|
||||
|
||||
metric.Enqueue("shortcut analytics")
|
||||
return c.JSON(http.StatusOK, &AnalysisData{
|
||||
ReferenceData: mapToReferenceInfoSlice(referenceMap),
|
||||
DeviceData: mapToDeviceInfoSlice(deviceMap),
|
||||
BrowserData: mapToBrowserInfoSlice(browserMap),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func mapToReferenceInfoSlice(m map[string]int) []ReferenceInfo {
|
||||
referenceInfoSlice := make([]ReferenceInfo, 0)
|
||||
for key, value := range m {
|
||||
referenceInfoSlice = append(referenceInfoSlice, ReferenceInfo{
|
||||
Name: key,
|
||||
Count: value,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(referenceInfoSlice, func(i, j ReferenceInfo) int {
|
||||
return i.Count - j.Count
|
||||
})
|
||||
return referenceInfoSlice
|
||||
}
|
||||
|
||||
func mapToDeviceInfoSlice(m map[string]int) []DeviceInfo {
|
||||
deviceInfoSlice := make([]DeviceInfo, 0)
|
||||
for key, value := range m {
|
||||
deviceInfoSlice = append(deviceInfoSlice, DeviceInfo{
|
||||
Name: key,
|
||||
Count: value,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(deviceInfoSlice, func(i, j DeviceInfo) int {
|
||||
return i.Count - j.Count
|
||||
})
|
||||
return deviceInfoSlice
|
||||
}
|
||||
|
||||
func mapToBrowserInfoSlice(m map[string]int) []BrowserInfo {
|
||||
browserInfoSlice := make([]BrowserInfo, 0)
|
||||
for key, value := range m {
|
||||
browserInfoSlice = append(browserInfoSlice, BrowserInfo{
|
||||
Name: key,
|
||||
Count: value,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(browserInfoSlice, func(i, j BrowserInfo) int {
|
||||
return i.Count - j.Count
|
||||
})
|
||||
return browserInfoSlice
|
||||
}
|
211
api/v1/auth.go
211
api/v1/auth.go
@@ -1,211 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/yourselfhosted/slash/api/auth"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/metric"
|
||||
"github.com/yourselfhosted/slash/server/service/license"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
type SignInRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type SignUpRequest struct {
|
||||
Nickname string `json:"nickname"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func (s *APIV1Service) registerAuthRoutes(g *echo.Group, secret string) {
|
||||
g.POST("/auth/signin", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
signin := &SignInRequest{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("malformatted signin request, err: %s", err))
|
||||
}
|
||||
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Email: &signin.Email,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find user by email %s", signin.Email)).SetInternal(err)
|
||||
}
|
||||
if user == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("user not found with email %s", signin.Email))
|
||||
} else if user.RowStatus == store.Archived {
|
||||
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("user has been archived with email %s", signin.Email))
|
||||
}
|
||||
|
||||
// Compare the stored hashed password, with the hashed version of the password that was received.
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "unmatched email and password")
|
||||
}
|
||||
|
||||
accessToken, err := auth.GenerateAccessToken(user.Email, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(secret))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
cookieExp := time.Now().Add(auth.CookieExpDuration)
|
||||
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
|
||||
metric.Enqueue("user sign in")
|
||||
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
||||
})
|
||||
|
||||
g.POST("/auth/signup", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
enableSignUpSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSAPCE_SETTING_ENABLE_SIGNUP,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to get workspace setting, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if enableSignUpSetting != nil && !enableSignUpSetting.GetEnableSignup() {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "sign up has been disabled")
|
||||
}
|
||||
|
||||
if !s.LicenseService.IsFeatureEnabled(license.FeatureTypeUnlimitedAccounts) {
|
||||
userList, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list users").SetInternal(err)
|
||||
}
|
||||
if len(userList) >= 5 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Maximum number of users reached")
|
||||
}
|
||||
}
|
||||
|
||||
signup := &SignUpRequest{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(signup); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("malformatted signup request, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(signup.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err)
|
||||
}
|
||||
|
||||
create := &store.User{
|
||||
Email: signup.Email,
|
||||
Nickname: signup.Nickname,
|
||||
PasswordHash: string(passwordHash),
|
||||
}
|
||||
existingUsers, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find existing users, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
// The first user to sign up is an admin by default.
|
||||
if len(existingUsers) == 0 {
|
||||
create.Role = store.RoleAdmin
|
||||
} else {
|
||||
create.Role = store.RoleUser
|
||||
}
|
||||
|
||||
user, err := s.Store.CreateUser(ctx, create)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
accessToken, err := auth.GenerateAccessToken(user.Email, user.ID, time.Now().Add(auth.AccessTokenDuration), []byte(secret))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to generate tokens, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if err := s.UpsertAccessTokenToStore(ctx, user, accessToken); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert access token, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
cookieExp := time.Now().Add(auth.CookieExpDuration)
|
||||
setTokenCookie(c, auth.AccessTokenCookieName, accessToken, cookieExp)
|
||||
metric.Enqueue("user sign up")
|
||||
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
||||
})
|
||||
|
||||
g.POST("/auth/logout", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
RemoveTokensAndCookies(c)
|
||||
accessToken := findAccessToken(c)
|
||||
userID, _ := getUserIDFromAccessToken(accessToken, secret)
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
|
||||
// Auto remove the current access token from the user access tokens.
|
||||
if err == nil && len(userAccessTokens) != 0 {
|
||||
accessTokens := []*storepb.AccessTokensUserSetting_AccessToken{}
|
||||
for _, userAccessToken := range userAccessTokens {
|
||||
if accessToken != userAccessToken.AccessToken {
|
||||
accessTokens = append(accessTokens, userAccessToken)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
||||
UserId: userID,
|
||||
Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
|
||||
Value: &storepb.UserSetting_AccessTokens{
|
||||
AccessTokens: &storepb.AccessTokensUserSetting{
|
||||
AccessTokens: accessTokens,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
}
|
||||
c.Response().WriteHeader(http.StatusOK)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken string) error {
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get user access tokens")
|
||||
}
|
||||
userAccessToken := storepb.AccessTokensUserSetting_AccessToken{
|
||||
AccessToken: accessToken,
|
||||
Description: "Account sign in",
|
||||
}
|
||||
userAccessTokens = append(userAccessTokens, &userAccessToken)
|
||||
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
||||
UserId: user.ID,
|
||||
Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
|
||||
Value: &storepb.UserSetting_AccessTokens{
|
||||
AccessTokens: &storepb.AccessTokensUserSetting{
|
||||
AccessTokens: userAccessTokens,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to upsert user setting, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveTokensAndCookies removes the jwt token from the cookies.
|
||||
func RemoveTokensAndCookies(c echo.Context) {
|
||||
cookieExp := time.Now().Add(-1 * time.Hour)
|
||||
setTokenCookie(c, auth.AccessTokenCookieName, "", cookieExp)
|
||||
}
|
||||
|
||||
// setTokenCookie sets the token to the cookie.
|
||||
func setTokenCookie(c echo.Context, name, token string, expiration time.Time) {
|
||||
cookie := new(http.Cookie)
|
||||
cookie.Name = name
|
||||
cookie.Value = token
|
||||
cookie.Expires = expiration
|
||||
cookie.Path = "/"
|
||||
// Http-only helps mitigate the risk of client side script accessing the protected cookie.
|
||||
cookie.HttpOnly = true
|
||||
cookie.SameSite = http.SameSiteStrictMode
|
||||
c.SetCookie(cookie)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"github.com/yourselfhosted/slash/api/auth"
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/metric"
|
||||
"github.com/yourselfhosted/slash/server/service/license"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv2pb.GetAuthStatusRequest) (*apiv2pb.GetAuthStatusResponse, error) {
|
||||
func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv1pb.GetAuthStatusRequest) (*apiv1pb.GetAuthStatusResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
@@ -27,12 +27,12 @@ func (s *APIV2Service) GetAuthStatus(ctx context.Context, _ *apiv2pb.GetAuthStat
|
||||
if user == nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "user not found")
|
||||
}
|
||||
return &apiv2pb.GetAuthStatusResponse{
|
||||
return &apiv1pb.GetAuthStatusResponse{
|
||||
User: convertUserFromStore(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) SignIn(ctx context.Context, request *apiv2pb.SignInRequest) (*apiv2pb.SignInResponse, error) {
|
||||
func (s *APIV2Service) SignIn(ctx context.Context, request *apiv1pb.SignInRequest) (*apiv1pb.SignInResponse, error) {
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
Email: &request.Email,
|
||||
})
|
||||
@@ -65,12 +65,12 @@ func (s *APIV2Service) SignIn(ctx context.Context, request *apiv2pb.SignInReques
|
||||
}
|
||||
|
||||
metric.Enqueue("user sign in")
|
||||
return &apiv2pb.SignInResponse{
|
||||
return &apiv1pb.SignInResponse{
|
||||
User: convertUserFromStore(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) SignUp(ctx context.Context, request *apiv2pb.SignUpRequest) (*apiv2pb.SignUpResponse, error) {
|
||||
func (s *APIV2Service) SignUp(ctx context.Context, request *apiv1pb.SignUpRequest) (*apiv1pb.SignUpResponse, error) {
|
||||
enableSignUpSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSAPCE_SETTING_ENABLE_SIGNUP,
|
||||
})
|
||||
@@ -132,12 +132,12 @@ func (s *APIV2Service) SignUp(ctx context.Context, request *apiv2pb.SignUpReques
|
||||
}
|
||||
|
||||
metric.Enqueue("user sign up")
|
||||
return &apiv2pb.SignUpResponse{
|
||||
return &apiv1pb.SignUpResponse{
|
||||
User: convertUserFromStore(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*APIV2Service) SignOut(ctx context.Context, _ *apiv2pb.SignOutRequest) (*apiv2pb.SignOutResponse, error) {
|
||||
func (*APIV2Service) SignOut(ctx context.Context, _ *apiv1pb.SignOutRequest) (*apiv1pb.SignOutResponse, error) {
|
||||
// Set the cookie header to expire access token.
|
||||
if err := grpc.SetHeader(ctx, metadata.New(map[string]string{
|
||||
"Set-Cookie": fmt.Sprintf("%s=; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; SameSite=Strict", auth.AccessTokenCookieName),
|
||||
@@ -145,5 +145,5 @@ func (*APIV2Service) SignOut(ctx context.Context, _ *apiv2pb.SignOutRequest) (*a
|
||||
return nil, status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err)
|
||||
}
|
||||
|
||||
return &apiv2pb.SignOutResponse{}, nil
|
||||
return &apiv1pb.SignOutResponse{}, nil
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -8,17 +8,20 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/metric"
|
||||
"github.com/yourselfhosted/slash/server/service/license"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) ListCollections(ctx context.Context, _ *apiv2pb.ListCollectionsRequest) (*apiv2pb.ListCollectionsResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
func (s *APIV2Service) ListCollections(ctx context.Context, _ *apiv1pb.ListCollectionsRequest) (*apiv1pb.ListCollectionsResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
collections, err := s.Store.ListCollections(ctx, &store.FindCollection{
|
||||
CreatorID: &userID,
|
||||
CreatorID: &user.ID,
|
||||
VisibilityList: []store.Visibility{
|
||||
store.VisibilityPrivate,
|
||||
},
|
||||
@@ -38,18 +41,18 @@ func (s *APIV2Service) ListCollections(ctx context.Context, _ *apiv2pb.ListColle
|
||||
}
|
||||
collections = append(collections, sharedCollections...)
|
||||
|
||||
convertedCollections := []*apiv2pb.Collection{}
|
||||
convertedCollections := []*apiv1pb.Collection{}
|
||||
for _, collection := range collections {
|
||||
convertedCollections = append(convertedCollections, convertCollectionFromStore(collection))
|
||||
}
|
||||
|
||||
response := &apiv2pb.ListCollectionsResponse{
|
||||
response := &apiv1pb.ListCollectionsResponse{
|
||||
Collections: convertedCollections,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetCollection(ctx context.Context, request *apiv2pb.GetCollectionRequest) (*apiv2pb.GetCollectionResponse, error) {
|
||||
func (s *APIV2Service) GetCollection(ctx context.Context, request *apiv1pb.GetCollectionRequest) (*apiv1pb.GetCollectionResponse, error) {
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
ID: &request.Id,
|
||||
})
|
||||
@@ -60,17 +63,20 @@ func (s *APIV2Service) GetCollection(ctx context.Context, request *apiv2pb.GetCo
|
||||
return nil, status.Errorf(codes.NotFound, "collection not found")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if collection.Visibility == storepb.Visibility_PRIVATE && collection.CreatorId != userID {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
if collection.Visibility == storepb.Visibility_PRIVATE && collection.CreatorId != user.ID {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
response := &apiv2pb.GetCollectionResponse{
|
||||
response := &apiv1pb.GetCollectionResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetCollectionByName(ctx context.Context, request *apiv2pb.GetCollectionByNameRequest) (*apiv2pb.GetCollectionByNameResponse, error) {
|
||||
func (s *APIV2Service) GetCollectionByName(ctx context.Context, request *apiv1pb.GetCollectionByNameRequest) (*apiv1pb.GetCollectionByNameResponse, error) {
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
Name: &request.Name,
|
||||
})
|
||||
@@ -91,14 +97,14 @@ func (s *APIV2Service) GetCollectionByName(ctx context.Context, request *apiv2pb
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
}
|
||||
response := &apiv2pb.GetCollectionByNameResponse{
|
||||
response := &apiv1pb.GetCollectionByNameResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
metric.Enqueue("collection view")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) CreateCollection(ctx context.Context, request *apiv2pb.CreateCollectionRequest) (*apiv2pb.CreateCollectionResponse, error) {
|
||||
func (s *APIV2Service) CreateCollection(ctx context.Context, request *apiv1pb.CreateCollectionRequest) (*apiv1pb.CreateCollectionResponse, error) {
|
||||
if request.Collection.Name == "" || request.Collection.Title == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "name and title are required")
|
||||
}
|
||||
@@ -115,38 +121,38 @@ func (s *APIV2Service) CreateCollection(ctx context.Context, request *apiv2pb.Cr
|
||||
}
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
collection := &storepb.Collection{
|
||||
CreatorId: userID,
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
collectionCreate := &storepb.Collection{
|
||||
CreatorId: user.ID,
|
||||
Name: request.Collection.Name,
|
||||
Title: request.Collection.Title,
|
||||
Description: request.Collection.Description,
|
||||
ShortcutIds: request.Collection.ShortcutIds,
|
||||
Visibility: storepb.Visibility(request.Collection.Visibility),
|
||||
}
|
||||
collection, err := s.Store.CreateCollection(ctx, collection)
|
||||
collection, err := s.Store.CreateCollection(ctx, collectionCreate)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create collection, err: %v", err)
|
||||
}
|
||||
|
||||
response := &apiv2pb.CreateCollectionResponse{
|
||||
response := &apiv1pb.CreateCollectionResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
metric.Enqueue("collection create")
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateCollection(ctx context.Context, request *apiv2pb.UpdateCollectionRequest) (*apiv2pb.UpdateCollectionResponse, error) {
|
||||
func (s *APIV2Service) UpdateCollection(ctx context.Context, request *apiv1pb.UpdateCollectionRequest) (*apiv1pb.UpdateCollectionResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "updateMask is required")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
ID: &request.Collection.Id,
|
||||
@@ -157,7 +163,7 @@ func (s *APIV2Service) UpdateCollection(ctx context.Context, request *apiv2pb.Up
|
||||
if collection == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "collection not found")
|
||||
}
|
||||
if collection.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
if collection.CreatorId != user.ID && user.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
@@ -184,19 +190,16 @@ func (s *APIV2Service) UpdateCollection(ctx context.Context, request *apiv2pb.Up
|
||||
return nil, status.Errorf(codes.Internal, "failed to update collection, err: %v", err)
|
||||
}
|
||||
|
||||
response := &apiv2pb.UpdateCollectionResponse{
|
||||
response := &apiv1pb.UpdateCollectionResponse{
|
||||
Collection: convertCollectionFromStore(collection),
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteCollection(ctx context.Context, request *apiv2pb.DeleteCollectionRequest) (*apiv2pb.DeleteCollectionResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
func (s *APIV2Service) DeleteCollection(ctx context.Context, request *apiv1pb.DeleteCollectionRequest) (*apiv1pb.DeleteCollectionResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
|
||||
ID: &request.Id,
|
||||
@@ -207,7 +210,7 @@ func (s *APIV2Service) DeleteCollection(ctx context.Context, request *apiv2pb.De
|
||||
if collection == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "collection not found")
|
||||
}
|
||||
if collection.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
if collection.CreatorId != user.ID && user.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
@@ -217,12 +220,12 @@ func (s *APIV2Service) DeleteCollection(ctx context.Context, request *apiv2pb.De
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to delete collection, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.DeleteCollectionResponse{}
|
||||
response := &apiv1pb.DeleteCollectionResponse{}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func convertCollectionFromStore(collection *storepb.Collection) *apiv2pb.Collection {
|
||||
return &apiv2pb.Collection{
|
||||
func convertCollectionFromStore(collection *storepb.Collection) *apiv1pb.Collection {
|
||||
return &apiv1pb.Collection{
|
||||
Id: collection.Id,
|
||||
CreatorId: collection.CreatorId,
|
||||
CreatedTime: timestamppb.New(time.Unix(collection.CreatedTs, 0)),
|
||||
@@ -231,6 +234,6 @@ func convertCollectionFromStore(collection *storepb.Collection) *apiv2pb.Collect
|
||||
Title: collection.Title,
|
||||
Description: collection.Description,
|
||||
ShortcutIds: collection.ShortcutIds,
|
||||
Visibility: apiv2pb.Visibility(collection.Visibility),
|
||||
Visibility: apiv1pb.Visibility(collection.Visibility),
|
||||
}
|
||||
}
|
@@ -1,15 +1,33 @@
|
||||
package v1
|
||||
|
||||
// RowStatus is the status for a row.
|
||||
type RowStatus string
|
||||
import (
|
||||
"context"
|
||||
|
||||
const (
|
||||
// Normal is the status for a normal row.
|
||||
Normal RowStatus = "NORMAL"
|
||||
// Archived is the status for an archived row.
|
||||
Archived RowStatus = "ARCHIVED"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func (s RowStatus) String() string {
|
||||
return string(s)
|
||||
func convertRowStatusFromStore(rowStatus store.RowStatus) apiv1pb.RowStatus {
|
||||
switch rowStatus {
|
||||
case store.Normal:
|
||||
return apiv1pb.RowStatus_NORMAL
|
||||
case store.Archived:
|
||||
return apiv1pb.RowStatus_ARCHIVED
|
||||
default:
|
||||
return apiv1pb.RowStatus_ROW_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func getCurrentUser(ctx context.Context, s *store.Store) (*store.User, error) {
|
||||
userID, ok := ctx.Value(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
user, err := s.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
133
api/v1/jwt.go
133
api/v1/jwt.go
@@ -1,133 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/yourselfhosted/slash/api/auth"
|
||||
"github.com/yourselfhosted/slash/internal/util"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
const (
|
||||
// The key name used to store user id in the context
|
||||
// user id is extracted from the jwt token subject field.
|
||||
userIDContextKey = "user-id"
|
||||
)
|
||||
|
||||
func extractTokenFromHeader(c echo.Context) (string, error) {
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
authHeaderParts := strings.Fields(authHeader)
|
||||
if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
|
||||
return "", errors.New("Authorization header format must be Bearer {token}")
|
||||
}
|
||||
|
||||
return authHeaderParts[1], nil
|
||||
}
|
||||
|
||||
func findAccessToken(c echo.Context) string {
|
||||
// Check the HTTP request header first.
|
||||
accessToken, _ := extractTokenFromHeader(c)
|
||||
if accessToken == "" {
|
||||
// Check the cookie.
|
||||
cookie, _ := c.Cookie(auth.AccessTokenCookieName)
|
||||
if cookie != nil {
|
||||
accessToken = cookie.Value
|
||||
}
|
||||
}
|
||||
return accessToken
|
||||
}
|
||||
|
||||
// JWTMiddleware validates the access token.
|
||||
func JWTMiddleware(s *APIV1Service, next echo.HandlerFunc, secret string) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
path := c.Request().URL.Path
|
||||
method := c.Request().Method
|
||||
|
||||
// Pass auth and profile endpoints.
|
||||
if util.HasPrefixes(path, "/api/v1/auth", "/api/v1/workspace/profile") {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
accessToken := findAccessToken(c)
|
||||
if accessToken == "" {
|
||||
// When the request is not authenticated, we allow the user to access the shortcut endpoints for those public shortcuts.
|
||||
if util.HasPrefixes(path, "/s/", "/api/v1/user/") && method == http.MethodGet {
|
||||
return next(c)
|
||||
}
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing access token")
|
||||
}
|
||||
|
||||
userID, err := getUserIDFromAccessToken(accessToken, secret)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid or expired access token")
|
||||
}
|
||||
|
||||
accessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user access tokens.").WithInternal(err)
|
||||
}
|
||||
if !validateAccessToken(accessToken, accessTokens) {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid access token.")
|
||||
}
|
||||
|
||||
// Even if there is no error, we still need to make sure the user still exists.
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Server error to find user ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
if user == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Failed to find user ID: %d", userID))
|
||||
}
|
||||
|
||||
// Stores userID into context.
|
||||
c.Set(userIDContextKey, userID)
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
|
||||
func getUserIDFromAccessToken(accessToken, secret string) (int32, error) {
|
||||
claims := &auth.ClaimsMessage{}
|
||||
_, err := jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) {
|
||||
if t.Method.Alg() != jwt.SigningMethodHS256.Name {
|
||||
return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256)
|
||||
}
|
||||
if kid, ok := t.Header["kid"].(string); ok {
|
||||
if kid == "v1" {
|
||||
return []byte(secret), nil
|
||||
}
|
||||
}
|
||||
return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"])
|
||||
})
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Invalid or expired access token")
|
||||
}
|
||||
// We either have a valid access token or we will attempt to generate new access token.
|
||||
userID, err := util.ConvertStringToInt32(claims.Subject)
|
||||
if err != nil {
|
||||
return 0, errors.Wrap(err, "Malformed ID in the token")
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
func validateAccessToken(accessTokenString string, userAccessTokens []*storepb.AccessTokensUserSetting_AccessToken) bool {
|
||||
for _, userAccessToken := range userAccessTokens {
|
||||
if accessTokenString == userAccessToken.AccessToken {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -9,19 +9,19 @@ import (
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) ListMemos(ctx context.Context, _ *apiv2pb.ListMemosRequest) (*apiv2pb.ListMemosResponse, error) {
|
||||
func (s *APIV2Service) ListMemos(ctx context.Context, _ *apiv1pb.ListMemosRequest) (*apiv1pb.ListMemosResponse, error) {
|
||||
find := &store.FindMemo{}
|
||||
memos, err := s.Store.ListMemos(ctx, find)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to fetch memo list, err: %v", err)
|
||||
}
|
||||
|
||||
composedMemos := []*apiv2pb.Memo{}
|
||||
composedMemos := []*apiv1pb.Memo{}
|
||||
for _, memo := range memos {
|
||||
composedMemo, err := s.convertMemoFromStorepb(ctx, memo)
|
||||
if err != nil {
|
||||
@@ -30,13 +30,13 @@ func (s *APIV2Service) ListMemos(ctx context.Context, _ *apiv2pb.ListMemosReques
|
||||
composedMemos = append(composedMemos, composedMemo)
|
||||
}
|
||||
|
||||
response := &apiv2pb.ListMemosResponse{
|
||||
response := &apiv1pb.ListMemosResponse{
|
||||
Memos: composedMemos,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequest) (*apiv2pb.GetMemoResponse, error) {
|
||||
func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv1pb.GetMemoRequest) (*apiv1pb.GetMemoResponse, error) {
|
||||
memo, err := s.Store.GetMemo(ctx, &store.FindMemo{
|
||||
ID: &request.Id,
|
||||
})
|
||||
@@ -51,23 +51,26 @@ func (s *APIV2Service) GetMemo(ctx context.Context, request *apiv2pb.GetMemoRequ
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert memo, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.GetMemoResponse{
|
||||
response := &apiv1pb.GetMemoResponse{
|
||||
Memo: composedMemo,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMemoRequest) (*apiv2pb.CreateMemoResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
memo := &storepb.Memo{
|
||||
CreatorId: userID,
|
||||
func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv1pb.CreateMemoRequest) (*apiv1pb.CreateMemoResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
memoCreate := &storepb.Memo{
|
||||
CreatorId: user.ID,
|
||||
Name: request.Memo.Name,
|
||||
Title: request.Memo.Title,
|
||||
Content: request.Memo.Content,
|
||||
Tags: request.Memo.Tags,
|
||||
Visibility: storepb.Visibility(request.Memo.Visibility),
|
||||
}
|
||||
memo, err := s.Store.CreateMemo(ctx, memo)
|
||||
memo, err := s.Store.CreateMemo(ctx, memoCreate)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create memo, err: %v", err)
|
||||
}
|
||||
@@ -76,20 +79,23 @@ func (s *APIV2Service) CreateMemo(ctx context.Context, request *apiv2pb.CreateMe
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert memo, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.CreateMemoResponse{
|
||||
response := &apiv1pb.CreateMemoResponse{
|
||||
Memo: composedMemo,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMemoRequest) (*apiv2pb.UpdateMemoResponse, error) {
|
||||
func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv1pb.UpdateMemoRequest) (*apiv1pb.UpdateMemoResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "updateMask is required")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
ID: &user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
@@ -103,7 +109,7 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
|
||||
if memo == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "memo not found")
|
||||
}
|
||||
if memo.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
if memo.CreatorId != user.ID && currentUser.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
@@ -135,16 +141,19 @@ func (s *APIV2Service) UpdateMemo(ctx context.Context, request *apiv2pb.UpdateMe
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert memo, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.UpdateMemoResponse{
|
||||
response := &apiv1pb.UpdateMemoResponse{
|
||||
Memo: composedMemo,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteMemo(ctx context.Context, request *apiv2pb.DeleteMemoRequest) (*apiv2pb.DeleteMemoResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
func (s *APIV2Service) DeleteMemo(ctx context.Context, request *apiv1pb.DeleteMemoRequest) (*apiv1pb.DeleteMemoResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
ID: &user.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
@@ -158,7 +167,7 @@ func (s *APIV2Service) DeleteMemo(ctx context.Context, request *apiv2pb.DeleteMe
|
||||
if memo == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "memo not found")
|
||||
}
|
||||
if memo.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
if memo.CreatorId != user.ID && currentUser.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
@@ -168,12 +177,12 @@ func (s *APIV2Service) DeleteMemo(ctx context.Context, request *apiv2pb.DeleteMe
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to delete memo, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.DeleteMemoResponse{}
|
||||
response := &apiv1pb.DeleteMemoResponse{}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (*APIV2Service) convertMemoFromStorepb(_ context.Context, memo *storepb.Memo) (*apiv2pb.Memo, error) {
|
||||
return &apiv2pb.Memo{
|
||||
func (*APIV2Service) convertMemoFromStorepb(_ context.Context, memo *storepb.Memo) (*apiv1pb.Memo, error) {
|
||||
return &apiv1pb.Memo{
|
||||
Id: memo.Id,
|
||||
CreatedTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)),
|
||||
UpdatedTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)),
|
||||
@@ -182,6 +191,6 @@ func (*APIV2Service) convertMemoFromStorepb(_ context.Context, memo *storepb.Mem
|
||||
Title: memo.Title,
|
||||
Content: memo.Content,
|
||||
Tags: memo.Tags,
|
||||
Visibility: apiv2pb.Visibility(memo.Visibility),
|
||||
Visibility: apiv1pb.Visibility(memo.Visibility),
|
||||
}, nil
|
||||
}
|
@@ -1,386 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/yourselfhosted/slash/internal/util"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/metric"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
// Visibility is the type of a shortcut visibility.
|
||||
type Visibility string
|
||||
|
||||
const (
|
||||
// VisibilityPublic is the PUBLIC visibility.
|
||||
VisibilityPublic Visibility = "PUBLIC"
|
||||
// VisibilityWorkspace is the WORKSPACE visibility.
|
||||
VisibilityWorkspace Visibility = "WORKSPACE"
|
||||
// VisibilityPrivate is the PRIVATE visibility.
|
||||
VisibilityPrivate Visibility = "PRIVATE"
|
||||
)
|
||||
|
||||
func (v Visibility) String() string {
|
||||
return string(v)
|
||||
}
|
||||
|
||||
type OpenGraphMetadata struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
|
||||
type Shortcut struct {
|
||||
ID int32 `json:"id"`
|
||||
|
||||
// Standard fields
|
||||
CreatorID int32 `json:"creatorId"`
|
||||
Creator *User `json:"creator"`
|
||||
CreatedTs int64 `json:"createdTs"`
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
RowStatus RowStatus `json:"rowStatus"`
|
||||
|
||||
// Domain specific fields
|
||||
Name string `json:"name"`
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Tags []string `json:"tags"`
|
||||
View int `json:"view"`
|
||||
OpenGraphMetadata *OpenGraphMetadata `json:"openGraphMetadata"`
|
||||
}
|
||||
|
||||
type CreateShortcutRequest struct {
|
||||
Name string `json:"name"`
|
||||
Link string `json:"link"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Visibility Visibility `json:"visibility"`
|
||||
Tags []string `json:"tags"`
|
||||
OpenGraphMetadata *OpenGraphMetadata `json:"openGraphMetadata"`
|
||||
}
|
||||
|
||||
type PatchShortcutRequest struct {
|
||||
RowStatus *RowStatus `json:"rowStatus"`
|
||||
Name *string `json:"name"`
|
||||
Link *string `json:"link"`
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Visibility *Visibility `json:"visibility"`
|
||||
Tags []string `json:"tags"`
|
||||
OpenGraphMetadata *OpenGraphMetadata `json:"openGraphMetadata"`
|
||||
}
|
||||
|
||||
func (s *APIV1Service) registerShortcutRoutes(g *echo.Group) {
|
||||
g.POST("/shortcut", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||
}
|
||||
create := &CreateShortcutRequest{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(create); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("malformatted post shortcut request, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
shortcut := &storepb.Shortcut{
|
||||
CreatorId: userID,
|
||||
Name: create.Name,
|
||||
Link: create.Link,
|
||||
Title: create.Title,
|
||||
Description: create.Description,
|
||||
Visibility: convertVisibilityToStorepb(create.Visibility),
|
||||
Tags: create.Tags,
|
||||
OgMetadata: &storepb.OpenGraphMetadata{},
|
||||
}
|
||||
if create.Name == "" {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "name is required")
|
||||
}
|
||||
if create.OpenGraphMetadata != nil {
|
||||
shortcut.OgMetadata = &storepb.OpenGraphMetadata{
|
||||
Title: create.OpenGraphMetadata.Title,
|
||||
Description: create.OpenGraphMetadata.Description,
|
||||
Image: create.OpenGraphMetadata.Image,
|
||||
}
|
||||
}
|
||||
shortcut, err := s.Store.CreateShortcut(ctx, shortcut)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
if err := s.createShortcutCreateActivity(ctx, shortcut); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to create shortcut activity, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
shortcutMessage, err := s.composeShortcut(ctx, convertShortcutFromStorepb(shortcut))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to compose shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
metric.Enqueue("shortcut create")
|
||||
return c.JSON(http.StatusOK, shortcutMessage)
|
||||
})
|
||||
|
||||
g.PATCH("/shortcut/:shortcutId", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
shortcutID, err := util.ConvertStringToInt32(c.Param("shortcutId"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("shortcut ID is not a number: %s", c.Param("shortcutId"))).SetInternal(err)
|
||||
}
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||
}
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &shortcutID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if shortcut == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found shortcut with id: %d", shortcutID))
|
||||
}
|
||||
if shortcut.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update shortcut")
|
||||
}
|
||||
|
||||
patch := &PatchShortcutRequest{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(patch); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to decode patch shortcut request, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
shortcutUpdate := &store.UpdateShortcut{
|
||||
ID: shortcutID,
|
||||
Name: patch.Name,
|
||||
Link: patch.Link,
|
||||
Title: patch.Title,
|
||||
Description: patch.Description,
|
||||
}
|
||||
if patch.RowStatus != nil {
|
||||
shortcutUpdate.RowStatus = (*store.RowStatus)(patch.RowStatus)
|
||||
}
|
||||
if patch.Visibility != nil {
|
||||
shortcutUpdate.Visibility = (*store.Visibility)(patch.Visibility)
|
||||
}
|
||||
if patch.Tags != nil {
|
||||
tag := strings.Join(patch.Tags, " ")
|
||||
shortcutUpdate.Tag = &tag
|
||||
}
|
||||
if patch.OpenGraphMetadata != nil {
|
||||
shortcutUpdate.OpenGraphMetadata = &storepb.OpenGraphMetadata{
|
||||
Title: patch.OpenGraphMetadata.Title,
|
||||
Description: patch.OpenGraphMetadata.Description,
|
||||
Image: patch.OpenGraphMetadata.Image,
|
||||
}
|
||||
}
|
||||
shortcut, err = s.Store.UpdateShortcut(ctx, shortcutUpdate)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to patch shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
shortcutMessage, err := s.composeShortcut(ctx, convertShortcutFromStorepb(shortcut))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to compose shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, shortcutMessage)
|
||||
})
|
||||
|
||||
g.GET("/shortcut", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||
}
|
||||
|
||||
find := &store.FindShortcut{}
|
||||
if tag := c.QueryParam("tag"); tag != "" {
|
||||
find.Tag = &tag
|
||||
}
|
||||
|
||||
list := []*storepb.Shortcut{}
|
||||
find.VisibilityList = []store.Visibility{store.VisibilityWorkspace, store.VisibilityPublic}
|
||||
visibleShortcutList, err := s.Store.ListShortcuts(ctx, find)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to fetch shortcut list, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
list = append(list, visibleShortcutList...)
|
||||
|
||||
find.VisibilityList = []store.Visibility{store.VisibilityPrivate}
|
||||
find.CreatorID = &userID
|
||||
privateShortcutList, err := s.Store.ListShortcuts(ctx, find)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to fetch private shortcut list, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
list = append(list, privateShortcutList...)
|
||||
|
||||
shortcutMessageList := []*Shortcut{}
|
||||
for _, shortcut := range list {
|
||||
shortcutMessage, err := s.composeShortcut(ctx, convertShortcutFromStorepb(shortcut))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to compose shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
shortcutMessageList = append(shortcutMessageList, shortcutMessage)
|
||||
}
|
||||
return c.JSON(http.StatusOK, shortcutMessageList)
|
||||
})
|
||||
|
||||
g.GET("/shortcut/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
shortcutID, err := util.ConvertStringToInt32(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("shortcut id is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &shortcutID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to fetch shortcut by id, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if shortcut == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found shortcut with id: %d", shortcutID))
|
||||
}
|
||||
|
||||
shortcutMessage, err := s.composeShortcut(ctx, convertShortcutFromStorepb(shortcut))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to compose shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, shortcutMessage)
|
||||
})
|
||||
|
||||
g.DELETE("/shortcut/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
shortcutID, err := util.ConvertStringToInt32(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("shortcut id is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||
}
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &shortcutID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to fetch shortcut by id, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if shortcut == nil {
|
||||
return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("not found shortcut with id: %d", shortcutID))
|
||||
}
|
||||
if shortcut.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "Unauthorized to delete shortcut")
|
||||
}
|
||||
|
||||
err = s.Store.DeleteShortcut(ctx, &store.DeleteShortcut{ID: shortcutID})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to delete shortcut, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *APIV1Service) composeShortcut(ctx context.Context, shortcut *Shortcut) (*Shortcut, error) {
|
||||
if shortcut == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &shortcut.CreatorID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to get creator")
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New("Creator not found")
|
||||
}
|
||||
shortcut.Creator = convertUserFromStore(user)
|
||||
|
||||
activityList, err := s.Store.ListActivities(ctx, &store.FindActivity{
|
||||
Type: store.ActivityShortcutView,
|
||||
Level: store.ActivityInfo,
|
||||
PayloadShortcutID: &shortcut.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to list activities")
|
||||
}
|
||||
shortcut.View = len(activityList)
|
||||
|
||||
return shortcut, nil
|
||||
}
|
||||
|
||||
func convertShortcutFromStorepb(shortcut *storepb.Shortcut) *Shortcut {
|
||||
return &Shortcut{
|
||||
ID: shortcut.Id,
|
||||
CreatedTs: shortcut.CreatedTs,
|
||||
UpdatedTs: shortcut.UpdatedTs,
|
||||
CreatorID: shortcut.CreatorId,
|
||||
RowStatus: RowStatus(shortcut.RowStatus.String()),
|
||||
Name: shortcut.Name,
|
||||
Link: shortcut.Link,
|
||||
Title: shortcut.Title,
|
||||
Description: shortcut.Description,
|
||||
Visibility: Visibility(shortcut.Visibility.String()),
|
||||
Tags: shortcut.Tags,
|
||||
OpenGraphMetadata: &OpenGraphMetadata{
|
||||
Title: shortcut.OgMetadata.Title,
|
||||
Description: shortcut.OgMetadata.Description,
|
||||
Image: shortcut.OgMetadata.Image,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func convertVisibilityToStorepb(visibility Visibility) storepb.Visibility {
|
||||
switch visibility {
|
||||
case VisibilityPublic:
|
||||
return storepb.Visibility_PUBLIC
|
||||
case VisibilityWorkspace:
|
||||
return storepb.Visibility_WORKSPACE
|
||||
case VisibilityPrivate:
|
||||
return storepb.Visibility_PRIVATE
|
||||
default:
|
||||
return storepb.Visibility_PUBLIC
|
||||
}
|
||||
}
|
||||
|
||||
func (s *APIV1Service) createShortcutCreateActivity(ctx context.Context, shortcut *storepb.Shortcut) error {
|
||||
payload := &ActivityShorcutCreatePayload{
|
||||
ShortcutID: shortcut.Id,
|
||||
}
|
||||
payloadStr, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to marshal activity payload")
|
||||
}
|
||||
activity := &store.Activity{
|
||||
CreatorID: shortcut.CreatorId,
|
||||
Type: store.ActivityShortcutCreate,
|
||||
Level: store.ActivityInfo,
|
||||
Payload: string(payloadStr),
|
||||
}
|
||||
_, err = s.Store.CreateActivity(ctx, activity)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to create activity")
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -16,14 +16,17 @@ import (
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/metric"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) ListShortcuts(ctx context.Context, _ *apiv2pb.ListShortcutsRequest) (*apiv2pb.ListShortcutsResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
func (s *APIV2Service) ListShortcuts(ctx context.Context, _ *apiv1pb.ListShortcutsRequest) (*apiv1pb.ListShortcutsResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
find := &store.FindShortcut{}
|
||||
find.VisibilityList = []store.Visibility{store.VisibilityWorkspace, store.VisibilityPublic}
|
||||
visibleShortcutList, err := s.Store.ListShortcuts(ctx, find)
|
||||
@@ -32,14 +35,14 @@ func (s *APIV2Service) ListShortcuts(ctx context.Context, _ *apiv2pb.ListShortcu
|
||||
}
|
||||
|
||||
find.VisibilityList = []store.Visibility{store.VisibilityPrivate}
|
||||
find.CreatorID = &userID
|
||||
find.CreatorID = &user.ID
|
||||
shortcutList, err := s.Store.ListShortcuts(ctx, find)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to fetch private shortcut list, err: %v", err)
|
||||
}
|
||||
|
||||
shortcutList = append(shortcutList, visibleShortcutList...)
|
||||
shortcuts := []*apiv2pb.Shortcut{}
|
||||
shortcuts := []*apiv1pb.Shortcut{}
|
||||
for _, shortcut := range shortcutList {
|
||||
composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut)
|
||||
if err != nil {
|
||||
@@ -48,13 +51,13 @@ func (s *APIV2Service) ListShortcuts(ctx context.Context, _ *apiv2pb.ListShortcu
|
||||
shortcuts = append(shortcuts, composedShortcut)
|
||||
}
|
||||
|
||||
response := &apiv2pb.ListShortcutsResponse{
|
||||
response := &apiv1pb.ListShortcutsResponse{
|
||||
Shortcuts: shortcuts,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetShortcut(ctx context.Context, request *apiv2pb.GetShortcutRequest) (*apiv2pb.GetShortcutResponse, error) {
|
||||
func (s *APIV2Service) GetShortcut(ctx context.Context, request *apiv1pb.GetShortcutRequest) (*apiv1pb.GetShortcutResponse, error) {
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &request.Id,
|
||||
})
|
||||
@@ -80,13 +83,13 @@ func (s *APIV2Service) GetShortcut(ctx context.Context, request *apiv2pb.GetShor
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.GetShortcutResponse{
|
||||
response := &apiv1pb.GetShortcutResponse{
|
||||
Shortcut: composedShortcut,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetShortcutByName(ctx context.Context, request *apiv2pb.GetShortcutByNameRequest) (*apiv2pb.GetShortcutByNameResponse, error) {
|
||||
func (s *APIV2Service) GetShortcutByName(ctx context.Context, request *apiv1pb.GetShortcutByNameRequest) (*apiv1pb.GetShortcutByNameResponse, error) {
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
Name: &request.Name,
|
||||
})
|
||||
@@ -117,20 +120,23 @@ func (s *APIV2Service) GetShortcutByName(ctx context.Context, request *apiv2pb.G
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.GetShortcutByNameResponse{
|
||||
response := &apiv1pb.GetShortcutByNameResponse{
|
||||
Shortcut: composedShortcut,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) CreateShortcut(ctx context.Context, request *apiv2pb.CreateShortcutRequest) (*apiv2pb.CreateShortcutResponse, error) {
|
||||
func (s *APIV2Service) CreateShortcut(ctx context.Context, request *apiv1pb.CreateShortcutRequest) (*apiv1pb.CreateShortcutResponse, error) {
|
||||
if request.Shortcut.Name == "" || request.Shortcut.Link == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "name and link are required")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
shortcut := &storepb.Shortcut{
|
||||
CreatorId: userID,
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
shortcutCreate := &storepb.Shortcut{
|
||||
CreatorId: user.ID,
|
||||
Name: request.Shortcut.Name,
|
||||
Link: request.Shortcut.Link,
|
||||
Title: request.Shortcut.Title,
|
||||
@@ -140,13 +146,13 @@ func (s *APIV2Service) CreateShortcut(ctx context.Context, request *apiv2pb.Crea
|
||||
OgMetadata: &storepb.OpenGraphMetadata{},
|
||||
}
|
||||
if request.Shortcut.OgMetadata != nil {
|
||||
shortcut.OgMetadata = &storepb.OpenGraphMetadata{
|
||||
shortcutCreate.OgMetadata = &storepb.OpenGraphMetadata{
|
||||
Title: request.Shortcut.OgMetadata.Title,
|
||||
Description: request.Shortcut.OgMetadata.Description,
|
||||
Image: request.Shortcut.OgMetadata.Image,
|
||||
}
|
||||
}
|
||||
shortcut, err := s.Store.CreateShortcut(ctx, shortcut)
|
||||
shortcut, err := s.Store.CreateShortcut(ctx, shortcutCreate)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create shortcut, err: %v", err)
|
||||
}
|
||||
@@ -158,23 +164,20 @@ func (s *APIV2Service) CreateShortcut(ctx context.Context, request *apiv2pb.Crea
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.CreateShortcutResponse{
|
||||
response := &apiv1pb.CreateShortcutResponse{
|
||||
Shortcut: composedShortcut,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateShortcut(ctx context.Context, request *apiv2pb.UpdateShortcutRequest) (*apiv2pb.UpdateShortcutResponse, error) {
|
||||
func (s *APIV2Service) UpdateShortcut(ctx context.Context, request *apiv1pb.UpdateShortcutRequest) (*apiv1pb.UpdateShortcutResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "updateMask is required")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &request.Shortcut.Id,
|
||||
@@ -185,7 +188,7 @@ func (s *APIV2Service) UpdateShortcut(ctx context.Context, request *apiv2pb.Upda
|
||||
if shortcut == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "shortcut not found")
|
||||
}
|
||||
if shortcut.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
if shortcut.CreatorId != user.ID && user.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
@@ -227,19 +230,16 @@ func (s *APIV2Service) UpdateShortcut(ctx context.Context, request *apiv2pb.Upda
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.UpdateShortcutResponse{
|
||||
response := &apiv1pb.UpdateShortcutResponse{
|
||||
Shortcut: composedShortcut,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteShortcut(ctx context.Context, request *apiv2pb.DeleteShortcutRequest) (*apiv2pb.DeleteShortcutResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
func (s *APIV2Service) DeleteShortcut(ctx context.Context, request *apiv1pb.DeleteShortcutRequest) (*apiv1pb.DeleteShortcutResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get current user, err: %v", err)
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &request.Id,
|
||||
@@ -250,7 +250,7 @@ func (s *APIV2Service) DeleteShortcut(ctx context.Context, request *apiv2pb.Dele
|
||||
if shortcut == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "shortcut not found")
|
||||
}
|
||||
if shortcut.CreatorId != userID && currentUser.Role != store.RoleAdmin {
|
||||
if shortcut.CreatorId != user.ID && user.Role != store.RoleAdmin {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
@@ -260,11 +260,11 @@ func (s *APIV2Service) DeleteShortcut(ctx context.Context, request *apiv2pb.Dele
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to delete shortcut, err: %v", err)
|
||||
}
|
||||
response := &apiv2pb.DeleteShortcutResponse{}
|
||||
response := &apiv1pb.DeleteShortcutResponse{}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetShortcutAnalytics(ctx context.Context, request *apiv2pb.GetShortcutAnalyticsRequest) (*apiv2pb.GetShortcutAnalyticsResponse, error) {
|
||||
func (s *APIV2Service) GetShortcutAnalytics(ctx context.Context, request *apiv1pb.GetShortcutAnalyticsRequest) (*apiv1pb.GetShortcutAnalyticsResponse, error) {
|
||||
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
|
||||
ID: &request.Id,
|
||||
})
|
||||
@@ -313,7 +313,7 @@ func (s *APIV2Service) GetShortcutAnalytics(ctx context.Context, request *apiv2p
|
||||
}
|
||||
|
||||
metric.Enqueue("shortcut analytics")
|
||||
response := &apiv2pb.GetShortcutAnalyticsResponse{
|
||||
response := &apiv1pb.GetShortcutAnalyticsResponse{
|
||||
References: mapToAnalyticsSlice(referenceMap),
|
||||
Devices: mapToAnalyticsSlice(deviceMap),
|
||||
Browsers: mapToAnalyticsSlice(browserMap),
|
||||
@@ -321,15 +321,15 @@ func (s *APIV2Service) GetShortcutAnalytics(ctx context.Context, request *apiv2p
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func mapToAnalyticsSlice(m map[string]int32) []*apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem {
|
||||
analyticsSlice := make([]*apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem, 0)
|
||||
func mapToAnalyticsSlice(m map[string]int32) []*apiv1pb.GetShortcutAnalyticsResponse_AnalyticsItem {
|
||||
analyticsSlice := make([]*apiv1pb.GetShortcutAnalyticsResponse_AnalyticsItem, 0)
|
||||
for key, value := range m {
|
||||
analyticsSlice = append(analyticsSlice, &apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem{
|
||||
analyticsSlice = append(analyticsSlice, &apiv1pb.GetShortcutAnalyticsResponse_AnalyticsItem{
|
||||
Name: key,
|
||||
Count: value,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(analyticsSlice, func(i, j *apiv2pb.GetShortcutAnalyticsResponse_AnalyticsItem) int {
|
||||
slices.SortFunc(analyticsSlice, func(i, j *apiv1pb.GetShortcutAnalyticsResponse_AnalyticsItem) int {
|
||||
return int(i.Count - j.Count)
|
||||
})
|
||||
return analyticsSlice
|
||||
@@ -385,20 +385,20 @@ func (s *APIV2Service) createShortcutCreateActivity(ctx context.Context, shortcu
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) convertShortcutFromStorepb(ctx context.Context, shortcut *storepb.Shortcut) (*apiv2pb.Shortcut, error) {
|
||||
composedShortcut := &apiv2pb.Shortcut{
|
||||
func (s *APIV2Service) convertShortcutFromStorepb(ctx context.Context, shortcut *storepb.Shortcut) (*apiv1pb.Shortcut, error) {
|
||||
composedShortcut := &apiv1pb.Shortcut{
|
||||
Id: shortcut.Id,
|
||||
CreatorId: shortcut.CreatorId,
|
||||
CreatedTime: timestamppb.New(time.Unix(shortcut.CreatedTs, 0)),
|
||||
UpdatedTime: timestamppb.New(time.Unix(shortcut.UpdatedTs, 0)),
|
||||
RowStatus: apiv2pb.RowStatus(shortcut.RowStatus),
|
||||
RowStatus: apiv1pb.RowStatus(shortcut.RowStatus),
|
||||
Name: shortcut.Name,
|
||||
Link: shortcut.Link,
|
||||
Title: shortcut.Title,
|
||||
Tags: shortcut.Tags,
|
||||
Description: shortcut.Description,
|
||||
Visibility: apiv2pb.Visibility(shortcut.Visibility),
|
||||
OgMetadata: &apiv2pb.OpenGraphMetadata{
|
||||
Visibility: apiv1pb.Visibility(shortcut.Visibility),
|
||||
OgMetadata: &apiv1pb.OpenGraphMetadata{
|
||||
Title: shortcut.OgMetadata.Title,
|
||||
Description: shortcut.OgMetadata.Description,
|
||||
Image: shortcut.OgMetadata.Image,
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,25 +6,25 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) GetSubscription(ctx context.Context, _ *apiv2pb.GetSubscriptionRequest) (*apiv2pb.GetSubscriptionResponse, error) {
|
||||
func (s *APIV2Service) GetSubscription(ctx context.Context, _ *apiv1pb.GetSubscriptionRequest) (*apiv1pb.GetSubscriptionResponse, error) {
|
||||
subscription, err := s.LicenseService.LoadSubscription(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to load subscription: %v", err)
|
||||
}
|
||||
return &apiv2pb.GetSubscriptionResponse{
|
||||
return &apiv1pb.GetSubscriptionResponse{
|
||||
Subscription: subscription,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateSubscription(ctx context.Context, request *apiv2pb.UpdateSubscriptionRequest) (*apiv2pb.UpdateSubscriptionResponse, error) {
|
||||
func (s *APIV2Service) UpdateSubscription(ctx context.Context, request *apiv1pb.UpdateSubscriptionRequest) (*apiv1pb.UpdateSubscriptionResponse, error) {
|
||||
subscription, err := s.LicenseService.UpdateSubscription(ctx, request.LicenseKey)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to load subscription: %v", err)
|
||||
}
|
||||
return &apiv2pb.UpdateSubscriptionResponse{
|
||||
return &apiv1pb.UpdateSubscriptionResponse{
|
||||
Subscription: subscription,
|
||||
}, nil
|
||||
}
|
340
api/v1/user.go
340
api/v1/user.go
@@ -1,340 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/yourselfhosted/slash/internal/util"
|
||||
"github.com/yourselfhosted/slash/server/metric"
|
||||
"github.com/yourselfhosted/slash/server/service/license"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
const (
|
||||
// BotID is the id of bot.
|
||||
BotID = 0
|
||||
)
|
||||
|
||||
// Role is the type of a role.
|
||||
type Role string
|
||||
|
||||
const (
|
||||
// RoleAdmin is the ADMIN role.
|
||||
RoleAdmin Role = "ADMIN"
|
||||
// RoleUser is the USER role.
|
||||
RoleUser Role = "USER"
|
||||
)
|
||||
|
||||
func (r Role) String() string {
|
||||
switch r {
|
||||
case RoleAdmin:
|
||||
return "ADMIN"
|
||||
case RoleUser:
|
||||
return "USER"
|
||||
}
|
||||
return "USER"
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int32 `json:"id"`
|
||||
|
||||
// Standard fields
|
||||
CreatedTs int64 `json:"createdTs"`
|
||||
UpdatedTs int64 `json:"updatedTs"`
|
||||
RowStatus RowStatus `json:"rowStatus"`
|
||||
|
||||
// Domain specific fields
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
Password string `json:"password"`
|
||||
Role Role `json:"role"`
|
||||
}
|
||||
|
||||
func (create CreateUserRequest) Validate() error {
|
||||
if create.Email != "" && !validateEmail(create.Email) {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
if create.Nickname != "" && len(create.Nickname) < 3 {
|
||||
return errors.New("nickname is too short, minimum length is 3")
|
||||
}
|
||||
if len(create.Password) < 3 {
|
||||
return errors.New("password is too short, minimum length is 3")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type PatchUserRequest struct {
|
||||
RowStatus *RowStatus `json:"rowStatus"`
|
||||
Email *string `json:"email"`
|
||||
Nickname *string `json:"nickname"`
|
||||
Password *string `json:"password"`
|
||||
Role *Role `json:"role"`
|
||||
}
|
||||
|
||||
func (s *APIV1Service) registerUserRoutes(g *echo.Group) {
|
||||
g.POST("/user", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user by id").SetInternal(err)
|
||||
}
|
||||
if currentUser == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Missing auth session")
|
||||
}
|
||||
if currentUser.Role != store.RoleAdmin {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized to create user")
|
||||
}
|
||||
|
||||
if !s.LicenseService.IsFeatureEnabled(license.FeatureTypeUnlimitedAccounts) {
|
||||
userList, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to list users").SetInternal(err)
|
||||
}
|
||||
if len(userList) >= 5 {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Maximum number of users reached")
|
||||
}
|
||||
}
|
||||
|
||||
userCreate := &CreateUserRequest{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userCreate); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted post user request").SetInternal(err)
|
||||
}
|
||||
if err := userCreate.Validate(); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user create format").SetInternal(err)
|
||||
}
|
||||
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(userCreate.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate password hash").SetInternal(err)
|
||||
}
|
||||
|
||||
user, err := s.Store.CreateUser(ctx, &store.User{
|
||||
Role: store.Role(userCreate.Role),
|
||||
Email: userCreate.Email,
|
||||
Nickname: userCreate.Nickname,
|
||||
PasswordHash: string(passwordHash),
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create user").SetInternal(err)
|
||||
}
|
||||
|
||||
userMessage := convertUserFromStore(user)
|
||||
metric.Enqueue("user create")
|
||||
return c.JSON(http.StatusOK, userMessage)
|
||||
})
|
||||
|
||||
g.GET("/user", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
list, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to list users, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
userList := []*User{}
|
||||
for _, user := range list {
|
||||
userList = append(userList, convertUserFromStore(user))
|
||||
}
|
||||
return c.JSON(http.StatusOK, userList)
|
||||
})
|
||||
|
||||
// GET /api/user/me is used to check if the user is logged in.
|
||||
g.GET("/user/me", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing auth session")
|
||||
}
|
||||
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
||||
})
|
||||
|
||||
g.GET("/user/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, err := util.ConvertStringToInt32(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("user id is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
userMessage := convertUserFromStore(user)
|
||||
userID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
userMessage.Email = ""
|
||||
}
|
||||
return c.JSON(http.StatusOK, userMessage)
|
||||
})
|
||||
|
||||
g.PATCH("/user/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
userID, err := util.ConvertStringToInt32(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("user id is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
currentUserID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||
}
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: ¤tUserID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "failed to find current user").SetInternal(err)
|
||||
}
|
||||
if currentUser == nil {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||
}
|
||||
if currentUser.ID != userID && currentUser.Role != store.RoleAdmin {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "access forbidden for current session user").SetInternal(err)
|
||||
}
|
||||
|
||||
userPatch := &PatchUserRequest{}
|
||||
if err := json.NewDecoder(c.Request().Body).Decode(userPatch); err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("failed to decode request body, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
updateUser := &store.UpdateUser{
|
||||
ID: userID,
|
||||
}
|
||||
if userPatch.Email != nil {
|
||||
if !validateEmail(*userPatch.Email) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("invalid email format: %s", *userPatch.Email))
|
||||
}
|
||||
updateUser.Email = userPatch.Email
|
||||
}
|
||||
if userPatch.Nickname != nil {
|
||||
updateUser.Nickname = userPatch.Nickname
|
||||
}
|
||||
if userPatch.Password != nil && *userPatch.Password != "" {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(*userPatch.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to hash password, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
passwordHashStr := string(passwordHash)
|
||||
updateUser.PasswordHash = &passwordHashStr
|
||||
}
|
||||
if userPatch.RowStatus != nil {
|
||||
rowStatus := store.RowStatus(*userPatch.RowStatus)
|
||||
updateUser.RowStatus = &rowStatus
|
||||
}
|
||||
if userPatch.Role != nil {
|
||||
adminRole := store.RoleAdmin
|
||||
adminUsers, err := s.Store.ListUsers(ctx, &store.FindUser{
|
||||
Role: &adminRole,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to list admin users, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if len(adminUsers) == 1 && adminUsers[0].ID == userID && *userPatch.Role != RoleAdmin {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "cannot remove admin role from the last admin user")
|
||||
}
|
||||
role := store.Role(*userPatch.Role)
|
||||
updateUser.Role = &role
|
||||
}
|
||||
|
||||
user, err := s.Store.UpdateUser(ctx, updateUser)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to update user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, convertUserFromStore(user))
|
||||
})
|
||||
|
||||
g.DELETE("/user/:id", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
currentUserID, ok := c.Get(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "missing user in session")
|
||||
}
|
||||
currentUser, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: ¤tUserID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find current session user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if currentUser == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("current session user not found with ID: %d", currentUserID)).SetInternal(err)
|
||||
}
|
||||
if currentUser.Role != store.RoleAdmin {
|
||||
return echo.NewHTTPError(http.StatusForbidden, "access forbidden for current session user").SetInternal(err)
|
||||
}
|
||||
|
||||
userID, err := util.ConvertStringToInt32(c.Param("id"))
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("user id is not a number: %s", c.Param("id"))).SetInternal(err)
|
||||
}
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if user == nil {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("user not found with ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
if user.Role == store.RoleAdmin {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("cannot delete admin user with ID: %d", userID)).SetInternal(err)
|
||||
}
|
||||
|
||||
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{
|
||||
ID: userID,
|
||||
}); err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to delete user, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, true)
|
||||
})
|
||||
}
|
||||
|
||||
// validateEmail validates the email.
|
||||
func validateEmail(email string) bool {
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// convertUserFromStore converts a store user to a user.
|
||||
func convertUserFromStore(user *store.User) *User {
|
||||
return &User{
|
||||
ID: user.ID,
|
||||
CreatedTs: user.CreatedTs,
|
||||
UpdatedTs: user.UpdatedTs,
|
||||
RowStatus: RowStatus(user.RowStatus),
|
||||
Email: user.Email,
|
||||
Nickname: user.Nickname,
|
||||
Role: Role(user.Role),
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
|
||||
"github.com/yourselfhosted/slash/api/auth"
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/service/license"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
@@ -24,23 +24,23 @@ const (
|
||||
BotID = 0
|
||||
)
|
||||
|
||||
func (s *APIV2Service) ListUsers(ctx context.Context, _ *apiv2pb.ListUsersRequest) (*apiv2pb.ListUsersResponse, error) {
|
||||
func (s *APIV2Service) ListUsers(ctx context.Context, _ *apiv1pb.ListUsersRequest) (*apiv1pb.ListUsersResponse, error) {
|
||||
users, err := s.Store.ListUsers(ctx, &store.FindUser{})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list users: %v", err)
|
||||
}
|
||||
|
||||
userMessages := []*apiv2pb.User{}
|
||||
userMessages := []*apiv1pb.User{}
|
||||
for _, user := range users {
|
||||
userMessages = append(userMessages, convertUserFromStore(user))
|
||||
}
|
||||
response := &apiv2pb.ListUsersResponse{
|
||||
response := &apiv1pb.ListUsersResponse{
|
||||
Users: userMessages,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetUser(ctx context.Context, request *apiv2pb.GetUserRequest) (*apiv2pb.GetUserResponse, error) {
|
||||
func (s *APIV2Service) GetUser(ctx context.Context, request *apiv1pb.GetUserRequest) (*apiv1pb.GetUserResponse, error) {
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &request.Id,
|
||||
})
|
||||
@@ -52,13 +52,13 @@ func (s *APIV2Service) GetUser(ctx context.Context, request *apiv2pb.GetUserRequ
|
||||
}
|
||||
|
||||
userMessage := convertUserFromStore(user)
|
||||
response := &apiv2pb.GetUserResponse{
|
||||
response := &apiv1pb.GetUserResponse{
|
||||
User: userMessage,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUserRequest) (*apiv2pb.CreateUserResponse, error) {
|
||||
func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv1pb.CreateUserRequest) (*apiv1pb.CreateUserResponse, error) {
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to hash password: %v", err)
|
||||
@@ -83,15 +83,18 @@ func (s *APIV2Service) CreateUser(ctx context.Context, request *apiv2pb.CreateUs
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create user: %v", err)
|
||||
}
|
||||
response := &apiv2pb.CreateUserResponse{
|
||||
response := &apiv1pb.CreateUserResponse{
|
||||
User: convertUserFromStore(user),
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUserRequest) (*apiv2pb.UpdateUserResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.User.Id {
|
||||
func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv1pb.UpdateUserRequest) (*apiv1pb.UpdateUserResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
if user.ID != request.User.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
@@ -108,43 +111,46 @@ func (s *APIV2Service) UpdateUser(ctx context.Context, request *apiv2pb.UpdateUs
|
||||
userUpdate.Nickname = &request.User.Nickname
|
||||
}
|
||||
}
|
||||
user, err := s.Store.UpdateUser(ctx, userUpdate)
|
||||
user, err = s.Store.UpdateUser(ctx, userUpdate)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to update user: %v", err)
|
||||
}
|
||||
return &apiv2pb.UpdateUserResponse{
|
||||
return &apiv1pb.UpdateUserResponse{
|
||||
User: convertUserFromStore(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteUser(ctx context.Context, request *apiv2pb.DeleteUserRequest) (*apiv2pb.DeleteUserResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID == request.Id {
|
||||
func (s *APIV2Service) DeleteUser(ctx context.Context, request *apiv1pb.DeleteUserRequest) (*apiv1pb.DeleteUserResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
if user.ID == request.Id {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "cannot delete yourself")
|
||||
}
|
||||
|
||||
err := s.Store.DeleteUser(ctx, &store.DeleteUser{
|
||||
ID: request.Id,
|
||||
})
|
||||
if err != nil {
|
||||
if err := s.Store.DeleteUser(ctx, &store.DeleteUser{ID: request.Id}); err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err)
|
||||
}
|
||||
response := &apiv2pb.DeleteUserResponse{}
|
||||
response := &apiv1pb.DeleteUserResponse{}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2pb.ListUserAccessTokensRequest) (*apiv2pb.ListUserAccessTokensResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.Id {
|
||||
func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv1pb.ListUserAccessTokensRequest) (*apiv1pb.ListUserAccessTokensResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
if user.ID != request.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err)
|
||||
}
|
||||
|
||||
accessTokens := []*apiv2pb.UserAccessToken{}
|
||||
accessTokens := []*apiv1pb.UserAccessToken{}
|
||||
for _, userAccessToken := range userAccessTokens {
|
||||
claims := &auth.ClaimsMessage{}
|
||||
_, err := jwt.ParseWithClaims(userAccessToken.AccessToken, claims, func(t *jwt.Token) (any, error) {
|
||||
@@ -163,7 +169,7 @@ func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2p
|
||||
continue
|
||||
}
|
||||
|
||||
userAccessToken := &apiv2pb.UserAccessToken{
|
||||
userAccessToken := &apiv1pb.UserAccessToken{
|
||||
AccessToken: userAccessToken.AccessToken,
|
||||
Description: userAccessToken.Description,
|
||||
IssuedAt: timestamppb.New(claims.IssuedAt.Time),
|
||||
@@ -175,29 +181,22 @@ func (s *APIV2Service) ListUserAccessTokens(ctx context.Context, request *apiv2p
|
||||
}
|
||||
|
||||
// Sort by issued time in descending order.
|
||||
slices.SortFunc(accessTokens, func(i, j *apiv2pb.UserAccessToken) int {
|
||||
slices.SortFunc(accessTokens, func(i, j *apiv1pb.UserAccessToken) int {
|
||||
return int(i.IssuedAt.Seconds - j.IssuedAt.Seconds)
|
||||
})
|
||||
response := &apiv2pb.ListUserAccessTokensResponse{
|
||||
response := &apiv1pb.ListUserAccessTokensResponse{
|
||||
AccessTokens: accessTokens,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) CreateUserAccessToken(ctx context.Context, request *apiv2pb.CreateUserAccessTokenRequest) (*apiv2pb.CreateUserAccessTokenResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
user, err := s.Store.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
func (s *APIV2Service) CreateUserAccessToken(ctx context.Context, request *apiv1pb.CreateUserAccessTokenRequest) (*apiv1pb.CreateUserAccessTokenResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
if user == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "user not found")
|
||||
if user.ID != request.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
}
|
||||
|
||||
expiresAt := time.Time{}
|
||||
@@ -230,7 +229,7 @@ func (s *APIV2Service) CreateUserAccessToken(ctx context.Context, request *apiv2
|
||||
return nil, status.Errorf(codes.Internal, "failed to upsert access token to store: %v", err)
|
||||
}
|
||||
|
||||
userAccessToken := &apiv2pb.UserAccessToken{
|
||||
userAccessToken := &apiv1pb.UserAccessToken{
|
||||
AccessToken: accessToken,
|
||||
Description: request.Description,
|
||||
IssuedAt: timestamppb.New(claims.IssuedAt.Time),
|
||||
@@ -238,19 +237,18 @@ func (s *APIV2Service) CreateUserAccessToken(ctx context.Context, request *apiv2
|
||||
if claims.ExpiresAt != nil {
|
||||
userAccessToken.ExpiresAt = timestamppb.New(claims.ExpiresAt.Time)
|
||||
}
|
||||
response := &apiv2pb.CreateUserAccessTokenResponse{
|
||||
response := &apiv1pb.CreateUserAccessTokenResponse{
|
||||
AccessToken: userAccessToken,
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) DeleteUserAccessToken(ctx context.Context, request *apiv2pb.DeleteUserAccessTokenRequest) (*apiv2pb.DeleteUserAccessTokenResponse, error) {
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
if userID != request.Id {
|
||||
return nil, status.Errorf(codes.PermissionDenied, "Permission denied")
|
||||
func (s *APIV2Service) DeleteUserAccessToken(ctx context.Context, request *apiv1pb.DeleteUserAccessTokenRequest) (*apiv1pb.DeleteUserAccessTokenResponse, error) {
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID)
|
||||
userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err)
|
||||
}
|
||||
@@ -262,7 +260,7 @@ func (s *APIV2Service) DeleteUserAccessToken(ctx context.Context, request *apiv2
|
||||
updatedUserAccessTokens = append(updatedUserAccessTokens, userAccessToken)
|
||||
}
|
||||
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
||||
UserId: userID,
|
||||
UserId: user.ID,
|
||||
Key: storepb.UserSettingKey_USER_SETTING_ACCESS_TOKENS,
|
||||
Value: &storepb.UserSetting_AccessTokens{
|
||||
AccessTokens: &storepb.AccessTokensUserSetting{
|
||||
@@ -273,7 +271,7 @@ func (s *APIV2Service) DeleteUserAccessToken(ctx context.Context, request *apiv2
|
||||
return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err)
|
||||
}
|
||||
|
||||
return &apiv2pb.DeleteUserAccessTokenResponse{}, nil
|
||||
return &apiv1pb.DeleteUserAccessTokenResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken, description string) error {
|
||||
@@ -300,8 +298,8 @@ func (s *APIV2Service) UpsertAccessTokenToStore(ctx context.Context, user *store
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertUserFromStore(user *store.User) *apiv2pb.User {
|
||||
return &apiv2pb.User{
|
||||
func convertUserFromStore(user *store.User) *apiv1pb.User {
|
||||
return &apiv1pb.User{
|
||||
Id: int32(user.ID),
|
||||
RowStatus: convertRowStatusFromStore(user.RowStatus),
|
||||
CreatedTime: timestamppb.New(time.Unix(user.CreatedTs, 0)),
|
||||
@@ -312,13 +310,13 @@ func convertUserFromStore(user *store.User) *apiv2pb.User {
|
||||
}
|
||||
}
|
||||
|
||||
func convertUserRoleFromStore(role store.Role) apiv2pb.Role {
|
||||
func convertUserRoleFromStore(role store.Role) apiv1pb.Role {
|
||||
switch role {
|
||||
case store.RoleAdmin:
|
||||
return apiv2pb.Role_ADMIN
|
||||
return apiv1pb.Role_ADMIN
|
||||
case store.RoleUser:
|
||||
return apiv2pb.Role_USER
|
||||
return apiv1pb.Role_USER
|
||||
default:
|
||||
return apiv2pb.Role_ROLE_UNSPECIFIED
|
||||
return apiv1pb.Role_ROLE_UNSPECIFIED
|
||||
}
|
||||
}
|
@@ -1,67 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type UserSettingKey string
|
||||
|
||||
const (
|
||||
// UserSettingLocaleKey is the key type for user locale.
|
||||
UserSettingLocaleKey UserSettingKey = "locale"
|
||||
)
|
||||
|
||||
// String returns the string format of UserSettingKey type.
|
||||
func (k UserSettingKey) String() string {
|
||||
return string(k)
|
||||
}
|
||||
|
||||
var (
|
||||
UserSettingLocaleValue = []string{"en", "zh"}
|
||||
)
|
||||
|
||||
type UserSetting struct {
|
||||
UserID int
|
||||
Key UserSettingKey `json:"key"`
|
||||
// Value is a JSON string with basic value.
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type UserSettingUpsert struct {
|
||||
UserID int
|
||||
Key UserSettingKey `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
func (upsert UserSettingUpsert) Validate() error {
|
||||
if upsert.Key == UserSettingLocaleKey {
|
||||
localeValue := "en"
|
||||
err := json.Unmarshal([]byte(upsert.Value), &localeValue)
|
||||
if err != nil {
|
||||
return errors.New("failed to unmarshal user setting locale value")
|
||||
}
|
||||
|
||||
invalid := true
|
||||
for _, value := range UserSettingLocaleValue {
|
||||
if localeValue == value {
|
||||
invalid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if invalid {
|
||||
return errors.New("invalid user setting locale value")
|
||||
}
|
||||
} else {
|
||||
return errors.New("invalid user setting key")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type UserSettingFind struct {
|
||||
UserID int
|
||||
|
||||
Key *UserSettingKey `json:"key"`
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -7,31 +7,34 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) GetUserSetting(ctx context.Context, request *apiv2pb.GetUserSettingRequest) (*apiv2pb.GetUserSettingResponse, error) {
|
||||
func (s *APIV2Service) GetUserSetting(ctx context.Context, request *apiv1pb.GetUserSettingRequest) (*apiv1pb.GetUserSettingResponse, error) {
|
||||
userSetting, err := getUserSetting(ctx, s.Store, request.Id)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err)
|
||||
}
|
||||
return &apiv2pb.GetUserSettingResponse{
|
||||
return &apiv1pb.GetUserSettingResponse{
|
||||
UserSetting: userSetting,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.UpdateUserSettingRequest) (*apiv2pb.UpdateUserSettingResponse, error) {
|
||||
func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv1pb.UpdateUserSettingRequest) (*apiv1pb.UpdateUserSettingResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
|
||||
}
|
||||
|
||||
userID := ctx.Value(userIDContextKey).(int32)
|
||||
user, err := getCurrentUser(ctx, s.Store)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err)
|
||||
}
|
||||
for _, path := range request.UpdateMask.Paths {
|
||||
if path == "locale" {
|
||||
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
||||
UserId: userID,
|
||||
UserId: user.ID,
|
||||
Key: storepb.UserSettingKey_USER_SETTING_LOCALE,
|
||||
Value: &storepb.UserSetting_Locale{
|
||||
Locale: convertUserSettingLocaleToStore(request.UserSetting.Locale),
|
||||
@@ -41,7 +44,7 @@ func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.U
|
||||
}
|
||||
} else if path == "color_theme" {
|
||||
if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{
|
||||
UserId: userID,
|
||||
UserId: user.ID,
|
||||
Key: storepb.UserSettingKey_USER_SETTING_COLOR_THEME,
|
||||
Value: &storepb.UserSetting_ColorTheme{
|
||||
ColorTheme: convertUserSettingColorThemeToStore(request.UserSetting.ColorTheme),
|
||||
@@ -58,12 +61,12 @@ func (s *APIV2Service) UpdateUserSetting(ctx context.Context, request *apiv2pb.U
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get user setting: %v", err)
|
||||
}
|
||||
return &apiv2pb.UpdateUserSettingResponse{
|
||||
return &apiv1pb.UpdateUserSettingResponse{
|
||||
UserSetting: userSetting,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getUserSetting(ctx context.Context, s *store.Store, userID int32) (*apiv2pb.UserSetting, error) {
|
||||
func getUserSetting(ctx context.Context, s *store.Store, userID int32) (*apiv1pb.UserSetting, error) {
|
||||
userSettings, err := s.ListUserSettings(ctx, &store.FindUserSetting{
|
||||
UserID: &userID,
|
||||
})
|
||||
@@ -71,10 +74,10 @@ func getUserSetting(ctx context.Context, s *store.Store, userID int32) (*apiv2pb
|
||||
return nil, errors.Wrap(err, "failed to find user setting")
|
||||
}
|
||||
|
||||
userSetting := &apiv2pb.UserSetting{
|
||||
userSetting := &apiv1pb.UserSetting{
|
||||
Id: userID,
|
||||
Locale: apiv2pb.UserSetting_LOCALE_EN,
|
||||
ColorTheme: apiv2pb.UserSetting_COLOR_THEME_SYSTEM,
|
||||
Locale: apiv1pb.UserSetting_LOCALE_EN,
|
||||
ColorTheme: apiv1pb.UserSetting_COLOR_THEME_SYSTEM,
|
||||
}
|
||||
for _, setting := range userSettings {
|
||||
if setting.Key == storepb.UserSettingKey_USER_SETTING_LOCALE {
|
||||
@@ -86,50 +89,50 @@ func getUserSetting(ctx context.Context, s *store.Store, userID int32) (*apiv2pb
|
||||
return userSetting, nil
|
||||
}
|
||||
|
||||
func convertUserSettingLocaleToStore(locale apiv2pb.UserSetting_Locale) storepb.LocaleUserSetting {
|
||||
func convertUserSettingLocaleToStore(locale apiv1pb.UserSetting_Locale) storepb.LocaleUserSetting {
|
||||
switch locale {
|
||||
case apiv2pb.UserSetting_LOCALE_EN:
|
||||
case apiv1pb.UserSetting_LOCALE_EN:
|
||||
return storepb.LocaleUserSetting_LOCALE_USER_SETTING_EN
|
||||
case apiv2pb.UserSetting_LOCALE_ZH:
|
||||
case apiv1pb.UserSetting_LOCALE_ZH:
|
||||
return storepb.LocaleUserSetting_LOCALE_USER_SETTING_ZH
|
||||
default:
|
||||
return storepb.LocaleUserSetting_LOCALE_USER_SETTING_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func convertUserSettingLocaleFromStore(locale storepb.LocaleUserSetting) apiv2pb.UserSetting_Locale {
|
||||
func convertUserSettingLocaleFromStore(locale storepb.LocaleUserSetting) apiv1pb.UserSetting_Locale {
|
||||
switch locale {
|
||||
case storepb.LocaleUserSetting_LOCALE_USER_SETTING_EN:
|
||||
return apiv2pb.UserSetting_LOCALE_EN
|
||||
return apiv1pb.UserSetting_LOCALE_EN
|
||||
case storepb.LocaleUserSetting_LOCALE_USER_SETTING_ZH:
|
||||
return apiv2pb.UserSetting_LOCALE_ZH
|
||||
return apiv1pb.UserSetting_LOCALE_ZH
|
||||
default:
|
||||
return apiv2pb.UserSetting_LOCALE_UNSPECIFIED
|
||||
return apiv1pb.UserSetting_LOCALE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func convertUserSettingColorThemeToStore(colorTheme apiv2pb.UserSetting_ColorTheme) storepb.ColorThemeUserSetting {
|
||||
func convertUserSettingColorThemeToStore(colorTheme apiv1pb.UserSetting_ColorTheme) storepb.ColorThemeUserSetting {
|
||||
switch colorTheme {
|
||||
case apiv2pb.UserSetting_COLOR_THEME_SYSTEM:
|
||||
case apiv1pb.UserSetting_COLOR_THEME_SYSTEM:
|
||||
return storepb.ColorThemeUserSetting_COLOR_THEME_USER_SETTING_SYSTEM
|
||||
case apiv2pb.UserSetting_COLOR_THEME_LIGHT:
|
||||
case apiv1pb.UserSetting_COLOR_THEME_LIGHT:
|
||||
return storepb.ColorThemeUserSetting_COLOR_THEME_USER_SETTING_LIGHT
|
||||
case apiv2pb.UserSetting_COLOR_THEME_DARK:
|
||||
case apiv1pb.UserSetting_COLOR_THEME_DARK:
|
||||
return storepb.ColorThemeUserSetting_COLOR_THEME_USER_SETTING_DARK
|
||||
default:
|
||||
return storepb.ColorThemeUserSetting_COLOR_THEME_USER_SETTING_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func convertUserSettingColorThemeFromStore(colorTheme storepb.ColorThemeUserSetting) apiv2pb.UserSetting_ColorTheme {
|
||||
func convertUserSettingColorThemeFromStore(colorTheme storepb.ColorThemeUserSetting) apiv1pb.UserSetting_ColorTheme {
|
||||
switch colorTheme {
|
||||
case storepb.ColorThemeUserSetting_COLOR_THEME_USER_SETTING_SYSTEM:
|
||||
return apiv2pb.UserSetting_COLOR_THEME_SYSTEM
|
||||
return apiv1pb.UserSetting_COLOR_THEME_SYSTEM
|
||||
case storepb.ColorThemeUserSetting_COLOR_THEME_USER_SETTING_LIGHT:
|
||||
return apiv2pb.UserSetting_COLOR_THEME_LIGHT
|
||||
return apiv1pb.UserSetting_COLOR_THEME_LIGHT
|
||||
case storepb.ColorThemeUserSetting_COLOR_THEME_USER_SETTING_DARK:
|
||||
return apiv2pb.UserSetting_COLOR_THEME_DARK
|
||||
return apiv1pb.UserSetting_COLOR_THEME_DARK
|
||||
default:
|
||||
return apiv2pb.UserSetting_COLOR_THEME_UNSPECIFIED
|
||||
return apiv1pb.UserSetting_COLOR_THEME_UNSPECIFIED
|
||||
}
|
||||
}
|
116
api/v1/v1.go
116
api/v1/v1.go
@@ -1,35 +1,123 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"github.com/labstack/echo/v4"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
||||
"github.com/labstack/echo/v4"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
"github.com/yourselfhosted/slash/server/profile"
|
||||
"github.com/yourselfhosted/slash/server/service/license"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
type APIV1Service struct {
|
||||
type APIV2Service struct {
|
||||
apiv1pb.UnimplementedWorkspaceServiceServer
|
||||
apiv1pb.UnimplementedSubscriptionServiceServer
|
||||
apiv1pb.UnimplementedAuthServiceServer
|
||||
apiv1pb.UnimplementedUserServiceServer
|
||||
apiv1pb.UnimplementedUserSettingServiceServer
|
||||
apiv1pb.UnimplementedShortcutServiceServer
|
||||
apiv1pb.UnimplementedCollectionServiceServer
|
||||
apiv1pb.UnimplementedMemoServiceServer
|
||||
|
||||
Secret string
|
||||
Profile *profile.Profile
|
||||
Store *store.Store
|
||||
LicenseService *license.LicenseService
|
||||
|
||||
grpcServer *grpc.Server
|
||||
grpcServerPort int
|
||||
}
|
||||
|
||||
func NewAPIV1Service(profile *profile.Profile, store *store.Store, licenseService *license.LicenseService) *APIV1Service {
|
||||
return &APIV1Service{
|
||||
func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store, licenseService *license.LicenseService, grpcServerPort int) *APIV2Service {
|
||||
authProvider := NewGRPCAuthInterceptor(store, secret)
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(
|
||||
authProvider.AuthenticationInterceptor,
|
||||
),
|
||||
)
|
||||
apiV2Service := &APIV2Service{
|
||||
Secret: secret,
|
||||
Profile: profile,
|
||||
Store: store,
|
||||
LicenseService: licenseService,
|
||||
grpcServer: grpcServer,
|
||||
grpcServerPort: grpcServerPort,
|
||||
}
|
||||
|
||||
apiv1pb.RegisterSubscriptionServiceServer(grpcServer, apiV2Service)
|
||||
apiv1pb.RegisterWorkspaceServiceServer(grpcServer, apiV2Service)
|
||||
apiv1pb.RegisterAuthServiceServer(grpcServer, apiV2Service)
|
||||
apiv1pb.RegisterUserServiceServer(grpcServer, apiV2Service)
|
||||
apiv1pb.RegisterUserSettingServiceServer(grpcServer, apiV2Service)
|
||||
apiv1pb.RegisterShortcutServiceServer(grpcServer, apiV2Service)
|
||||
apiv1pb.RegisterCollectionServiceServer(grpcServer, apiV2Service)
|
||||
apiv1pb.RegisterMemoServiceServer(grpcServer, apiV2Service)
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
return apiV2Service
|
||||
}
|
||||
|
||||
func (s *APIV1Service) Start(apiGroup *echo.Group, secret string) {
|
||||
apiV1Group := apiGroup.Group("/api/v1")
|
||||
apiV1Group.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return JWTMiddleware(s, next, secret)
|
||||
})
|
||||
s.registerWorkspaceRoutes(apiV1Group)
|
||||
s.registerAuthRoutes(apiV1Group, secret)
|
||||
s.registerUserRoutes(apiV1Group)
|
||||
s.registerShortcutRoutes(apiV1Group)
|
||||
s.registerAnalyticsRoutes(apiV1Group)
|
||||
func (s *APIV2Service) GetGRPCServer() *grpc.Server {
|
||||
return s.grpcServer
|
||||
}
|
||||
|
||||
// RegisterGateway registers the gRPC-Gateway with the given Echo instance.
|
||||
func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error {
|
||||
// Create a client connection to the gRPC Server we just started.
|
||||
// This is where the gRPC-Gateway proxies the requests.
|
||||
conn, err := grpc.DialContext(
|
||||
ctx,
|
||||
fmt.Sprintf(":%d", s.grpcServerPort),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gwMux := runtime.NewServeMux()
|
||||
if err := apiv1pb.RegisterSubscriptionServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv1pb.RegisterWorkspaceServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv1pb.RegisterAuthServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv1pb.RegisterUserServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv1pb.RegisterUserSettingServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv1pb.RegisterShortcutServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv1pb.RegisterCollectionServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv1pb.RegisterMemoServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Any("/api/v1/*", echo.WrapHandler(gwMux))
|
||||
|
||||
// GRPC web proxy.
|
||||
options := []grpcweb.Option{
|
||||
grpcweb.WithCorsForRegisteredEndpointsOnly(false),
|
||||
grpcweb.WithOriginFunc(func(_ string) bool {
|
||||
return true
|
||||
}),
|
||||
}
|
||||
wrappedGrpc := grpcweb.WrapServer(s.grpcServer, options...)
|
||||
e.Any("/slash.api.v1.*", echo.WrapHandler(wrappedGrpc))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,39 +0,0 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/server/profile"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
type WorkspaceProfile struct {
|
||||
Profile *profile.Profile `json:"profile"`
|
||||
DisallowSignUp bool `json:"disallowSignUp"`
|
||||
}
|
||||
|
||||
func (s *APIV1Service) registerWorkspaceRoutes(g *echo.Group) {
|
||||
g.GET("/workspace/profile", func(c echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
workspaceProfile := WorkspaceProfile{
|
||||
Profile: s.Profile,
|
||||
DisallowSignUp: false,
|
||||
}
|
||||
|
||||
enableSignUpSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{
|
||||
Key: storepb.WorkspaceSettingKey_WORKSAPCE_SETTING_ENABLE_SIGNUP,
|
||||
})
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("failed to find workspace setting, err: %s", err)).SetInternal(err)
|
||||
}
|
||||
if enableSignUpSetting != nil {
|
||||
workspaceProfile.DisallowSignUp = !enableSignUpSetting.GetEnableSignup()
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, workspaceProfile)
|
||||
})
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package v2
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -6,16 +6,16 @@ import (
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
apiv1pb "github.com/yourselfhosted/slash/proto/gen/api/v1"
|
||||
storepb "github.com/yourselfhosted/slash/proto/gen/store"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func (s *APIV2Service) GetWorkspaceProfile(ctx context.Context, _ *apiv2pb.GetWorkspaceProfileRequest) (*apiv2pb.GetWorkspaceProfileResponse, error) {
|
||||
profile := &apiv2pb.WorkspaceProfile{
|
||||
func (s *APIV2Service) GetWorkspaceProfile(ctx context.Context, _ *apiv1pb.GetWorkspaceProfileRequest) (*apiv1pb.GetWorkspaceProfileResponse, error) {
|
||||
profile := &apiv1pb.WorkspaceProfile{
|
||||
Mode: s.Profile.Mode,
|
||||
Version: s.Profile.Version,
|
||||
Plan: apiv2pb.PlanType_FREE,
|
||||
Plan: apiv1pb.PlanType_FREE,
|
||||
}
|
||||
|
||||
// Load subscription plan from license service.
|
||||
@@ -25,7 +25,7 @@ func (s *APIV2Service) GetWorkspaceProfile(ctx context.Context, _ *apiv2pb.GetWo
|
||||
}
|
||||
profile.Plan = subscription.Plan
|
||||
|
||||
workspaceSetting, err := s.GetWorkspaceSetting(ctx, &apiv2pb.GetWorkspaceSettingRequest{})
|
||||
workspaceSetting, err := s.GetWorkspaceSetting(ctx, &apiv1pb.GetWorkspaceSettingRequest{})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err)
|
||||
}
|
||||
@@ -35,12 +35,12 @@ func (s *APIV2Service) GetWorkspaceProfile(ctx context.Context, _ *apiv2pb.GetWo
|
||||
profile.CustomStyle = setting.GetCustomStyle()
|
||||
profile.CustomScript = setting.GetCustomScript()
|
||||
}
|
||||
return &apiv2pb.GetWorkspaceProfileResponse{
|
||||
return &apiv1pb.GetWorkspaceProfileResponse{
|
||||
Profile: profile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.GetWorkspaceSettingRequest) (*apiv2pb.GetWorkspaceSettingResponse, error) {
|
||||
func (s *APIV2Service) GetWorkspaceSetting(ctx context.Context, _ *apiv1pb.GetWorkspaceSettingRequest) (*apiv1pb.GetWorkspaceSettingResponse, error) {
|
||||
isAdmin := false
|
||||
userID, ok := ctx.Value(userIDContextKey).(int32)
|
||||
if ok {
|
||||
@@ -56,7 +56,7 @@ func (s *APIV2Service) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.GetWo
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to list workspace settings: %v", err)
|
||||
}
|
||||
workspaceSetting := &apiv2pb.WorkspaceSetting{
|
||||
workspaceSetting := &apiv1pb.WorkspaceSetting{
|
||||
EnableSignup: true,
|
||||
}
|
||||
for _, v := range workspaceSettings {
|
||||
@@ -75,12 +75,12 @@ func (s *APIV2Service) GetWorkspaceSetting(ctx context.Context, _ *apiv2pb.GetWo
|
||||
}
|
||||
}
|
||||
}
|
||||
return &apiv2pb.GetWorkspaceSettingResponse{
|
||||
return &apiv1pb.GetWorkspaceSettingResponse{
|
||||
Setting: workspaceSetting,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *APIV2Service) UpdateWorkspaceSetting(ctx context.Context, request *apiv2pb.UpdateWorkspaceSettingRequest) (*apiv2pb.UpdateWorkspaceSettingResponse, error) {
|
||||
func (s *APIV2Service) UpdateWorkspaceSetting(ctx context.Context, request *apiv1pb.UpdateWorkspaceSettingRequest) (*apiv1pb.UpdateWorkspaceSettingResponse, error) {
|
||||
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "update mask is empty")
|
||||
}
|
||||
@@ -136,11 +136,11 @@ func (s *APIV2Service) UpdateWorkspaceSetting(ctx context.Context, request *apiv
|
||||
}
|
||||
}
|
||||
|
||||
getWorkspaceSettingResponse, err := s.GetWorkspaceSetting(ctx, &apiv2pb.GetWorkspaceSettingRequest{})
|
||||
getWorkspaceSettingResponse, err := s.GetWorkspaceSetting(ctx, &apiv1pb.GetWorkspaceSettingRequest{})
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err)
|
||||
}
|
||||
return &apiv2pb.UpdateWorkspaceSettingResponse{
|
||||
return &apiv1pb.UpdateWorkspaceSettingResponse{
|
||||
Setting: getWorkspaceSettingResponse.Setting,
|
||||
}, nil
|
||||
}
|
@@ -1,35 +0,0 @@
|
||||
package v2
|
||||
|
||||
import "strings"
|
||||
|
||||
var allowedMethodsWhenUnauthorized = map[string]bool{
|
||||
"/slash.api.v2.WorkspaceService/GetWorkspaceProfile": true,
|
||||
"/slash.api.v2.WorkspaceService/GetWorkspaceSetting": true,
|
||||
"/slash.api.v2.AuthService/SignIn": true,
|
||||
"/slash.api.v2.AuthService/SignUp": true,
|
||||
"/slash.api.v2.AuthService/SignOut": true,
|
||||
"/memos.api.v2.AuthService/GetAuthStatus": true,
|
||||
"/slash.api.v2.ShortcutService/GetShortcutByName": true,
|
||||
"/slash.api.v2.ShortcutService/GetShortcut": true,
|
||||
"/slash.api.v2.CollectionService/GetCollectionByName": true,
|
||||
}
|
||||
|
||||
// isUnauthorizeAllowedMethod returns true if the method is allowed to be called when the user is not authorized.
|
||||
func isUnauthorizeAllowedMethod(methodName string) bool {
|
||||
if strings.HasPrefix(methodName, "/grpc.reflection") {
|
||||
return true
|
||||
}
|
||||
return allowedMethodsWhenUnauthorized[methodName]
|
||||
}
|
||||
|
||||
var allowedMethodsOnlyForAdmin = map[string]bool{
|
||||
"/slash.api.v2.UserService/CreateUser": true,
|
||||
"/slash.api.v2.UserService/DeleteUser": true,
|
||||
"/slash.api.v2.WorkspaceService/UpdateWorkspaceSetting": true,
|
||||
"/slash.api.v2.SubscriptionService/UpdateSubscription": true,
|
||||
}
|
||||
|
||||
// isOnlyForAdminAllowedMethod returns true if the method is allowed to be called only by admin.
|
||||
func isOnlyForAdminAllowedMethod(methodName string) bool {
|
||||
return allowedMethodsOnlyForAdmin[methodName]
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
func convertRowStatusFromStore(rowStatus store.RowStatus) apiv2pb.RowStatus {
|
||||
switch rowStatus {
|
||||
case store.Normal:
|
||||
return apiv2pb.RowStatus_NORMAL
|
||||
case store.Archived:
|
||||
return apiv2pb.RowStatus_ARCHIVED
|
||||
default:
|
||||
return apiv2pb.RowStatus_ROW_STATUS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func getCurrentUser(ctx context.Context, s *store.Store) (*store.User, error) {
|
||||
userID, ok := ctx.Value(userIDContextKey).(int32)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
user, err := s.GetUser(ctx, &store.FindUser{
|
||||
ID: &userID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
123
api/v2/v2.go
123
api/v2/v2.go
@@ -1,123 +0,0 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
|
||||
"github.com/improbable-eng/grpc-web/go/grpcweb"
|
||||
"github.com/labstack/echo/v4"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/reflection"
|
||||
|
||||
apiv2pb "github.com/yourselfhosted/slash/proto/gen/api/v2"
|
||||
"github.com/yourselfhosted/slash/server/profile"
|
||||
"github.com/yourselfhosted/slash/server/service/license"
|
||||
"github.com/yourselfhosted/slash/store"
|
||||
)
|
||||
|
||||
type APIV2Service struct {
|
||||
apiv2pb.UnimplementedWorkspaceServiceServer
|
||||
apiv2pb.UnimplementedSubscriptionServiceServer
|
||||
apiv2pb.UnimplementedAuthServiceServer
|
||||
apiv2pb.UnimplementedUserServiceServer
|
||||
apiv2pb.UnimplementedUserSettingServiceServer
|
||||
apiv2pb.UnimplementedShortcutServiceServer
|
||||
apiv2pb.UnimplementedCollectionServiceServer
|
||||
apiv2pb.UnimplementedMemoServiceServer
|
||||
|
||||
Secret string
|
||||
Profile *profile.Profile
|
||||
Store *store.Store
|
||||
LicenseService *license.LicenseService
|
||||
|
||||
grpcServer *grpc.Server
|
||||
grpcServerPort int
|
||||
}
|
||||
|
||||
func NewAPIV2Service(secret string, profile *profile.Profile, store *store.Store, licenseService *license.LicenseService, grpcServerPort int) *APIV2Service {
|
||||
authProvider := NewGRPCAuthInterceptor(store, secret)
|
||||
grpcServer := grpc.NewServer(
|
||||
grpc.ChainUnaryInterceptor(
|
||||
authProvider.AuthenticationInterceptor,
|
||||
),
|
||||
)
|
||||
apiV2Service := &APIV2Service{
|
||||
Secret: secret,
|
||||
Profile: profile,
|
||||
Store: store,
|
||||
LicenseService: licenseService,
|
||||
grpcServer: grpcServer,
|
||||
grpcServerPort: grpcServerPort,
|
||||
}
|
||||
|
||||
apiv2pb.RegisterSubscriptionServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterWorkspaceServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterAuthServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterUserServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterUserSettingServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterShortcutServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterCollectionServiceServer(grpcServer, apiV2Service)
|
||||
apiv2pb.RegisterMemoServiceServer(grpcServer, apiV2Service)
|
||||
reflection.Register(grpcServer)
|
||||
|
||||
return apiV2Service
|
||||
}
|
||||
|
||||
func (s *APIV2Service) GetGRPCServer() *grpc.Server {
|
||||
return s.grpcServer
|
||||
}
|
||||
|
||||
// RegisterGateway registers the gRPC-Gateway with the given Echo instance.
|
||||
func (s *APIV2Service) RegisterGateway(ctx context.Context, e *echo.Echo) error {
|
||||
// Create a client connection to the gRPC Server we just started.
|
||||
// This is where the gRPC-Gateway proxies the requests.
|
||||
conn, err := grpc.DialContext(
|
||||
ctx,
|
||||
fmt.Sprintf(":%d", s.grpcServerPort),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gwMux := runtime.NewServeMux()
|
||||
if err := apiv2pb.RegisterSubscriptionServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterWorkspaceServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterAuthServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterUserServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterUserSettingServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterShortcutServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterCollectionServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := apiv2pb.RegisterMemoServiceHandler(context.Background(), gwMux, conn); err != nil {
|
||||
return err
|
||||
}
|
||||
e.Any("/api/v2/*", echo.WrapHandler(gwMux))
|
||||
|
||||
// GRPC web proxy.
|
||||
options := []grpcweb.Option{
|
||||
grpcweb.WithCorsForRegisteredEndpointsOnly(false),
|
||||
grpcweb.WithOriginFunc(func(origin string) bool {
|
||||
return true
|
||||
}),
|
||||
}
|
||||
wrappedGrpc := grpcweb.WrapServer(s.grpcServer, options...)
|
||||
e.Any("/slash.api.v2.*", echo.WrapHandler(wrappedGrpc))
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user