mirror of https://github.com/usememos/memos
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
318 lines
10 KiB
Go
318 lines
10 KiB
Go
package v1
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/emptypb"
|
|
|
|
"github.com/usememos/memos/internal/util"
|
|
v1pb "github.com/usememos/memos/proto/gen/api/v1"
|
|
storepb "github.com/usememos/memos/proto/gen/store"
|
|
)
|
|
|
|
func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWebhookRequest) (*v1pb.Webhook, error) {
|
|
currentUser, err := s.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Extract user ID from parent (format: users/{user})
|
|
parentUserID, err := ExtractUserIDFromName(request.Parent)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
|
|
}
|
|
|
|
// Users can only create webhooks for themselves
|
|
if parentUserID != currentUser.ID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
// Only host users can create webhooks
|
|
if !isSuperUser(currentUser) {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
// Validate required fields
|
|
if request.Webhook == nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "webhook is required")
|
|
}
|
|
if strings.TrimSpace(request.Webhook.Url) == "" {
|
|
return nil, status.Errorf(codes.InvalidArgument, "webhook URL is required")
|
|
}
|
|
|
|
// Handle validate_only field
|
|
if request.ValidateOnly {
|
|
// Perform validation checks without actually creating the webhook
|
|
return &v1pb.Webhook{
|
|
Name: fmt.Sprintf("users/%d/webhooks/validate", currentUser.ID),
|
|
DisplayName: request.Webhook.DisplayName,
|
|
Url: request.Webhook.Url,
|
|
}, nil
|
|
}
|
|
|
|
err = s.Store.AddUserWebhook(ctx, currentUser.ID, &storepb.WebhooksUserSetting_Webhook{
|
|
Id: generateWebhookID(),
|
|
Title: request.Webhook.DisplayName,
|
|
Url: strings.TrimSpace(request.Webhook.Url),
|
|
})
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to create webhook, error: %+v", err)
|
|
}
|
|
|
|
// Return the newly created webhook
|
|
webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get user webhooks, error: %+v", err)
|
|
}
|
|
|
|
// Find the webhook we just created
|
|
for _, webhook := range webhooks {
|
|
if webhook.Title == request.Webhook.DisplayName && webhook.Url == strings.TrimSpace(request.Webhook.Url) {
|
|
return convertWebhookFromUserSetting(webhook, currentUser.ID), nil
|
|
}
|
|
}
|
|
|
|
return nil, status.Errorf(codes.Internal, "failed to find created webhook")
|
|
}
|
|
|
|
func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebhooksRequest) (*v1pb.ListWebhooksResponse, error) {
|
|
currentUser, err := s.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Extract user ID from parent (format: users/{user})
|
|
parentUserID, err := ExtractUserIDFromName(request.Parent)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err)
|
|
}
|
|
|
|
// Users can only list their own webhooks
|
|
if parentUserID != currentUser.ID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to list webhooks, error: %+v", err)
|
|
}
|
|
|
|
response := &v1pb.ListWebhooksResponse{
|
|
Webhooks: []*v1pb.Webhook{},
|
|
}
|
|
for _, webhook := range webhooks {
|
|
response.Webhooks = append(response.Webhooks, convertWebhookFromUserSetting(webhook, currentUser.ID))
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookRequest) (*v1pb.Webhook, error) {
|
|
// Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook})
|
|
tokens, err := GetNameParentTokens(request.Name, UserNamePrefix, WebhookNamePrefix)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
|
|
}
|
|
if len(tokens) != 2 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format")
|
|
}
|
|
|
|
userIDStr := tokens[0]
|
|
webhookID := tokens[1]
|
|
|
|
requestedUserID, err := util.ConvertStringToInt32(userIDStr)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err)
|
|
}
|
|
|
|
currentUser, err := s.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Users can only access their own webhooks
|
|
if requestedUserID != currentUser.ID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get webhooks, error: %+v", err)
|
|
}
|
|
|
|
// Find webhook by ID
|
|
for _, webhook := range webhooks {
|
|
if webhook.Id == webhookID {
|
|
return convertWebhookFromUserSetting(webhook, currentUser.ID), nil
|
|
}
|
|
}
|
|
return nil, status.Errorf(codes.NotFound, "webhook not found")
|
|
}
|
|
|
|
func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWebhookRequest) (*v1pb.Webhook, error) {
|
|
if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "update_mask is required")
|
|
}
|
|
|
|
// Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook})
|
|
tokens, err := GetNameParentTokens(request.Webhook.Name, UserNamePrefix, WebhookNamePrefix)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
|
|
}
|
|
if len(tokens) != 2 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format")
|
|
}
|
|
|
|
userIDStr := tokens[0]
|
|
webhookID := tokens[1]
|
|
|
|
requestedUserID, err := util.ConvertStringToInt32(userIDStr)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err)
|
|
}
|
|
|
|
currentUser, err := s.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Users can only update their own webhooks
|
|
if requestedUserID != currentUser.ID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
// Get existing webhooks from user settings
|
|
webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get webhooks: %v", err)
|
|
}
|
|
|
|
// Find the webhook to update
|
|
var existingWebhook *storepb.WebhooksUserSetting_Webhook
|
|
for _, webhook := range webhooks {
|
|
if webhook.Id == webhookID {
|
|
existingWebhook = webhook
|
|
break
|
|
}
|
|
}
|
|
|
|
if existingWebhook == nil {
|
|
return nil, status.Errorf(codes.NotFound, "webhook not found")
|
|
}
|
|
|
|
// Create updated webhook
|
|
updatedWebhook := &storepb.WebhooksUserSetting_Webhook{
|
|
Id: existingWebhook.Id,
|
|
Title: existingWebhook.Title,
|
|
Url: existingWebhook.Url,
|
|
}
|
|
|
|
// Apply updates based on update mask
|
|
for _, field := range request.UpdateMask.Paths {
|
|
switch field {
|
|
case "display_name":
|
|
updatedWebhook.Title = request.Webhook.DisplayName
|
|
case "url":
|
|
updatedWebhook.Url = request.Webhook.Url
|
|
default:
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
|
|
}
|
|
}
|
|
|
|
// Update the webhook in user settings
|
|
err = s.Store.UpdateUserWebhook(ctx, currentUser.ID, updatedWebhook)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to update webhook: %v", err)
|
|
}
|
|
|
|
return convertWebhookFromUserSetting(updatedWebhook, currentUser.ID), nil
|
|
}
|
|
|
|
func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWebhookRequest) (*emptypb.Empty, error) {
|
|
// Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook})
|
|
tokens, err := GetNameParentTokens(request.Name, UserNamePrefix, WebhookNamePrefix)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err)
|
|
}
|
|
if len(tokens) != 2 {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format")
|
|
}
|
|
|
|
userIDStr := tokens[0]
|
|
webhookID := tokens[1]
|
|
|
|
requestedUserID, err := util.ConvertStringToInt32(userIDStr)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err)
|
|
}
|
|
|
|
currentUser, err := s.GetCurrentUser(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get user: %v", err)
|
|
}
|
|
if currentUser == nil {
|
|
return nil, status.Errorf(codes.Unauthenticated, "user not authenticated")
|
|
}
|
|
|
|
// Users can only delete their own webhooks
|
|
if requestedUserID != currentUser.ID {
|
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
|
}
|
|
|
|
// Get existing webhooks from user settings to verify it exists
|
|
webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to get webhooks: %v", err)
|
|
}
|
|
|
|
// Check if webhook exists
|
|
webhookExists := false
|
|
for _, webhook := range webhooks {
|
|
if webhook.Id == webhookID {
|
|
webhookExists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !webhookExists {
|
|
return nil, status.Errorf(codes.NotFound, "webhook not found")
|
|
}
|
|
|
|
err = s.Store.RemoveUserWebhook(ctx, currentUser.ID, webhookID)
|
|
if err != nil {
|
|
return nil, status.Errorf(codes.Internal, "failed to delete webhook: %v", err)
|
|
}
|
|
return &emptypb.Empty{}, nil
|
|
}
|
|
|
|
func convertWebhookFromUserSetting(webhook *storepb.WebhooksUserSetting_Webhook, userID int32) *v1pb.Webhook {
|
|
return &v1pb.Webhook{
|
|
Name: fmt.Sprintf("users/%d/webhooks/%s", userID, webhook.Id),
|
|
DisplayName: webhook.Title,
|
|
Url: webhook.Url,
|
|
}
|
|
}
|
|
|
|
func generateWebhookID() string {
|
|
b := make([]byte, 8)
|
|
rand.Read(b)
|
|
return hex.EncodeToString(b)
|
|
}
|