161 lines
5.8 KiB
Go
161 lines
5.8 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package apiv2
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"code.vikunja.io/api/pkg/config"
|
|
"code.vikunja.io/api/pkg/models"
|
|
"code.vikunja.io/api/pkg/web/handler"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
"github.com/danielgtaylor/huma/v2/conditional"
|
|
)
|
|
|
|
type linkShareListBody struct {
|
|
Body Paginated[*models.LinkSharing]
|
|
}
|
|
|
|
// RegisterLinkSharingRoutes wires the nested LinkSharing routes onto the Huma
|
|
// API. There is no update operation — a share is created, read, listed or
|
|
// deleted, never modified in place.
|
|
//
|
|
// The feature gate is checked here, not in the central wiring: the registrar
|
|
// runs at RegisterAll time after the config has loaded, so a disabled instance
|
|
// registers no link-sharing routes at all.
|
|
func RegisterLinkSharingRoutes(api huma.API) {
|
|
if !config.ServiceEnableLinkSharing.GetBool() {
|
|
return
|
|
}
|
|
|
|
tags := []string{"sharing"}
|
|
|
|
Register(api, huma.Operation{
|
|
OperationID: "shares-list",
|
|
Summary: "List the link shares of a project",
|
|
Description: "Returns the link shares of the given project, paginated. Only project admins may list them.",
|
|
Method: http.MethodGet,
|
|
Path: "/projects/{project}/shares",
|
|
Tags: tags,
|
|
}, linkSharesList)
|
|
|
|
Register(api, huma.Operation{
|
|
OperationID: "shares-read",
|
|
Summary: "Get a single link share of a project",
|
|
Description: "Returns one link share of a project. The share must belong to the project in the path. Sends an ETag; pass it as If-None-Match on a later read to get a 304 Not Modified.",
|
|
Method: http.MethodGet,
|
|
Path: "/projects/{project}/shares/{share}",
|
|
Tags: tags,
|
|
}, linkSharesRead)
|
|
|
|
Register(api, huma.Operation{
|
|
OperationID: "shares-create",
|
|
Summary: "Share a project via link",
|
|
Description: "Creates a link share for the given project. The parent project is taken from the URL, not the body, and the authenticated user becomes the sharer. Creating an admin share requires project admin; read/write shares require write access. The hash is generated by the server; a password, if set, is write-only and cannot be read back.",
|
|
Method: http.MethodPost,
|
|
Path: "/projects/{project}/shares",
|
|
Tags: tags,
|
|
}, linkSharesCreate)
|
|
|
|
Register(api, huma.Operation{
|
|
OperationID: "shares-delete",
|
|
Summary: "Remove a link share from a project",
|
|
Description: "Deletes a link share of a project. The share must belong to the project in the path. Requires write access to the project.",
|
|
Method: http.MethodDelete,
|
|
Path: "/projects/{project}/shares/{share}",
|
|
Tags: tags,
|
|
}, linkSharesDelete)
|
|
}
|
|
|
|
func init() { AddRouteRegistrar(RegisterLinkSharingRoutes) }
|
|
|
|
func linkSharesList(ctx context.Context, in *struct {
|
|
ProjectID int64 `path:"project"`
|
|
ListParams
|
|
}) (*linkShareListBody, error) {
|
|
a, err := authFromCtx(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result, _, total, err := handler.DoReadAll(ctx, &models.LinkSharing{ProjectID: in.ProjectID}, a, in.Q, in.Page, in.PerPage)
|
|
if err != nil {
|
|
return nil, translateDomainError(err)
|
|
}
|
|
items, ok := result.([]*models.LinkSharing)
|
|
if !ok {
|
|
return nil, fmt.Errorf("linkShares.ReadAll returned unexpected type %T (expected []*models.LinkSharing)", result)
|
|
}
|
|
return &linkShareListBody{Body: NewPaginated(items, total, in.Page, in.PerPage)}, nil
|
|
}
|
|
|
|
type linkShareReadBody struct {
|
|
models.LinkSharing
|
|
MaxPermission models.Permission `json:"max_permission" readOnly:"true" doc:"The maximum permission the requesting user has on this link share (0=read, 1=read/write, 2=admin)."`
|
|
}
|
|
|
|
func linkSharesRead(ctx context.Context, in *struct {
|
|
ProjectID int64 `path:"project"`
|
|
ID int64 `path:"share"`
|
|
conditional.Params
|
|
}) (*singleReadBody[linkShareReadBody], error) {
|
|
a, err := authFromCtx(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// ProjectID scopes the lookup to the parent project, guarding against
|
|
// reading a share of one project through another.
|
|
share := &models.LinkSharing{ID: in.ID, ProjectID: in.ProjectID}
|
|
maxPermission, err := handler.DoReadOne(ctx, share, a)
|
|
if err != nil {
|
|
return nil, translateDomainError(err)
|
|
}
|
|
body := &linkShareReadBody{LinkSharing: *share, MaxPermission: models.Permission(maxPermission)}
|
|
return conditionalReadResponse(&in.Params, body, share.Updated, maxPermission)
|
|
}
|
|
|
|
func linkSharesCreate(ctx context.Context, in *struct {
|
|
ProjectID int64 `path:"project"`
|
|
Body models.LinkSharing
|
|
}) (*singleBody[models.LinkSharing], error) {
|
|
a, err := authFromCtx(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
in.Body.ProjectID = in.ProjectID // URL wins over body
|
|
if err := handler.DoCreate(ctx, &in.Body, a); err != nil {
|
|
return nil, translateDomainError(err)
|
|
}
|
|
return &singleBody[models.LinkSharing]{Body: &in.Body}, nil
|
|
}
|
|
|
|
func linkSharesDelete(ctx context.Context, in *struct {
|
|
ProjectID int64 `path:"project"`
|
|
ID int64 `path:"share"`
|
|
}) (*emptyBody, error) {
|
|
a, err := authFromCtx(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := handler.DoDelete(ctx, &models.LinkSharing{ID: in.ID, ProjectID: in.ProjectID}, a); err != nil {
|
|
return nil, translateDomainError(err)
|
|
}
|
|
return &emptyBody{}, nil
|
|
}
|