package grpcserver import ( "context" "database/sql" "time" pb "git.wntrmute.dev/kyle/eng-pad-server/gen/engpad/v1" "git.wntrmute.dev/kyle/eng-pad-server/internal/share" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/timestamppb" ) func (s *SyncService) CreateShareLink(ctx context.Context, req *pb.CreateShareLinkRequest) (*pb.CreateShareLinkResponse, error) { userID, ok := UserIDFromContext(ctx) if !ok { return nil, status.Error(codes.Internal, "missing user context") } // Verify notebook belongs to user var notebookID int64 err := s.DB.QueryRowContext(ctx, "SELECT id FROM notebooks WHERE user_id = ? AND remote_id = ?", userID, req.NotebookId, ).Scan(¬ebookID) if err == sql.ErrNoRows { return nil, status.Error(codes.NotFound, "notebook not found") } else if err != nil { return nil, status.Errorf(codes.Internal, "query: %v", err) } var expiry time.Duration if req.ExpiresInSeconds > 0 { expiry = time.Duration(req.ExpiresInSeconds) * time.Second } token, expiresAt, err := share.CreateLink(s.DB, notebookID, expiry, s.BaseURL) if err != nil { return nil, status.Errorf(codes.Internal, "create link: %v", err) } resp := &pb.CreateShareLinkResponse{ Token: token, Url: s.BaseURL + "/s/" + token, } if expiresAt != nil { resp.ExpiresAt = timestamppb.New(*expiresAt) } return resp, nil } func (s *SyncService) RevokeShareLink(ctx context.Context, req *pb.RevokeShareLinkRequest) (*pb.RevokeShareLinkResponse, error) { userID, ok := UserIDFromContext(ctx) if !ok { return nil, status.Error(codes.Internal, "missing user context") } // Verify the calling user owns the notebook associated with this share link. var count int err := s.DB.QueryRowContext(ctx, `SELECT COUNT(*) FROM share_links sl JOIN notebooks n ON sl.notebook_id = n.id WHERE sl.token = ? AND n.user_id = ?`, req.Token, userID, ).Scan(&count) if err != nil || count == 0 { return nil, status.Error(codes.NotFound, "share link not found") } if err := share.RevokeLink(s.DB, req.Token); err != nil { return nil, status.Errorf(codes.Internal, "revoke: %v", err) } return &pb.RevokeShareLinkResponse{}, nil } func (s *SyncService) ListShareLinks(ctx context.Context, req *pb.ListShareLinksRequest) (*pb.ListShareLinksResponse, error) { userID, ok := UserIDFromContext(ctx) if !ok { return nil, status.Error(codes.Internal, "missing user context") } var notebookID int64 err := s.DB.QueryRowContext(ctx, "SELECT id FROM notebooks WHERE user_id = ? AND remote_id = ?", userID, req.NotebookId, ).Scan(¬ebookID) if err != nil { return nil, status.Error(codes.NotFound, "notebook not found") } links, err := share.ListLinks(s.DB, notebookID, s.BaseURL) if err != nil { return nil, status.Errorf(codes.Internal, "list: %v", err) } var pbLinks []*pb.ShareLinkInfo for _, l := range links { pbl := &pb.ShareLinkInfo{ Token: l.Token, Url: l.URL, CreatedAt: timestamppb.New(l.CreatedAt), } if l.ExpiresAt != nil { pbl.ExpiresAt = timestamppb.New(*l.ExpiresAt) } pbLinks = append(pbLinks, pbl) } return &pb.ListShareLinksResponse{Links: pbLinks}, nil }