mirror of
https://github.com/go-gitea/gitea
synced 2025-12-07 13:28:25 +00:00
Merge branch 'main' into api-repo-actions
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
syntax = "proto3";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
package github.actions.results.api.v1;
|
||||
|
||||
message CreateArtifactRequest {
|
||||
string workflow_run_backend_id = 1;
|
||||
string workflow_job_run_backend_id = 2;
|
||||
string name = 3;
|
||||
google.protobuf.Timestamp expires_at = 4;
|
||||
int32 version = 5;
|
||||
}
|
||||
|
||||
message CreateArtifactResponse {
|
||||
bool ok = 1;
|
||||
string signed_upload_url = 2;
|
||||
}
|
||||
|
||||
message FinalizeArtifactRequest {
|
||||
string workflow_run_backend_id = 1;
|
||||
string workflow_job_run_backend_id = 2;
|
||||
string name = 3;
|
||||
int64 size = 4;
|
||||
google.protobuf.StringValue hash = 5;
|
||||
}
|
||||
|
||||
message FinalizeArtifactResponse {
|
||||
bool ok = 1;
|
||||
int64 artifact_id = 2;
|
||||
}
|
||||
|
||||
message ListArtifactsRequest {
|
||||
string workflow_run_backend_id = 1;
|
||||
string workflow_job_run_backend_id = 2;
|
||||
google.protobuf.StringValue name_filter = 3;
|
||||
google.protobuf.Int64Value id_filter = 4;
|
||||
}
|
||||
|
||||
message ListArtifactsResponse {
|
||||
repeated ListArtifactsResponse_MonolithArtifact artifacts = 1;
|
||||
}
|
||||
|
||||
message ListArtifactsResponse_MonolithArtifact {
|
||||
string workflow_run_backend_id = 1;
|
||||
string workflow_job_run_backend_id = 2;
|
||||
int64 database_id = 3;
|
||||
string name = 4;
|
||||
int64 size = 5;
|
||||
google.protobuf.Timestamp created_at = 6;
|
||||
}
|
||||
|
||||
message GetSignedArtifactURLRequest {
|
||||
string workflow_run_backend_id = 1;
|
||||
string workflow_job_run_backend_id = 2;
|
||||
string name = 3;
|
||||
}
|
||||
|
||||
message GetSignedArtifactURLResponse {
|
||||
string signed_url = 1;
|
||||
}
|
||||
|
||||
message DeleteArtifactRequest {
|
||||
string workflow_run_backend_id = 1;
|
||||
string workflow_job_run_backend_id = 2;
|
||||
string name = 3;
|
||||
}
|
||||
|
||||
message DeleteArtifactResponse {
|
||||
bool ok = 1;
|
||||
int64 artifact_id = 2;
|
||||
}
|
||||
@@ -70,7 +70,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@@ -78,6 +78,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
web_types "code.gitea.io/gitea/modules/web/types"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const artifactRouteBase = "/_apis/pipelines/workflows/{run_id}/artifacts"
|
||||
@@ -137,12 +139,33 @@ func ArtifactContexter() func(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
authToken := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
task, err := actions.GetRunningTaskByToken(req.Context(), authToken)
|
||||
if err != nil {
|
||||
log.Error("Error runner api getting task: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting task")
|
||||
return
|
||||
// New act_runner uses jwt to authenticate
|
||||
tID, err := actions_service.ParseAuthorizationToken(req)
|
||||
|
||||
var task *actions.ActionTask
|
||||
if err == nil {
|
||||
|
||||
task, err = actions.GetTaskByID(req.Context(), tID)
|
||||
if err != nil {
|
||||
log.Error("Error runner api getting task by ID: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
|
||||
return
|
||||
}
|
||||
if task.Status != actions.StatusRunning {
|
||||
log.Error("Error runner api getting task: task is not running")
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// Old act_runner uses GITEA_TOKEN to authenticate
|
||||
authToken := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
task, err = actions.GetRunningTaskByToken(req.Context(), authToken)
|
||||
if err != nil {
|
||||
log.Error("Error runner api getting task: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting task")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := task.LoadJob(req.Context()); err != nil {
|
||||
@@ -257,8 +280,11 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// update artifact size if zero
|
||||
if artifact.FileSize == 0 || artifact.FileCompressedSize == 0 {
|
||||
// update artifact size if zero or not match, over write artifact size
|
||||
if artifact.FileSize == 0 ||
|
||||
artifact.FileCompressedSize == 0 ||
|
||||
artifact.FileSize != fileRealTotalSize ||
|
||||
artifact.FileCompressedSize != chunksTotalSize {
|
||||
artifact.FileSize = fileRealTotalSize
|
||||
artifact.FileCompressedSize = chunksTotalSize
|
||||
artifact.ContentEncoding = ctx.Req.Header.Get("Content-Encoding")
|
||||
@@ -267,6 +293,8 @@ func (ar artifactRoutes) uploadArtifact(ctx *ArtifactContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "Error update artifact")
|
||||
return
|
||||
}
|
||||
log.Debug("[artifact] update artifact size, artifact_id: %d, size: %d, compressed size: %d",
|
||||
artifact.ID, artifact.FileSize, artifact.FileCompressedSize)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, map[string]string{
|
||||
@@ -314,7 +342,7 @@ func (ar artifactRoutes) listArtifacts(ctx *ArtifactContext) {
|
||||
return
|
||||
}
|
||||
|
||||
artifacts, err := actions.ListArtifactsByRunID(ctx, runID)
|
||||
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID})
|
||||
if err != nil {
|
||||
log.Error("Error getting artifacts: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
@@ -376,7 +404,10 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
|
||||
return
|
||||
}
|
||||
|
||||
artifacts, err := actions.ListArtifactsByRunIDAndArtifactName(ctx, runID, itemPath)
|
||||
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
|
||||
RunID: runID,
|
||||
ArtifactName: itemPath,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("Error getting artifacts: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
@@ -396,7 +427,19 @@ func (ar artifactRoutes) getDownloadArtifactURL(ctx *ArtifactContext) {
|
||||
|
||||
var items []downloadArtifactResponseItem
|
||||
for _, artifact := range artifacts {
|
||||
downloadURL := ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
|
||||
var downloadURL string
|
||||
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
|
||||
u, err := ar.fs.URL(artifact.StoragePath, artifact.ArtifactName)
|
||||
if err != nil && !errors.Is(err, storage.ErrURLNotSupported) {
|
||||
log.Error("Error getting serve direct url: %v", err)
|
||||
}
|
||||
if u != nil {
|
||||
downloadURL = u.String()
|
||||
}
|
||||
}
|
||||
if downloadURL == "" {
|
||||
downloadURL = ar.buildArtifactURL(runID, strconv.FormatInt(artifact.ID, 10), "download")
|
||||
}
|
||||
item := downloadArtifactResponseItem{
|
||||
Path: util.PathJoinRel(itemPath, artifact.ArtifactPath),
|
||||
ItemType: "file",
|
||||
@@ -419,15 +462,15 @@ func (ar artifactRoutes) downloadArtifact(ctx *ArtifactContext) {
|
||||
}
|
||||
|
||||
artifactID := ctx.ParamsInt64("artifact_id")
|
||||
artifact, err := actions.GetArtifactByID(ctx, artifactID)
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
log.Error("Error getting artifact: %v", err)
|
||||
ctx.Error(http.StatusNotFound, err.Error())
|
||||
return
|
||||
} else if err != nil {
|
||||
artifact, exist, err := db.GetByID[actions.ActionArtifact](ctx, artifactID)
|
||||
if err != nil {
|
||||
log.Error("Error getting artifact: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
} else if !exist {
|
||||
log.Error("artifact with ID %d does not exist", artifactID)
|
||||
ctx.Error(http.StatusNotFound, fmt.Sprintf("artifact with ID %d does not exist", artifactID))
|
||||
return
|
||||
}
|
||||
if artifact.RunID != runID {
|
||||
log.Error("Error dismatch runID and artifactID, task: %v, artifact: %v", runID, artifactID)
|
||||
|
||||
@@ -5,18 +5,70 @@ package actions
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
)
|
||||
|
||||
func saveUploadChunkBase(st storage.ObjectStorage, ctx *ArtifactContext,
|
||||
artifact *actions.ActionArtifact,
|
||||
contentSize, runID, start, end, length int64, checkMd5 bool,
|
||||
) (int64, error) {
|
||||
// build chunk store path
|
||||
storagePath := fmt.Sprintf("tmp%d/%d-%d-%d-%d.chunk", runID, runID, artifact.ID, start, end)
|
||||
var r io.Reader = ctx.Req.Body
|
||||
var hasher hash.Hash
|
||||
if checkMd5 {
|
||||
// use io.TeeReader to avoid reading all body to md5 sum.
|
||||
// it writes data to hasher after reading end
|
||||
// if hash is not matched, delete the read-end result
|
||||
hasher = md5.New()
|
||||
r = io.TeeReader(r, hasher)
|
||||
}
|
||||
// save chunk to storage
|
||||
writtenSize, err := st.Save(storagePath, r, -1)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("save chunk to storage error: %v", err)
|
||||
}
|
||||
var checkErr error
|
||||
if checkMd5 {
|
||||
// check md5
|
||||
reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
|
||||
chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
|
||||
log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
|
||||
// if md5 not match, delete the chunk
|
||||
if reqMd5String != chunkMd5String {
|
||||
checkErr = fmt.Errorf("md5 not match")
|
||||
}
|
||||
}
|
||||
if writtenSize != contentSize {
|
||||
checkErr = errors.Join(checkErr, fmt.Errorf("contentSize not match body size"))
|
||||
}
|
||||
if checkErr != nil {
|
||||
if err := st.Delete(storagePath); err != nil {
|
||||
log.Error("Error deleting chunk: %s, %v", storagePath, err)
|
||||
}
|
||||
return -1, checkErr
|
||||
}
|
||||
log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
|
||||
storagePath, contentSize, artifact.ID, start, end)
|
||||
// return chunk total size
|
||||
return length, nil
|
||||
}
|
||||
|
||||
func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
|
||||
artifact *actions.ActionArtifact,
|
||||
contentSize, runID int64,
|
||||
@@ -25,38 +77,22 @@ func saveUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
|
||||
contentRange := ctx.Req.Header.Get("Content-Range")
|
||||
start, end, length := int64(0), int64(0), int64(0)
|
||||
if _, err := fmt.Sscanf(contentRange, "bytes %d-%d/%d", &start, &end, &length); err != nil {
|
||||
log.Warn("parse content range error: %v, content-range: %s", err, contentRange)
|
||||
return -1, fmt.Errorf("parse content range error: %v", err)
|
||||
}
|
||||
// build chunk store path
|
||||
storagePath := fmt.Sprintf("tmp%d/%d-%d-%d.chunk", runID, artifact.ID, start, end)
|
||||
// use io.TeeReader to avoid reading all body to md5 sum.
|
||||
// it writes data to hasher after reading end
|
||||
// if hash is not matched, delete the read-end result
|
||||
hasher := md5.New()
|
||||
r := io.TeeReader(ctx.Req.Body, hasher)
|
||||
// save chunk to storage
|
||||
writtenSize, err := st.Save(storagePath, r, -1)
|
||||
if err != nil {
|
||||
return -1, fmt.Errorf("save chunk to storage error: %v", err)
|
||||
}
|
||||
// check md5
|
||||
reqMd5String := ctx.Req.Header.Get(artifactXActionsResultsMD5Header)
|
||||
chunkMd5String := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
|
||||
log.Info("[artifact] check chunk md5, sum: %s, header: %s", chunkMd5String, reqMd5String)
|
||||
// if md5 not match, delete the chunk
|
||||
if reqMd5String != chunkMd5String || writtenSize != contentSize {
|
||||
if err := st.Delete(storagePath); err != nil {
|
||||
log.Error("Error deleting chunk: %s, %v", storagePath, err)
|
||||
}
|
||||
return -1, fmt.Errorf("md5 not match")
|
||||
}
|
||||
log.Info("[artifact] save chunk %s, size: %d, artifact id: %d, start: %d, end: %d",
|
||||
storagePath, contentSize, artifact.ID, start, end)
|
||||
// return chunk total size
|
||||
return length, nil
|
||||
return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, length, true)
|
||||
}
|
||||
|
||||
func appendUploadChunk(st storage.ObjectStorage, ctx *ArtifactContext,
|
||||
artifact *actions.ActionArtifact,
|
||||
start, contentSize, runID int64,
|
||||
) (int64, error) {
|
||||
end := start + contentSize - 1
|
||||
return saveUploadChunkBase(st, ctx, artifact, contentSize, runID, start, end, contentSize, false)
|
||||
}
|
||||
|
||||
type chunkFileItem struct {
|
||||
RunID int64
|
||||
ArtifactID int64
|
||||
Start int64
|
||||
End int64
|
||||
@@ -66,9 +102,12 @@ type chunkFileItem struct {
|
||||
func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chunkFileItem, error) {
|
||||
storageDir := fmt.Sprintf("tmp%d", runID)
|
||||
var chunks []*chunkFileItem
|
||||
if err := st.IterateObjects(storageDir, func(path string, obj storage.Object) error {
|
||||
item := chunkFileItem{Path: path}
|
||||
if _, err := fmt.Sscanf(path, filepath.Join(storageDir, "%d-%d-%d.chunk"), &item.ArtifactID, &item.Start, &item.End); err != nil {
|
||||
if err := st.IterateObjects(storageDir, func(fpath string, obj storage.Object) error {
|
||||
baseName := filepath.Base(fpath)
|
||||
// when read chunks from storage, it only contains storage dir and basename,
|
||||
// no matter the subdirectory setting in storage config
|
||||
item := chunkFileItem{Path: storageDir + "/" + baseName}
|
||||
if _, err := fmt.Sscanf(baseName, "%d-%d-%d-%d.chunk", &item.RunID, &item.ArtifactID, &item.Start, &item.End); err != nil {
|
||||
return fmt.Errorf("parse content range error: %v", err)
|
||||
}
|
||||
chunks = append(chunks, &item)
|
||||
@@ -86,7 +125,10 @@ func listChunksByRunID(st storage.ObjectStorage, runID int64) (map[int64][]*chun
|
||||
|
||||
func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int64, artifactName string) error {
|
||||
// read all db artifacts by name
|
||||
artifacts, err := actions.ListArtifactsByRunIDAndName(ctx, runID, artifactName)
|
||||
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{
|
||||
RunID: runID,
|
||||
ArtifactName: artifactName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -102,14 +144,14 @@ func mergeChunksForRun(ctx *ArtifactContext, st storage.ObjectStorage, runID int
|
||||
log.Debug("artifact %d chunks not found", art.ID)
|
||||
continue
|
||||
}
|
||||
if err := mergeChunksForArtifact(ctx, chunks, st, art); err != nil {
|
||||
if err := mergeChunksForArtifact(ctx, chunks, st, art, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact) error {
|
||||
func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st storage.ObjectStorage, artifact *actions.ActionArtifact, checksum string) error {
|
||||
sort.Slice(chunks, func(i, j int) bool {
|
||||
return chunks[i].Start < chunks[j].Start
|
||||
})
|
||||
@@ -148,6 +190,14 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
|
||||
readers = append(readers, readCloser)
|
||||
}
|
||||
mergedReader := io.MultiReader(readers...)
|
||||
shaPrefix := "sha256:"
|
||||
var hash hash.Hash
|
||||
if strings.HasPrefix(checksum, shaPrefix) {
|
||||
hash = sha256.New()
|
||||
}
|
||||
if hash != nil {
|
||||
mergedReader = io.TeeReader(mergedReader, hash)
|
||||
}
|
||||
|
||||
// if chunk is gzip, use gz as extension
|
||||
// download-artifact action will use content-encoding header to decide if it should decompress the file
|
||||
@@ -176,8 +226,23 @@ func mergeChunksForArtifact(ctx *ArtifactContext, chunks []*chunkFileItem, st st
|
||||
}
|
||||
}()
|
||||
|
||||
if hash != nil {
|
||||
rawChecksum := hash.Sum(nil)
|
||||
actualChecksum := hex.EncodeToString(rawChecksum)
|
||||
if !strings.HasSuffix(checksum, actualChecksum) {
|
||||
return fmt.Errorf("update artifact error checksum is invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// save storage path to artifact
|
||||
log.Debug("[artifact] merge chunks to artifact: %d, %s", artifact.ID, storagePath)
|
||||
log.Debug("[artifact] merge chunks to artifact: %d, %s, old:%s", artifact.ID, storagePath, artifact.StoragePath)
|
||||
// if artifact is already uploaded, delete the old file
|
||||
if artifact.StoragePath != "" {
|
||||
if err := st.Delete(artifact.StoragePath); err != nil {
|
||||
log.Warn("Error deleting old artifact: %s, %v", artifact.StoragePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
artifact.StoragePath = storagePath
|
||||
artifact.Status = int64(actions.ArtifactStatusUploadConfirmed)
|
||||
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
|
||||
|
||||
@@ -43,6 +43,17 @@ func validateRunID(ctx *ArtifactContext) (*actions.ActionTask, int64, bool) {
|
||||
return task, runID, true
|
||||
}
|
||||
|
||||
func validateRunIDV4(ctx *ArtifactContext, rawRunID string) (*actions.ActionTask, int64, bool) {
|
||||
task := ctx.ActionTask
|
||||
runID, err := strconv.ParseInt(rawRunID, 10, 64)
|
||||
if err != nil || task.Job.RunID != runID {
|
||||
log.Error("Error runID not match")
|
||||
ctx.Error(http.StatusBadRequest, "run-id does not match")
|
||||
return nil, 0, false
|
||||
}
|
||||
return task, runID, true
|
||||
}
|
||||
|
||||
func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
|
||||
paramHash := ctx.Params("artifact_hash")
|
||||
// use artifact name to create upload url
|
||||
@@ -58,7 +69,8 @@ func validateArtifactHash(ctx *ArtifactContext, artifactName string) bool {
|
||||
func parseArtifactItemPath(ctx *ArtifactContext) (string, string, bool) {
|
||||
// itemPath is generated from upload-artifact action
|
||||
// it's formatted as {artifact_name}/{artfict_path_in_runner}
|
||||
itemPath := util.PathJoinRel(ctx.Req.URL.Query().Get("itemPath"))
|
||||
// act_runner in host mode on Windows, itemPath is joined by Windows slash '\'
|
||||
itemPath := util.PathJoinRelX(ctx.Req.URL.Query().Get("itemPath"))
|
||||
artifactName := strings.Split(itemPath, "/")[0]
|
||||
artifactPath := strings.TrimPrefix(itemPath, artifactName+"/")
|
||||
if !validateArtifactHash(ctx, artifactName) {
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package actions
|
||||
|
||||
// GitHub Actions Artifacts V4 API Simple Description
|
||||
//
|
||||
// 1. Upload artifact
|
||||
// 1.1. CreateArtifact
|
||||
// Post: /twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact
|
||||
// Request:
|
||||
// {
|
||||
// "workflow_run_backend_id": "21",
|
||||
// "workflow_job_run_backend_id": "49",
|
||||
// "name": "test",
|
||||
// "version": 4
|
||||
// }
|
||||
// Response:
|
||||
// {
|
||||
// "ok": true,
|
||||
// "signedUploadUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75"
|
||||
// }
|
||||
// 1.2. Upload Zip Content to Blobstorage (unauthenticated request)
|
||||
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=block
|
||||
// 1.3. Continue Upload Zip Content to Blobstorage (unauthenticated request), repeat until everything is uploaded
|
||||
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=appendBlock
|
||||
// 1.4. Unknown xml payload to Blobstorage (unauthenticated request), ignored for now
|
||||
// PUT: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/UploadArtifact?sig=mO7y35r4GyjN7fwg0DTv3-Fv1NDXD84KLEgLpoPOtDI=&expires=2024-01-23+21%3A48%3A37.20833956+%2B0100+CET&artifactName=test&taskID=75&comp=blockList
|
||||
// 1.5. FinalizeArtifact
|
||||
// Post: /twirp/github.actions.results.api.v1.ArtifactService/FinalizeArtifact
|
||||
// Request
|
||||
// {
|
||||
// "workflow_run_backend_id": "21",
|
||||
// "workflow_job_run_backend_id": "49",
|
||||
// "name": "test",
|
||||
// "size": "2097",
|
||||
// "hash": "sha256:b6325614d5649338b87215d9536b3c0477729b8638994c74cdefacb020a2cad4"
|
||||
// }
|
||||
// Response
|
||||
// {
|
||||
// "ok": true,
|
||||
// "artifactId": "4"
|
||||
// }
|
||||
// 2. Download artifact
|
||||
// 2.1. ListArtifacts and optionally filter by artifact exact name or id
|
||||
// Post: /twirp/github.actions.results.api.v1.ArtifactService/ListArtifacts
|
||||
// Request
|
||||
// {
|
||||
// "workflow_run_backend_id": "21",
|
||||
// "workflow_job_run_backend_id": "49",
|
||||
// "name_filter": "test"
|
||||
// }
|
||||
// Response
|
||||
// {
|
||||
// "artifacts": [
|
||||
// {
|
||||
// "workflowRunBackendId": "21",
|
||||
// "workflowJobRunBackendId": "49",
|
||||
// "databaseId": "4",
|
||||
// "name": "test",
|
||||
// "size": "2093",
|
||||
// "createdAt": "2024-01-23T00:13:28Z"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
// 2.2. GetSignedArtifactURL get the URL to download the artifact zip file of a specific artifact
|
||||
// Post: /twirp/github.actions.results.api.v1.ArtifactService/GetSignedArtifactURL
|
||||
// Request
|
||||
// {
|
||||
// "workflow_run_backend_id": "21",
|
||||
// "workflow_job_run_backend_id": "49",
|
||||
// "name": "test"
|
||||
// }
|
||||
// Response
|
||||
// {
|
||||
// "signedUrl": "http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76"
|
||||
// }
|
||||
// 2.3. Download Zip from Blobstorage (unauthenticated request)
|
||||
// GET: http://localhost:3000/twirp/github.actions.results.api.v1.ArtifactService/DownloadArtifact?sig=wHzFOwpF-6220-5CA0CIRmAX9VbiTC2Mji89UOqo1E8=&expires=2024-01-23+21%3A51%3A56.872846295+%2B0100+CET&artifactName=test&taskID=76
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
const (
|
||||
ArtifactV4RouteBase = "/twirp/github.actions.results.api.v1.ArtifactService"
|
||||
ArtifactV4ContentEncoding = "application/zip"
|
||||
)
|
||||
|
||||
type artifactV4Routes struct {
|
||||
prefix string
|
||||
fs storage.ObjectStorage
|
||||
}
|
||||
|
||||
func ArtifactV4Contexter() func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
base, baseCleanUp := context.NewBaseContext(resp, req)
|
||||
defer baseCleanUp()
|
||||
|
||||
ctx := &ArtifactContext{Base: base}
|
||||
ctx.AppendContextValue(artifactContextKey, ctx)
|
||||
|
||||
next.ServeHTTP(ctx.Resp, ctx.Req)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ArtifactsV4Routes(prefix string) *web.Route {
|
||||
m := web.NewRoute()
|
||||
|
||||
r := artifactV4Routes{
|
||||
prefix: prefix,
|
||||
fs: storage.ActionsArtifacts,
|
||||
}
|
||||
|
||||
m.Group("", func() {
|
||||
m.Post("CreateArtifact", r.createArtifact)
|
||||
m.Post("FinalizeArtifact", r.finalizeArtifact)
|
||||
m.Post("ListArtifacts", r.listArtifacts)
|
||||
m.Post("GetSignedArtifactURL", r.getSignedArtifactURL)
|
||||
m.Post("DeleteArtifact", r.deleteArtifact)
|
||||
}, ArtifactContexter())
|
||||
m.Group("", func() {
|
||||
m.Put("UploadArtifact", r.uploadArtifact)
|
||||
m.Get("DownloadArtifact", r.downloadArtifact)
|
||||
}, ArtifactV4Contexter())
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (r artifactV4Routes) buildSignature(endp, expires, artifactName string, taskID int64) []byte {
|
||||
mac := hmac.New(sha256.New, setting.GetGeneralTokenSigningSecret())
|
||||
mac.Write([]byte(endp))
|
||||
mac.Write([]byte(expires))
|
||||
mac.Write([]byte(artifactName))
|
||||
mac.Write([]byte(fmt.Sprint(taskID)))
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func (r artifactV4Routes) buildArtifactURL(endp, artifactName string, taskID int64) string {
|
||||
expires := time.Now().Add(60 * time.Minute).Format("2006-01-02 15:04:05.999999999 -0700 MST")
|
||||
uploadURL := strings.TrimSuffix(setting.AppURL, "/") + strings.TrimSuffix(r.prefix, "/") +
|
||||
"/" + endp + "?sig=" + base64.URLEncoding.EncodeToString(r.buildSignature(endp, expires, artifactName, taskID)) + "&expires=" + url.QueryEscape(expires) + "&artifactName=" + url.QueryEscape(artifactName) + "&taskID=" + fmt.Sprint(taskID)
|
||||
return uploadURL
|
||||
}
|
||||
|
||||
func (r artifactV4Routes) verifySignature(ctx *ArtifactContext, endp string) (*actions.ActionTask, string, bool) {
|
||||
rawTaskID := ctx.Req.URL.Query().Get("taskID")
|
||||
sig := ctx.Req.URL.Query().Get("sig")
|
||||
expires := ctx.Req.URL.Query().Get("expires")
|
||||
artifactName := ctx.Req.URL.Query().Get("artifactName")
|
||||
dsig, _ := base64.URLEncoding.DecodeString(sig)
|
||||
taskID, _ := strconv.ParseInt(rawTaskID, 10, 64)
|
||||
|
||||
expecedsig := r.buildSignature(endp, expires, artifactName, taskID)
|
||||
if !hmac.Equal(dsig, expecedsig) {
|
||||
log.Error("Error unauthorized")
|
||||
ctx.Error(http.StatusUnauthorized, "Error unauthorized")
|
||||
return nil, "", false
|
||||
}
|
||||
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", expires)
|
||||
if err != nil || t.Before(time.Now()) {
|
||||
log.Error("Error link expired")
|
||||
ctx.Error(http.StatusUnauthorized, "Error link expired")
|
||||
return nil, "", false
|
||||
}
|
||||
task, err := actions.GetTaskByID(ctx, taskID)
|
||||
if err != nil {
|
||||
log.Error("Error runner api getting task by ID: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting task by ID")
|
||||
return nil, "", false
|
||||
}
|
||||
if task.Status != actions.StatusRunning {
|
||||
log.Error("Error runner api getting task: task is not running")
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
|
||||
return nil, "", false
|
||||
}
|
||||
if err := task.LoadJob(ctx); err != nil {
|
||||
log.Error("Error runner api getting job: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting job")
|
||||
return nil, "", false
|
||||
}
|
||||
return task, artifactName, true
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) getArtifactByName(ctx *ArtifactContext, runID int64, name string) (*actions.ActionArtifact, error) {
|
||||
var art actions.ActionArtifact
|
||||
has, err := db.GetEngine(ctx).Where("run_id = ? AND artifact_name = ? AND artifact_path = ? AND content_encoding = ?", runID, name, name+".zip", ArtifactV4ContentEncoding).Get(&art)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, util.ErrNotExist
|
||||
}
|
||||
return &art, nil
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) parseProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) bool {
|
||||
body, err := io.ReadAll(ctx.Req.Body)
|
||||
if err != nil {
|
||||
log.Error("Error decode request body: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error decode request body")
|
||||
return false
|
||||
}
|
||||
err = protojson.Unmarshal(body, req)
|
||||
if err != nil {
|
||||
log.Error("Error decode request body: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error decode request body")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) sendProtbufBody(ctx *ArtifactContext, req protoreflect.ProtoMessage) {
|
||||
resp, err := protojson.Marshal(req)
|
||||
if err != nil {
|
||||
log.Error("Error encode response body: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error encode response body")
|
||||
return
|
||||
}
|
||||
ctx.Resp.Header().Set("Content-Type", "application/json;charset=utf-8")
|
||||
ctx.Resp.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Resp.Write(resp)
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) createArtifact(ctx *ArtifactContext) {
|
||||
var req CreateArtifactRequest
|
||||
|
||||
if ok := r.parseProtbufBody(ctx, &req); !ok {
|
||||
return
|
||||
}
|
||||
_, _, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
artifactName := req.Name
|
||||
|
||||
rententionDays := setting.Actions.ArtifactRetentionDays
|
||||
if req.ExpiresAt != nil {
|
||||
rententionDays = int64(time.Until(req.ExpiresAt.AsTime()).Hours() / 24)
|
||||
}
|
||||
// create or get artifact with name and path
|
||||
artifact, err := actions.CreateArtifact(ctx, ctx.ActionTask, artifactName, artifactName+".zip", rententionDays)
|
||||
if err != nil {
|
||||
log.Error("Error create or get artifact: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error create or get artifact")
|
||||
return
|
||||
}
|
||||
artifact.ContentEncoding = ArtifactV4ContentEncoding
|
||||
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
|
||||
log.Error("Error UpdateArtifactByID: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
|
||||
return
|
||||
}
|
||||
|
||||
respData := CreateArtifactResponse{
|
||||
Ok: true,
|
||||
SignedUploadUrl: r.buildArtifactURL("UploadArtifact", artifactName, ctx.ActionTask.ID),
|
||||
}
|
||||
r.sendProtbufBody(ctx, &respData)
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) uploadArtifact(ctx *ArtifactContext) {
|
||||
task, artifactName, ok := r.verifySignature(ctx, "UploadArtifact")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
comp := ctx.Req.URL.Query().Get("comp")
|
||||
switch comp {
|
||||
case "block", "appendBlock":
|
||||
// get artifact by name
|
||||
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
|
||||
if err != nil {
|
||||
log.Error("Error artifact not found: %v", err)
|
||||
ctx.Error(http.StatusNotFound, "Error artifact not found")
|
||||
return
|
||||
}
|
||||
|
||||
if comp == "block" {
|
||||
artifact.FileSize = 0
|
||||
artifact.FileCompressedSize = 0
|
||||
}
|
||||
|
||||
_, err = appendUploadChunk(r.fs, ctx, artifact, artifact.FileSize, ctx.Req.ContentLength, artifact.RunID)
|
||||
if err != nil {
|
||||
log.Error("Error runner api getting task: task is not running")
|
||||
ctx.Error(http.StatusInternalServerError, "Error runner api getting task: task is not running")
|
||||
return
|
||||
}
|
||||
artifact.FileCompressedSize += ctx.Req.ContentLength
|
||||
artifact.FileSize += ctx.Req.ContentLength
|
||||
if err := actions.UpdateArtifactByID(ctx, artifact.ID, artifact); err != nil {
|
||||
log.Error("Error UpdateArtifactByID: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error UpdateArtifactByID")
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusCreated, "appended")
|
||||
case "blocklist":
|
||||
ctx.JSON(http.StatusCreated, "created")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) finalizeArtifact(ctx *ArtifactContext) {
|
||||
var req FinalizeArtifactRequest
|
||||
|
||||
if ok := r.parseProtbufBody(ctx, &req); !ok {
|
||||
return
|
||||
}
|
||||
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get artifact by name
|
||||
artifact, err := r.getArtifactByName(ctx, runID, req.Name)
|
||||
if err != nil {
|
||||
log.Error("Error artifact not found: %v", err)
|
||||
ctx.Error(http.StatusNotFound, "Error artifact not found")
|
||||
return
|
||||
}
|
||||
chunkMap, err := listChunksByRunID(r.fs, runID)
|
||||
if err != nil {
|
||||
log.Error("Error merge chunks: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error merge chunks")
|
||||
return
|
||||
}
|
||||
chunks, ok := chunkMap[artifact.ID]
|
||||
if !ok {
|
||||
log.Error("Error merge chunks")
|
||||
ctx.Error(http.StatusInternalServerError, "Error merge chunks")
|
||||
return
|
||||
}
|
||||
checksum := ""
|
||||
if req.Hash != nil {
|
||||
checksum = req.Hash.Value
|
||||
}
|
||||
if err := mergeChunksForArtifact(ctx, chunks, r.fs, artifact, checksum); err != nil {
|
||||
log.Error("Error merge chunks: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, "Error merge chunks")
|
||||
return
|
||||
}
|
||||
|
||||
respData := FinalizeArtifactResponse{
|
||||
Ok: true,
|
||||
ArtifactId: artifact.ID,
|
||||
}
|
||||
r.sendProtbufBody(ctx, &respData)
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) listArtifacts(ctx *ArtifactContext) {
|
||||
var req ListArtifactsRequest
|
||||
|
||||
if ok := r.parseProtbufBody(ctx, &req); !ok {
|
||||
return
|
||||
}
|
||||
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
artifacts, err := db.Find[actions.ActionArtifact](ctx, actions.FindArtifactsOptions{RunID: runID})
|
||||
if err != nil {
|
||||
log.Error("Error getting artifacts: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if len(artifacts) == 0 {
|
||||
log.Debug("[artifact] handleListArtifacts, no artifacts")
|
||||
ctx.Error(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
list := []*ListArtifactsResponse_MonolithArtifact{}
|
||||
|
||||
table := map[string]*ListArtifactsResponse_MonolithArtifact{}
|
||||
for _, artifact := range artifacts {
|
||||
if _, ok := table[artifact.ArtifactName]; ok || req.IdFilter != nil && artifact.ID != req.IdFilter.Value || req.NameFilter != nil && artifact.ArtifactName != req.NameFilter.Value || artifact.ArtifactName+".zip" != artifact.ArtifactPath || artifact.ContentEncoding != ArtifactV4ContentEncoding {
|
||||
table[artifact.ArtifactName] = nil
|
||||
continue
|
||||
}
|
||||
|
||||
table[artifact.ArtifactName] = &ListArtifactsResponse_MonolithArtifact{
|
||||
Name: artifact.ArtifactName,
|
||||
CreatedAt: timestamppb.New(artifact.CreatedUnix.AsTime()),
|
||||
DatabaseId: artifact.ID,
|
||||
WorkflowRunBackendId: req.WorkflowRunBackendId,
|
||||
WorkflowJobRunBackendId: req.WorkflowJobRunBackendId,
|
||||
Size: artifact.FileSize,
|
||||
}
|
||||
}
|
||||
for _, artifact := range table {
|
||||
if artifact != nil {
|
||||
list = append(list, artifact)
|
||||
}
|
||||
}
|
||||
|
||||
respData := ListArtifactsResponse{
|
||||
Artifacts: list,
|
||||
}
|
||||
r.sendProtbufBody(ctx, &respData)
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) getSignedArtifactURL(ctx *ArtifactContext) {
|
||||
var req GetSignedArtifactURLRequest
|
||||
|
||||
if ok := r.parseProtbufBody(ctx, &req); !ok {
|
||||
return
|
||||
}
|
||||
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
artifactName := req.Name
|
||||
|
||||
// get artifact by name
|
||||
artifact, err := r.getArtifactByName(ctx, runID, artifactName)
|
||||
if err != nil {
|
||||
log.Error("Error artifact not found: %v", err)
|
||||
ctx.Error(http.StatusNotFound, "Error artifact not found")
|
||||
return
|
||||
}
|
||||
|
||||
respData := GetSignedArtifactURLResponse{}
|
||||
|
||||
if setting.Actions.ArtifactStorage.MinioConfig.ServeDirect {
|
||||
u, err := storage.ActionsArtifacts.URL(artifact.StoragePath, artifact.ArtifactPath)
|
||||
if u != nil && err == nil {
|
||||
respData.SignedUrl = u.String()
|
||||
}
|
||||
}
|
||||
if respData.SignedUrl == "" {
|
||||
respData.SignedUrl = r.buildArtifactURL("DownloadArtifact", artifactName, ctx.ActionTask.ID)
|
||||
}
|
||||
r.sendProtbufBody(ctx, &respData)
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) downloadArtifact(ctx *ArtifactContext) {
|
||||
task, artifactName, ok := r.verifySignature(ctx, "DownloadArtifact")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get artifact by name
|
||||
artifact, err := r.getArtifactByName(ctx, task.Job.RunID, artifactName)
|
||||
if err != nil {
|
||||
log.Error("Error artifact not found: %v", err)
|
||||
ctx.Error(http.StatusNotFound, "Error artifact not found")
|
||||
return
|
||||
}
|
||||
|
||||
file, _ := r.fs.Open(artifact.StoragePath)
|
||||
|
||||
_, _ = io.Copy(ctx.Resp, file)
|
||||
}
|
||||
|
||||
func (r *artifactV4Routes) deleteArtifact(ctx *ArtifactContext) {
|
||||
var req DeleteArtifactRequest
|
||||
|
||||
if ok := r.parseProtbufBody(ctx, &req); !ok {
|
||||
return
|
||||
}
|
||||
_, runID, ok := validateRunIDV4(ctx, req.WorkflowRunBackendId)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// get artifact by name
|
||||
artifact, err := r.getArtifactByName(ctx, runID, req.Name)
|
||||
if err != nil {
|
||||
log.Error("Error artifact not found: %v", err)
|
||||
ctx.Error(http.StatusNotFound, "Error artifact not found")
|
||||
return
|
||||
}
|
||||
|
||||
err = actions.SetArtifactNeedDelete(ctx, runID, req.Name)
|
||||
if err != nil {
|
||||
log.Error("Error deleting artifacts: %v", err)
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
respData := DeleteArtifactResponse{
|
||||
Ok: true,
|
||||
ArtifactId: artifact.ID,
|
||||
}
|
||||
r.sendProtbufBody(ctx, &respData)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
)
|
||||
|
||||
func NewPingServiceHandler() (string, http.Handler) {
|
||||
@@ -21,9 +21,7 @@ func NewPingServiceHandler() (string, http.Handler) {
|
||||
|
||||
var _ pingv1connect.PingServiceHandler = (*Service)(nil)
|
||||
|
||||
type Service struct {
|
||||
pingv1connect.UnimplementedPingServiceHandler
|
||||
}
|
||||
type Service struct{}
|
||||
|
||||
func (s *Service) Ping(
|
||||
ctx context.Context,
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
pingv1 "code.gitea.io/actions-proto-go/ping/v1"
|
||||
"code.gitea.io/actions-proto-go/ping/v1/pingv1connect"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
runnerv1 "code.gitea.io/actions-proto-go/runner/v1"
|
||||
"code.gitea.io/actions-proto-go/runner/v1/runnerv1connect"
|
||||
"github.com/bufbuild/connect-go"
|
||||
"connectrpc.com/connect"
|
||||
gouuid "github.com/google/uuid"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
@@ -32,9 +32,7 @@ func NewRunnerServiceHandler() (string, http.Handler) {
|
||||
|
||||
var _ runnerv1connect.RunnerServiceClient = (*Service)(nil)
|
||||
|
||||
type Service struct {
|
||||
runnerv1connect.UnimplementedRunnerServiceHandler
|
||||
}
|
||||
type Service struct{}
|
||||
|
||||
// Register for new runner.
|
||||
func (s *Service) Register(
|
||||
|
||||
@@ -8,13 +8,13 @@ import (
|
||||
"fmt"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
secret_model "code.gitea.io/gitea/models/secret"
|
||||
actions_module "code.gitea.io/gitea/modules/actions"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
secret_module "code.gitea.io/gitea/modules/secret"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/actions"
|
||||
|
||||
@@ -31,14 +31,24 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
secrets, err := secret_model.GetSecretsOfTask(ctx, t)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("GetSecretsOfTask: %w", err)
|
||||
}
|
||||
|
||||
vars, err := actions_model.GetVariablesOfRun(ctx, t.Job.Run)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("GetVariablesOfRun: %w", err)
|
||||
}
|
||||
|
||||
actions.CreateCommitStatus(ctx, t.Job)
|
||||
|
||||
task := &runnerv1.Task{
|
||||
Id: t.ID,
|
||||
WorkflowPayload: t.Job.WorkflowPayload,
|
||||
Context: generateTaskContext(t),
|
||||
Secrets: getSecretsOfTask(ctx, t),
|
||||
Vars: getVariablesOfTask(ctx, t),
|
||||
Secrets: secrets,
|
||||
Vars: vars,
|
||||
}
|
||||
|
||||
if needs, err := findTaskNeeds(ctx, t); err != nil {
|
||||
@@ -54,65 +64,6 @@ func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv
|
||||
return task, true, nil
|
||||
}
|
||||
|
||||
func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
|
||||
secrets := map[string]string{}
|
||||
|
||||
secrets["GITHUB_TOKEN"] = task.Token
|
||||
secrets["GITEA_TOKEN"] = task.Token
|
||||
|
||||
if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget {
|
||||
// ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated.
|
||||
// for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch
|
||||
// see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target
|
||||
return secrets
|
||||
}
|
||||
|
||||
ownerSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID})
|
||||
if err != nil {
|
||||
log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err)
|
||||
// go on
|
||||
}
|
||||
repoSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID})
|
||||
if err != nil {
|
||||
log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err)
|
||||
// go on
|
||||
}
|
||||
|
||||
for _, secret := range append(ownerSecrets, repoSecrets...) {
|
||||
if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil {
|
||||
log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err)
|
||||
// go on
|
||||
} else {
|
||||
secrets[secret.Name] = v
|
||||
}
|
||||
}
|
||||
|
||||
return secrets
|
||||
}
|
||||
|
||||
func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string {
|
||||
variables := map[string]string{}
|
||||
|
||||
// Org / User level
|
||||
ownerVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID})
|
||||
if err != nil {
|
||||
log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err)
|
||||
}
|
||||
|
||||
// Repo level
|
||||
repoVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID})
|
||||
if err != nil {
|
||||
log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err)
|
||||
}
|
||||
|
||||
// Level precedence: Repo > Org / User
|
||||
for _, v := range append(ownerVariables, repoVariables...) {
|
||||
variables[v.Name] = v.Data
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
|
||||
func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
|
||||
event := map[string]any{}
|
||||
_ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event)
|
||||
@@ -144,6 +95,11 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
|
||||
|
||||
refName := git.RefName(ref)
|
||||
|
||||
giteaRuntimeToken, err := actions.CreateAuthorizationToken(t.ID, t.Job.RunID, t.JobID)
|
||||
if err != nil {
|
||||
log.Error("actions.CreateAuthorizationToken failed: %v", err)
|
||||
}
|
||||
|
||||
taskContext, err := structpb.NewStruct(map[string]any{
|
||||
// standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
|
||||
"action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2.
|
||||
@@ -183,6 +139,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct {
|
||||
|
||||
// additional contexts
|
||||
"gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(),
|
||||
"gitea_runtime_token": giteaRuntimeToken,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error("structpb.NewStruct failed: %v", err)
|
||||
@@ -200,7 +157,7 @@ func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[str
|
||||
}
|
||||
needs := container.SetOf(task.Job.Needs...)
|
||||
|
||||
jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID})
|
||||
jobs, err := db.Find[actions_model.ActionRunJob](ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FindRunJobs: %w", err)
|
||||
}
|
||||
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
alpine_module "code.gitea.io/gitea/modules/packages/alpine"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
alpine_service "code.gitea.io/gitea/services/packages/alpine"
|
||||
)
|
||||
@@ -72,7 +72,7 @@ func GetRepositoryFile(ctx *context.Context) {
|
||||
ctx,
|
||||
pv,
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: alpine_service.IndexFilename,
|
||||
Filename: alpine_service.IndexArchiveFilename,
|
||||
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
|
||||
},
|
||||
)
|
||||
@@ -182,19 +182,38 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, &packages_model.PackageFileSearchOptions{
|
||||
branch := ctx.Params("branch")
|
||||
repository := ctx.Params("repository")
|
||||
architecture := ctx.Params("architecture")
|
||||
|
||||
opts := &packages_model.PackageFileSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
PackageType: packages_model.TypeAlpine,
|
||||
Query: ctx.Params("filename"),
|
||||
CompositeKey: fmt.Sprintf("%s|%s|%s", ctx.Params("branch"), ctx.Params("repository"), ctx.Params("architecture")),
|
||||
})
|
||||
CompositeKey: fmt.Sprintf("%s|%s|%s", branch, repository, architecture),
|
||||
}
|
||||
pfs, _, err := packages_model.SearchFiles(ctx, opts)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if len(pfs) != 1 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
if len(pfs) == 0 {
|
||||
// Try again with architecture 'noarch'
|
||||
if architecture == alpine_module.NoArch {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
opts.CompositeKey = fmt.Sprintf("%s|%s|%s", branch, repository, alpine_module.NoArch)
|
||||
if pfs, _, err = packages_model.SearchFiles(ctx, opts); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pfs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s, u, pf, err := packages_service.GetPackageFileStream(ctx, pfs[0])
|
||||
|
||||
+75
-12
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@@ -36,7 +35,7 @@ import (
|
||||
"code.gitea.io/gitea/routers/api/packages/swift"
|
||||
"code.gitea.io/gitea/routers/api/packages/vagrant"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func reqPackageAccess(accessMode perm.AccessMode) func(ctx *context.Context) {
|
||||
@@ -513,14 +512,75 @@ func CommonRoutes() *web.Route {
|
||||
r.Get("/simple/{id}", pypi.PackageMetadata)
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
r.Group("/rpm", func() {
|
||||
r.Get(".repo", rpm.GetRepositoryConfig)
|
||||
r.Get("/repository.key", rpm.GetRepositoryKey)
|
||||
r.Put("/upload", reqPackageAccess(perm.AccessModeWrite), rpm.UploadPackageFile)
|
||||
r.Group("/package/{name}/{version}/{architecture}", func() {
|
||||
r.Get("", rpm.DownloadPackageFile)
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), rpm.DeletePackageFile)
|
||||
r.Group("/repository.key", func() {
|
||||
r.Head("", rpm.GetRepositoryKey)
|
||||
r.Get("", rpm.GetRepositoryKey)
|
||||
})
|
||||
|
||||
var (
|
||||
repoPattern = regexp.MustCompile(`\A(.*?)\.repo\z`)
|
||||
uploadPattern = regexp.MustCompile(`\A(.*?)/upload\z`)
|
||||
filePattern = regexp.MustCompile(`\A(.*?)/package/([^/]+)/([^/]+)/([^/]+)(?:/([^/]+\.rpm)|)\z`)
|
||||
repoFilePattern = regexp.MustCompile(`\A(.*?)/repodata/([^/]+)\z`)
|
||||
)
|
||||
|
||||
r.Methods("HEAD,GET,PUT,DELETE", "*", func(ctx *context.Context) {
|
||||
path := ctx.Params("*")
|
||||
isHead := ctx.Req.Method == "HEAD"
|
||||
isGetHead := ctx.Req.Method == "HEAD" || ctx.Req.Method == "GET"
|
||||
isPut := ctx.Req.Method == "PUT"
|
||||
isDelete := ctx.Req.Method == "DELETE"
|
||||
|
||||
m := repoPattern.FindStringSubmatch(path)
|
||||
if len(m) == 2 && isGetHead {
|
||||
ctx.SetParams("group", strings.Trim(m[1], "/"))
|
||||
rpm.GetRepositoryConfig(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
m = repoFilePattern.FindStringSubmatch(path)
|
||||
if len(m) == 3 && isGetHead {
|
||||
ctx.SetParams("group", strings.Trim(m[1], "/"))
|
||||
ctx.SetParams("filename", m[2])
|
||||
if isHead {
|
||||
rpm.CheckRepositoryFileExistence(ctx)
|
||||
} else {
|
||||
rpm.GetRepositoryFile(ctx)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
m = uploadPattern.FindStringSubmatch(path)
|
||||
if len(m) == 2 && isPut {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
ctx.SetParams("group", strings.Trim(m[1], "/"))
|
||||
rpm.UploadPackageFile(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
m = filePattern.FindStringSubmatch(path)
|
||||
if len(m) == 6 && (isGetHead || isDelete) {
|
||||
ctx.SetParams("group", strings.Trim(m[1], "/"))
|
||||
ctx.SetParams("name", m[2])
|
||||
ctx.SetParams("version", m[3])
|
||||
ctx.SetParams("architecture", m[4])
|
||||
if isGetHead {
|
||||
rpm.DownloadPackageFile(ctx)
|
||||
} else {
|
||||
reqPackageAccess(perm.AccessModeWrite)(ctx)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
rpm.DeletePackageFile(ctx)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNotFound)
|
||||
})
|
||||
r.Get("/repodata/{filename}", rpm.GetRepositoryFile)
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
r.Group("/rubygems", func() {
|
||||
r.Get("/specs.4.8.gz", rubygems.EnumeratePackages)
|
||||
@@ -581,7 +641,7 @@ func CommonRoutes() *web.Route {
|
||||
})
|
||||
})
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
}, context_service.UserAssignmentWeb(), context.PackageAssignment())
|
||||
}, context.UserAssignmentWeb(), context.PackageAssignment())
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -600,7 +660,10 @@ func ContainerRoutes() *web.Route {
|
||||
})
|
||||
|
||||
r.Get("", container.ReqContainerAccess, container.DetermineSupport)
|
||||
r.Get("/token", container.Authenticate)
|
||||
r.Group("/token", func() {
|
||||
r.Get("", container.Authenticate)
|
||||
r.Post("", container.AuthenticateNotImplemented)
|
||||
})
|
||||
r.Get("/_catalog", container.ReqContainerAccess, container.GetRepositoryList)
|
||||
r.Group("/{username}", func() {
|
||||
r.Group("/{image}", func() {
|
||||
@@ -748,7 +811,7 @@ func ContainerRoutes() *web.Route {
|
||||
|
||||
ctx.Status(http.StatusNotFound)
|
||||
})
|
||||
}, container.ReqContainerAccess, context_service.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
|
||||
}, container.ReqContainerAccess, context.UserAssignmentWeb(), context.PackageAssignment(), reqPackageAccess(perm.AccessModeRead))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -12,14 +12,15 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
cargo_module "code.gitea.io/gitea/modules/packages/cargo"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
cargo_service "code.gitea.io/gitea/services/packages/cargo"
|
||||
@@ -110,7 +111,7 @@ func SearchPackages(ctx *context.Context) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeCargo,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &paginator,
|
||||
},
|
||||
)
|
||||
@@ -250,7 +251,7 @@ func UploadPackage(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
|
||||
if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
|
||||
if err := packages_service.DeletePackageVersionAndReferences(ctx, pv); err != nil {
|
||||
log.Error("Rollback creation of package version: %v", err)
|
||||
}
|
||||
@@ -301,7 +302,7 @@ func yankPackage(ctx *context.Context, yank bool) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := cargo_service.AddOrUpdatePackageIndex(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
|
||||
if err := cargo_service.UpdatePackageIndexIfExists(ctx, ctx.Doer, ctx.Package.Owner, pv.PackageID); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
@@ -26,8 +27,6 @@ import (
|
||||
chef_module "code.gitea.io/gitea/modules/packages/chef"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
chef_module "code.gitea.io/gitea/modules/packages/chef"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func PackagesUniverse(ctx *context.Context) {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeChef,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -85,7 +86,7 @@ func EnumeratePackages(ctx *context.Context) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeChef,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: db.NewAbsoluteListOptions(
|
||||
ctx.FormInt("start"),
|
||||
ctx.FormInt("items"),
|
||||
|
||||
@@ -14,12 +14,13 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
composer_module "code.gitea.io/gitea/modules/packages/composer"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
@@ -66,7 +67,7 @@ func SearchPackages(ctx *context.Context) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeComposer,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &paginator,
|
||||
}
|
||||
if ctx.FormTrim("type") != "" {
|
||||
|
||||
@@ -15,13 +15,13 @@ import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
conan_model "code.gitea.io/gitea/models/packages/conan"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
conan_module "code.gitea.io/gitea/modules/packages/conan"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
|
||||
conan_model "code.gitea.io/gitea/models/packages/conan"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
conan_module "code.gitea.io/gitea/modules/packages/conan"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// SearchResult contains the found recipe names
|
||||
|
||||
@@ -12,13 +12,13 @@ import (
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
conda_model "code.gitea.io/gitea/models/packages/conda"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
conda_module "code.gitea.io/gitea/modules/packages/conda"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/dsnet/compress/bzip2"
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
container_model "code.gitea.io/gitea/models/packages/container"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
@@ -25,6 +24,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
container_service "code.gitea.io/gitea/services/packages/container"
|
||||
|
||||
@@ -114,11 +114,15 @@ func apiErrorDefined(ctx *context.Context, err *namedError) {
|
||||
})
|
||||
}
|
||||
|
||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost for anonymous access)
|
||||
func apiUnauthorizedError(ctx *context.Context) {
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
|
||||
apiErrorDefined(ctx, errUnauthorized)
|
||||
}
|
||||
|
||||
// ReqContainerAccess is a middleware which checks the current user valid (real user or ghost if anonymous access is enabled)
|
||||
func ReqContainerAccess(ctx *context.Context) {
|
||||
if ctx.Doer == nil {
|
||||
ctx.Resp.Header().Add("WWW-Authenticate", `Bearer realm="`+setting.AppURL+`v2/token",service="container_registry",scope="*"`)
|
||||
apiErrorDefined(ctx, errUnauthorized)
|
||||
if ctx.Doer == nil || (setting.Service.RequireSignInView && ctx.Doer.IsGhost()) {
|
||||
apiUnauthorizedError(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,10 +142,15 @@ func DetermineSupport(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Authenticate creates a token for the current user
|
||||
// If the current user is anonymous, the ghost user is used
|
||||
// If the current user is anonymous, the ghost user is used unless RequireSignInView is enabled.
|
||||
func Authenticate(ctx *context.Context) {
|
||||
u := ctx.Doer
|
||||
if u == nil {
|
||||
if setting.Service.RequireSignInView {
|
||||
apiUnauthorizedError(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
u = user_model.NewGhostUser()
|
||||
}
|
||||
|
||||
@@ -156,6 +165,17 @@ func Authenticate(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// https://distribution.github.io/distribution/spec/auth/oauth/
|
||||
func AuthenticateNotImplemented(ctx *context.Context) {
|
||||
// This optional endpoint can be used to authenticate a client.
|
||||
// It must implement the specification described in:
|
||||
// https://datatracker.ietf.org/doc/html/rfc6749
|
||||
// https://distribution.github.io/distribution/spec/auth/oauth/
|
||||
// Purpose of this stub is to respond with 404 Not Found instead of 405 Method Not Allowed.
|
||||
|
||||
ctx.Status(http.StatusNotFound)
|
||||
}
|
||||
|
||||
// https://docs.docker.com/registry/spec/api/#listing-repositories
|
||||
func GetRepositoryList(ctx *context.Context) {
|
||||
n := ctx.FormInt("n")
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
cran_model "code.gitea.io/gitea/models/packages/cran"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
cran_module "code.gitea.io/gitea/modules/packages/cran"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
debian_module "code.gitea.io/gitea/modules/packages/debian"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
debian_service "code.gitea.io/gitea/services/packages/debian"
|
||||
|
||||
@@ -8,18 +8,19 @@ import (
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
var (
|
||||
packageNameRegex = regexp.MustCompile(`\A[A-Za-z0-9\.\_\-\+]+\z`)
|
||||
filenameRegex = packageNameRegex
|
||||
packageNameRegex = regexp.MustCompile(`\A[-_+.\w]+\z`)
|
||||
filenameRegex = regexp.MustCompile(`\A[-_+=:;.()\[\]{}~!@#$%^& \w]+\z`)
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj any) {
|
||||
@@ -54,20 +55,38 @@ func DownloadPackageFile(ctx *context.Context) {
|
||||
helper.ServePackageFile(ctx, s, u, pf)
|
||||
}
|
||||
|
||||
func isValidPackageName(packageName string) bool {
|
||||
if len(packageName) == 1 && !unicode.IsLetter(rune(packageName[0])) && !unicode.IsNumber(rune(packageName[0])) {
|
||||
return false
|
||||
}
|
||||
return packageNameRegex.MatchString(packageName) && packageName != ".."
|
||||
}
|
||||
|
||||
func isValidFileName(filename string) bool {
|
||||
return filenameRegex.MatchString(filename) &&
|
||||
strings.TrimSpace(filename) == filename &&
|
||||
filename != "." && filename != ".."
|
||||
}
|
||||
|
||||
// UploadPackage uploads the specific generic package.
|
||||
// Duplicated packages get rejected.
|
||||
func UploadPackage(ctx *context.Context) {
|
||||
packageName := ctx.Params("packagename")
|
||||
filename := ctx.Params("filename")
|
||||
|
||||
if !packageNameRegex.MatchString(packageName) || !filenameRegex.MatchString(filename) {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("Invalid package name or filename"))
|
||||
if !isValidPackageName(packageName) {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("invalid package name"))
|
||||
return
|
||||
}
|
||||
|
||||
if !isValidFileName(filename) {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("invalid filename"))
|
||||
return
|
||||
}
|
||||
|
||||
packageVersion := ctx.Params("packageversion")
|
||||
if packageVersion != strings.TrimSpace(packageVersion) {
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("Invalid package version"))
|
||||
apiError(ctx, http.StatusBadRequest, errors.New("invalid package version"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package generic
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidatePackageName(t *testing.T) {
|
||||
bad := []string{
|
||||
"",
|
||||
".",
|
||||
"..",
|
||||
"-",
|
||||
"a?b",
|
||||
"a b",
|
||||
"a/b",
|
||||
}
|
||||
for _, name := range bad {
|
||||
assert.False(t, isValidPackageName(name), "bad=%q", name)
|
||||
}
|
||||
|
||||
good := []string{
|
||||
"a",
|
||||
"1",
|
||||
"a-",
|
||||
"a_b",
|
||||
"c.d+",
|
||||
}
|
||||
for _, name := range good {
|
||||
assert.True(t, isValidPackageName(name), "good=%q", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateFileName(t *testing.T) {
|
||||
bad := []string{
|
||||
"",
|
||||
".",
|
||||
"..",
|
||||
"a?b",
|
||||
"a/b",
|
||||
" a",
|
||||
"a ",
|
||||
}
|
||||
for _, name := range bad {
|
||||
assert.False(t, isValidFileName(name), "bad=%q", name)
|
||||
}
|
||||
|
||||
good := []string{
|
||||
"-",
|
||||
"a",
|
||||
"1",
|
||||
"a-",
|
||||
"a_b",
|
||||
"a b",
|
||||
"c.d+",
|
||||
`-_+=:;.()[]{}~!@#$%^& aA1`,
|
||||
}
|
||||
for _, name := range good {
|
||||
assert.True(t, isValidFileName(name), "good=%q", name)
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,12 @@ import (
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
goproxy_module "code.gitea.io/gitea/modules/packages/goproxy"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
@@ -129,7 +130,7 @@ func resolvePackage(ctx *context.Context, ownerID int64, name, version string) (
|
||||
Value: name,
|
||||
ExactMatch: true,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Sort: packages_model.SortCreatedDesc,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -13,14 +13,15 @@ import (
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
helm_module "code.gitea.io/gitea/modules/packages/helm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -42,7 +43,7 @@ func Index(ctx *context.Context) {
|
||||
pvs, _, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeHelm,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -110,7 +111,7 @@ func DownloadPackageFile(ctx *context.Context) {
|
||||
Value: ctx.Params("package"),
|
||||
},
|
||||
HasFileWithName: filename,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
|
||||
@@ -10,9 +10,9 @@ import (
|
||||
"net/url"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// LogAndProcessError logs an error and calls a custom callback with the processed error message.
|
||||
|
||||
@@ -6,6 +6,7 @@ package maven
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
@@ -19,15 +20,13 @@ import (
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
maven_module "code.gitea.io/gitea/modules/packages/maven"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/minio/sha256-simd"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
npm_module "code.gitea.io/gitea/modules/packages/npm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
)
|
||||
|
||||
func createPackageMetadataResponse(registryURL string, pds []*packages_model.PackageDescriptor) *npm_module.PackageMetadata {
|
||||
@@ -98,7 +99,7 @@ func createPackageSearchResponse(pds []*packages_model.PackageDescriptor, total
|
||||
Maintainers: []npm_module.User{}, // npm cli needs this field
|
||||
Keywords: metadata.Keywords,
|
||||
Links: &npm_module.PackageSearchPackageLinks{
|
||||
Registry: pd.FullWebLink(),
|
||||
Registry: setting.AppURL + "api/packages/" + pd.Owner.Name + "/npm",
|
||||
Homepage: metadata.ProjectURL,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -17,12 +17,13 @@ import (
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
npm_module "code.gitea.io/gitea/modules/packages/npm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
@@ -120,7 +121,7 @@ func DownloadPackageFileByName(ctx *context.Context) {
|
||||
Value: packageNameFromParams(ctx),
|
||||
},
|
||||
HasFileWithName: filename,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -395,7 +396,7 @@ func setPackageTag(ctx std_ctx.Context, tag string, pv *packages_model.PackageVe
|
||||
Properties: map[string]string{
|
||||
npm_module.TagProperty: tag,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -431,7 +432,7 @@ func PackageSearch(ctx *context.Context) {
|
||||
pvs, total, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNpm,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Name: packages_model.SearchValue{
|
||||
ExactMatch: false,
|
||||
Value: ctx.FormTrim("text"),
|
||||
|
||||
@@ -17,13 +17,14 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
nuget_model "code.gitea.io/gitea/models/packages/nuget"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
nuget_module "code.gitea.io/gitea/modules/packages/nuget"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
@@ -122,7 +123,7 @@ func SearchServiceV2(ctx *context.Context) {
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: paginator,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -172,7 +173,7 @@ func SearchServiceV2Count(ctx *context.Context) {
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -187,7 +188,7 @@ func SearchServiceV3(ctx *context.Context) {
|
||||
pvs, count, err := nuget_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Name: packages_model.SearchValue{Value: ctx.FormTrim("q")},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: db.NewAbsoluteListOptions(
|
||||
ctx.FormInt("skip"),
|
||||
ctx.FormInt("take"),
|
||||
@@ -313,7 +314,7 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
|
||||
ExactMatch: true,
|
||||
Value: packageName,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: paginator,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -358,7 +359,7 @@ func EnumeratePackageVersionsV2Count(ctx *context.Context) {
|
||||
ExactMatch: true,
|
||||
Value: strings.Trim(ctx.FormTrim("id"), "'"),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"time"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
@@ -22,6 +21,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
pypi_module "code.gitea.io/gitea/modules/packages/pypi"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
rpm_module "code.gitea.io/gitea/modules/packages/rpm"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
rpm_service "code.gitea.io/gitea/services/packages/rpm"
|
||||
@@ -33,11 +33,18 @@ func apiError(ctx *context.Context, status int, obj any) {
|
||||
|
||||
// https://dnf.readthedocs.io/en/latest/conf_ref.html
|
||||
func GetRepositoryConfig(ctx *context.Context) {
|
||||
group := ctx.Params("group")
|
||||
|
||||
var groupParts []string
|
||||
if group != "" {
|
||||
groupParts = strings.Split(group, "/")
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%sapi/packages/%s/rpm", setting.AppURL, ctx.Package.Owner.Name)
|
||||
|
||||
ctx.PlainText(http.StatusOK, `[gitea-`+ctx.Package.Owner.LowerName+`]
|
||||
name=`+ctx.Package.Owner.Name+` - `+setting.AppName+`
|
||||
baseurl=`+url+`
|
||||
ctx.PlainText(http.StatusOK, `[gitea-`+strings.Join(append([]string{ctx.Package.Owner.LowerName}, groupParts...), "-")+`]
|
||||
name=`+strings.Join(append([]string{ctx.Package.Owner.Name, setting.AppName}, groupParts...), " - ")+`
|
||||
baseurl=`+strings.Join(append([]string{url}, groupParts...), "/")+`
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=`+url+`/repository.key`)
|
||||
@@ -57,6 +64,30 @@ func GetRepositoryKey(ctx *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func CheckRepositoryFileExistence(ctx *context.Context) {
|
||||
pv, err := rpm_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pf, err := packages_model.GetFileForVersionByName(ctx, pv.ID, ctx.Params("filename"), ctx.Params("group"))
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetServeHeaders(&context.ServeHeaderOptions{
|
||||
Filename: pf.Name,
|
||||
LastModified: pf.CreatedUnix.AsLocalTime(),
|
||||
})
|
||||
ctx.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// Gets a pre-generated repository metadata file
|
||||
func GetRepositoryFile(ctx *context.Context) {
|
||||
pv, err := rpm_service.GetOrCreateRepositoryVersion(ctx, ctx.Package.Owner.ID)
|
||||
@@ -69,7 +100,8 @@ func GetRepositoryFile(ctx *context.Context) {
|
||||
ctx,
|
||||
pv,
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: ctx.Params("filename"),
|
||||
Filename: ctx.Params("filename"),
|
||||
CompositeKey: ctx.Params("group"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -121,7 +153,7 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
group := ctx.Params("group")
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||
ctx,
|
||||
&packages_service.PackageCreationInfo{
|
||||
@@ -136,13 +168,16 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture),
|
||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", pck.Name, pck.Version, pck.FileMetadata.Architecture),
|
||||
CompositeKey: group,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
Properties: map[string]string{
|
||||
rpm_module.PropertyMetadata: string(fileMetadataRaw),
|
||||
rpm_module.PropertyGroup: group,
|
||||
rpm_module.PropertyArchitecture: pck.FileMetadata.Architecture,
|
||||
rpm_module.PropertyMetadata: string(fileMetadataRaw),
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -158,7 +193,7 @@ func UploadPackageFile(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := rpm_service.BuildRepositoryFiles(ctx, ctx.Package.Owner.ID); err != nil {
|
||||
if err := rpm_service.BuildSpecificRepositoryFiles(ctx, ctx.Package.Owner.ID, group); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
@@ -179,7 +214,8 @@ func DownloadPackageFile(ctx *context.Context) {
|
||||
Version: version,
|
||||
},
|
||||
&packages_service.PackageFileInfo{
|
||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
|
||||
Filename: fmt.Sprintf("%s-%s.%s.rpm", name, version, ctx.Params("architecture")),
|
||||
CompositeKey: ctx.Params("group"),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@@ -195,6 +231,7 @@ func DownloadPackageFile(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func DeletePackageFile(webctx *context.Context) {
|
||||
group := webctx.Params("group")
|
||||
name := webctx.Params("name")
|
||||
version := webctx.Params("version")
|
||||
architecture := webctx.Params("architecture")
|
||||
@@ -202,7 +239,12 @@ func DeletePackageFile(webctx *context.Context) {
|
||||
var pd *packages_model.PackageDescriptor
|
||||
|
||||
err := db.WithTx(webctx, func(ctx stdctx.Context) error {
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, webctx.Package.Owner.ID, packages_model.TypeRpm, name, version)
|
||||
pv, err := packages_model.GetVersionByNameAndVersion(ctx,
|
||||
webctx.Package.Owner.ID,
|
||||
packages_model.TypeRpm,
|
||||
name,
|
||||
version,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -211,7 +253,7 @@ func DeletePackageFile(webctx *context.Context) {
|
||||
ctx,
|
||||
pv.ID,
|
||||
fmt.Sprintf("%s-%s.%s.rpm", name, version, architecture),
|
||||
packages_model.EmptyFileKey,
|
||||
group,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -251,7 +293,7 @@ func DeletePackageFile(webctx *context.Context) {
|
||||
notify_service.PackageDelete(webctx, webctx.Doer, pd)
|
||||
}
|
||||
|
||||
if err := rpm_service.BuildRepositoryFiles(webctx, webctx.Package.Owner.ID); err != nil {
|
||||
if err := rpm_service.BuildSpecificRepositoryFiles(webctx, webctx.Package.Owner.ID, group); err != nil {
|
||||
apiError(webctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -13,11 +13,12 @@ import (
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
rubygems_module "code.gitea.io/gitea/modules/packages/rubygems"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
|
||||
@@ -43,7 +44,7 @@ func EnumeratePackagesLatest(ctx *context.Context) {
|
||||
pvs, _, err := packages_model.SearchLatestVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeRubyGems,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
@@ -304,7 +305,7 @@ func getVersionsByFilename(ctx *context.Context, filename string) ([]*packages_m
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeRubyGems,
|
||||
HasFileWithName: filename,
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
return pvs, err
|
||||
}
|
||||
|
||||
@@ -13,14 +13,15 @@ import (
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
swift_module "code.gitea.io/gitea/modules/packages/swift"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
@@ -157,7 +158,7 @@ func EnumeratePackageVersions(ctx *context.Context) {
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Name string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Checksum string `json:"checksum"`
|
||||
}
|
||||
@@ -433,7 +434,7 @@ func LookupPackageIdentifiers(ctx *context.Context) {
|
||||
Properties: map[string]string{
|
||||
swift_module.PropertyRepositoryURL: url,
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
vagrant_module "code.gitea.io/gitea/modules/packages/vagrant"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/go-ap/jsonld"
|
||||
|
||||
@@ -13,9 +13,9 @@ import (
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/activitypub"
|
||||
gitea_context "code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/httplib"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
gitea_context "code.gitea.io/gitea/services/context"
|
||||
|
||||
ap "github.com/go-ap/activitypub"
|
||||
"github.com/go-fed/httpsig"
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ package admin
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/cron"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
)
|
||||
|
||||
@@ -37,7 +38,7 @@ func ListHooks(ctx *context.APIContext) {
|
||||
// "200":
|
||||
// "$ref": "#/responses/HookList"
|
||||
|
||||
sysHooks, err := webhook.GetSystemWebhooks(ctx, util.OptionalBoolNone)
|
||||
sysHooks, err := webhook.GetSystemWebhooks(ctx, optional.None[bool]())
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetSystemWebhooks", err)
|
||||
return
|
||||
|
||||
@@ -10,10 +10,10 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/repo"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// CreateRepo api for creating a repository
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
|
||||
|
||||
// GetRegistrationToken returns the token to register global runners
|
||||
func GetRegistrationToken(ctx *context.APIContext) {
|
||||
// swagger:operation GET /admin/runners/registration-token admin adminGetRunnerRegistrationToken
|
||||
// ---
|
||||
// summary: Get an global actions runner registration token
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/RegistrationToken"
|
||||
|
||||
shared.GetRegistrationToken(ctx, 0, 0)
|
||||
}
|
||||
+83
-115
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
@@ -16,16 +15,16 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/auth/password"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
@@ -93,26 +92,32 @@ func CreateUser(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
err := errors.New("PasswordComplexity")
|
||||
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
||||
return
|
||||
}
|
||||
pwned, err := password.IsPwned(ctx, form.Password)
|
||||
if pwned {
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
|
||||
if u.LoginType == auth.Plain {
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
err := errors.New("PasswordIsRequired")
|
||||
ctx.Error(http.StatusBadRequest, "PasswordIsRequired", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
err := errors.New("PasswordComplexity")
|
||||
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := password.IsPwned(ctx, form.Password); err != nil {
|
||||
if password.IsErrIsPwnedRequest(err) {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
||||
return
|
||||
}
|
||||
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
||||
return
|
||||
}
|
||||
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
}
|
||||
|
||||
if form.Restricted != nil {
|
||||
overwriteDefault.IsRestricted = util.OptionalBoolOf(*form.Restricted)
|
||||
IsActive: optional.Some(true),
|
||||
IsRestricted: optional.FromPtr(form.Restricted),
|
||||
}
|
||||
|
||||
if form.Visibility != "" {
|
||||
@@ -128,7 +133,7 @@ func CreateUser(ctx *context.APIContext) {
|
||||
u.UpdatedUnix = u.CreatedUnix
|
||||
}
|
||||
|
||||
if err := user_model.CreateUser(ctx, u, overwriteDefault); err != nil {
|
||||
if err := user_model.AdminCreateUser(ctx, u, overwriteDefault); err != nil {
|
||||
if user_model.IsErrUserAlreadyExist(err) ||
|
||||
user_model.IsErrEmailAlreadyUsed(err) ||
|
||||
db.IsErrNameReserved(err) ||
|
||||
@@ -142,6 +147,11 @@ func CreateUser(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !user_model.IsEmailDomainAllowed(u.Email) {
|
||||
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", u.Email))
|
||||
}
|
||||
|
||||
log.Trace("Account created by admin (%s): %s", ctx.Doer.Name, u.Name)
|
||||
|
||||
// Send email notification.
|
||||
@@ -173,6 +183,8 @@ func EditUser(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/User"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
@@ -180,111 +192,69 @@ func EditUser(ctx *context.APIContext) {
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditUserOption)
|
||||
|
||||
parseAuthSource(ctx, ctx.ContextUser, form.SourceID, form.LoginName)
|
||||
if ctx.Written() {
|
||||
authOpts := &user_service.UpdateAuthOptions{
|
||||
LoginSource: optional.FromNonDefault(form.SourceID),
|
||||
LoginName: optional.Some(form.LoginName),
|
||||
Password: optional.FromNonDefault(form.Password),
|
||||
MustChangePassword: optional.FromPtr(form.MustChangePassword),
|
||||
ProhibitLogin: optional.FromPtr(form.ProhibitLogin),
|
||||
}
|
||||
if err := user_service.UpdateAuth(ctx, ctx.ContextUser, authOpts); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, password.ErrMinLength):
|
||||
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
|
||||
case errors.Is(err, password.ErrComplexity):
|
||||
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
||||
case errors.Is(err, password.ErrIsPwned), password.IsErrIsPwnedRequest(err):
|
||||
ctx.Error(http.StatusBadRequest, "PasswordIsPwned", err)
|
||||
default:
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAuth", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if len(form.Password) != 0 {
|
||||
if len(form.Password) < setting.MinPasswordLength {
|
||||
ctx.Error(http.StatusBadRequest, "PasswordTooShort", fmt.Errorf("password must be at least %d characters", setting.MinPasswordLength))
|
||||
return
|
||||
}
|
||||
if !password.IsComplexEnough(form.Password) {
|
||||
err := errors.New("PasswordComplexity")
|
||||
ctx.Error(http.StatusBadRequest, "PasswordComplexity", err)
|
||||
return
|
||||
}
|
||||
pwned, err := password.IsPwned(ctx, form.Password)
|
||||
if pwned {
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
ctx.Error(http.StatusBadRequest, "PasswordPwned", errors.New("PasswordPwned"))
|
||||
return
|
||||
}
|
||||
if ctx.ContextUser.Salt, err = user_model.GetUserSalt(); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
||||
return
|
||||
}
|
||||
if err = ctx.ContextUser.SetPassword(form.Password); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if form.MustChangePassword != nil {
|
||||
ctx.ContextUser.MustChangePassword = *form.MustChangePassword
|
||||
}
|
||||
|
||||
ctx.ContextUser.LoginName = form.LoginName
|
||||
|
||||
if form.FullName != nil {
|
||||
ctx.ContextUser.FullName = *form.FullName
|
||||
}
|
||||
var emailChanged bool
|
||||
if form.Email != nil {
|
||||
email := strings.TrimSpace(*form.Email)
|
||||
if len(email) == 0 {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("email is not allowed to be empty string"))
|
||||
if err := user_service.AdminAddOrSetPrimaryEmailAddress(ctx, ctx.ContextUser, *form.Email); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrEmailCharIsNotSupported(err), user_model.IsErrEmailInvalid(err):
|
||||
ctx.Error(http.StatusBadRequest, "EmailInvalid", err)
|
||||
case user_model.IsErrEmailAlreadyUsed(err):
|
||||
ctx.Error(http.StatusBadRequest, "EmailUsed", err)
|
||||
default:
|
||||
ctx.Error(http.StatusInternalServerError, "AddOrSetPrimaryEmailAddress", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.ValidateEmail(email); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
if !user_model.IsEmailDomainAllowed(*form.Email) {
|
||||
ctx.Resp.Header().Add("X-Gitea-Warning", fmt.Sprintf("the domain of user email %s conflicts with EMAIL_DOMAIN_ALLOWLIST or EMAIL_DOMAIN_BLOCKLIST", *form.Email))
|
||||
}
|
||||
|
||||
emailChanged = !strings.EqualFold(ctx.ContextUser.Email, email)
|
||||
ctx.ContextUser.Email = email
|
||||
}
|
||||
if form.Website != nil {
|
||||
ctx.ContextUser.Website = *form.Website
|
||||
}
|
||||
if form.Location != nil {
|
||||
ctx.ContextUser.Location = *form.Location
|
||||
}
|
||||
if form.Description != nil {
|
||||
ctx.ContextUser.Description = *form.Description
|
||||
}
|
||||
if form.Active != nil {
|
||||
ctx.ContextUser.IsActive = *form.Active
|
||||
}
|
||||
if len(form.Visibility) != 0 {
|
||||
ctx.ContextUser.Visibility = api.VisibilityModes[form.Visibility]
|
||||
}
|
||||
if form.Admin != nil {
|
||||
ctx.ContextUser.IsAdmin = *form.Admin
|
||||
}
|
||||
if form.AllowGitHook != nil {
|
||||
ctx.ContextUser.AllowGitHook = *form.AllowGitHook
|
||||
}
|
||||
if form.AllowImportLocal != nil {
|
||||
ctx.ContextUser.AllowImportLocal = *form.AllowImportLocal
|
||||
}
|
||||
if form.MaxRepoCreation != nil {
|
||||
ctx.ContextUser.MaxRepoCreation = *form.MaxRepoCreation
|
||||
}
|
||||
if form.AllowCreateOrganization != nil {
|
||||
ctx.ContextUser.AllowCreateOrganization = *form.AllowCreateOrganization
|
||||
}
|
||||
if form.ProhibitLogin != nil {
|
||||
ctx.ContextUser.ProhibitLogin = *form.ProhibitLogin
|
||||
}
|
||||
if form.Restricted != nil {
|
||||
ctx.ContextUser.IsRestricted = *form.Restricted
|
||||
}
|
||||
|
||||
if err := user_model.UpdateUser(ctx, ctx.ContextUser, emailChanged); err != nil {
|
||||
if user_model.IsErrEmailAlreadyUsed(err) ||
|
||||
user_model.IsErrEmailCharIsNotSupported(err) ||
|
||||
user_model.IsErrEmailInvalid(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
opts := &user_service.UpdateOptions{
|
||||
FullName: optional.FromPtr(form.FullName),
|
||||
Website: optional.FromPtr(form.Website),
|
||||
Location: optional.FromPtr(form.Location),
|
||||
Description: optional.FromPtr(form.Description),
|
||||
IsActive: optional.FromPtr(form.Active),
|
||||
IsAdmin: optional.FromPtr(form.Admin),
|
||||
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
|
||||
AllowGitHook: optional.FromPtr(form.AllowGitHook),
|
||||
AllowImportLocal: optional.FromPtr(form.AllowImportLocal),
|
||||
MaxRepoCreation: optional.FromPtr(form.MaxRepoCreation),
|
||||
AllowCreateOrganization: optional.FromPtr(form.AllowCreateOrganization),
|
||||
IsRestricted: optional.FromPtr(form.Restricted),
|
||||
}
|
||||
|
||||
if err := user_service.UpdateUser(ctx, ctx.ContextUser, opts); err != nil {
|
||||
if models.IsErrDeleteLastAdminUser(err) {
|
||||
ctx.Error(http.StatusBadRequest, "LastAdmin", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Account profile updated by admin (%s): %s", ctx.Doer.Name, ctx.ContextUser.Name)
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToUser(ctx, ctx.ContextUser, ctx.Doer))
|
||||
@@ -331,7 +301,8 @@ func DeleteUser(ctx *context.APIContext) {
|
||||
if err := user_service.DeleteUser(ctx, ctx.ContextUser, ctx.FormBool("purge")); err != nil {
|
||||
if models.IsErrUserOwnRepos(err) ||
|
||||
models.IsErrUserHasOrgs(err) ||
|
||||
models.IsErrUserOwnPackages(err) {
|
||||
models.IsErrUserOwnPackages(err) ||
|
||||
models.IsErrDeleteLastAdminUser(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteUser", err)
|
||||
@@ -510,9 +481,6 @@ func RenameUser(ctx *context.APIContext) {
|
||||
// Check if user name has been changed
|
||||
if err := user_service.RenameUser(ctx, ctx.ContextUser, newName); err != nil {
|
||||
switch {
|
||||
case user_model.IsErrUsernameNotChanged(err):
|
||||
// Noop as username is not changed
|
||||
ctx.Status(http.StatusNoContent)
|
||||
case user_model.IsErrUserAlreadyExist(err):
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", ctx.Tr("form.username_been_taken"))
|
||||
case db.IsErrNameReserved(err):
|
||||
@@ -528,5 +496,5 @@ func RenameUser(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
log.Trace("User name changed: %s -> %s", oldName, newName)
|
||||
ctx.Status(http.StatusOK)
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// ListUserBadges lists all badges belonging to a user
|
||||
func ListUserBadges(ctx *context.APIContext) {
|
||||
// swagger:operation GET /admin/users/{username}/badges admin adminListUserBadges
|
||||
// ---
|
||||
// summary: List a user's badges
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/BadgeList"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
badges, maxResults, err := user_model.GetUserBadges(ctx, ctx.ContextUser)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetUserBadges", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(maxResults)
|
||||
ctx.JSON(http.StatusOK, &badges)
|
||||
}
|
||||
|
||||
// AddUserBadges add badges to a user
|
||||
func AddUserBadges(ctx *context.APIContext) {
|
||||
// swagger:operation POST /admin/users/{username}/badges admin adminAddUserBadges
|
||||
// ---
|
||||
// summary: Add a badge to a user
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UserBadgeOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
|
||||
form := web.GetForm(ctx).(*api.UserBadgeOption)
|
||||
badges := prepareBadgesForReplaceOrAdd(ctx, *form)
|
||||
|
||||
if err := user_model.AddUserBadges(ctx, ctx.ContextUser, badges); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// DeleteUserBadges delete a badge from a user
|
||||
func DeleteUserBadges(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /admin/users/{username}/badges admin adminDeleteUserBadges
|
||||
// ---
|
||||
// summary: Remove a badge from a user
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: username of user
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UserBadgeOption"
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.UserBadgeOption)
|
||||
badges := prepareBadgesForReplaceOrAdd(ctx, *form)
|
||||
|
||||
if err := user_model.RemoveUserBadges(ctx, ctx.ContextUser, badges); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReplaceUserBadges", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func prepareBadgesForReplaceOrAdd(ctx *context.APIContext, form api.UserBadgeOption) []*user_model.Badge {
|
||||
badges := make([]*user_model.Badge, len(form.BadgeSlugs))
|
||||
for i, badge := range form.BadgeSlugs {
|
||||
badges[i] = &user_model.Badge{
|
||||
Slug: badge,
|
||||
}
|
||||
}
|
||||
return badges
|
||||
}
|
||||
+141
-80
@@ -2,13 +2,13 @@
|
||||
// Copyright 2016 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package v1 Gitea API.
|
||||
// Package v1 Gitea API
|
||||
//
|
||||
// This documentation describes the Gitea API.
|
||||
//
|
||||
// Schemes: http, https
|
||||
// Schemes: https, http
|
||||
// BasePath: /api/v1
|
||||
// Version: {{AppVer | JSEscape | Safe}}
|
||||
// Version: {{AppVer | JSEscape}}
|
||||
// License: MIT http://opensource.org/licenses/MIT
|
||||
//
|
||||
// Consumes:
|
||||
@@ -35,10 +35,12 @@
|
||||
// type: apiKey
|
||||
// name: token
|
||||
// in: query
|
||||
// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead.
|
||||
// AccessToken:
|
||||
// type: apiKey
|
||||
// name: access_token
|
||||
// in: query
|
||||
// description: This authentication option is deprecated for removal in Gitea 1.23. Please use AuthorizationHeaderToken instead.
|
||||
// AuthorizationHeaderToken:
|
||||
// type: apiKey
|
||||
// name: Authorization
|
||||
@@ -77,7 +79,6 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
@@ -93,7 +94,7 @@ import (
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
context_service "code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
_ "code.gitea.io/gitea/routers/api/v1/swagger" // for swagger generation
|
||||
@@ -316,10 +317,6 @@ func reqToken() func(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.IsBasicAuth {
|
||||
ctx.CheckForOTP()
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned {
|
||||
return
|
||||
}
|
||||
@@ -344,7 +341,6 @@ func reqBasicOrRevProxyAuth() func(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusUnauthorized, "reqBasicAuth", "auth required")
|
||||
return
|
||||
}
|
||||
ctx.CheckForOTP()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -701,12 +697,6 @@ func bind[T any](_ T) any {
|
||||
}
|
||||
}
|
||||
|
||||
// The OAuth2 plugin is expected to be executed first, as it must ignore the user id stored
|
||||
// in the session (if there is a user id stored in session other plugins might return the user
|
||||
// object for that id).
|
||||
//
|
||||
// The Session plugin is expected to be executed second, in order to skip authentication
|
||||
// for users that have already signed in.
|
||||
func buildAuthGroup() *auth.Group {
|
||||
group := auth.NewGroup(
|
||||
&auth.OAuth2{},
|
||||
@@ -786,31 +776,6 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
|
||||
})
|
||||
return
|
||||
}
|
||||
if ctx.IsSigned && ctx.IsBasicAuth {
|
||||
if skip, ok := ctx.Data["SkipLocalTwoFA"]; ok && skip.(bool) {
|
||||
return // Skip 2FA
|
||||
}
|
||||
twofa, err := auth_model.GetTwoFactorByUID(ctx, ctx.Doer.ID)
|
||||
if err != nil {
|
||||
if auth_model.IsErrTwoFactorNotEnrolled(err) {
|
||||
return // No 2FA enrollment for this user
|
||||
}
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
otpHeader := ctx.Req.Header.Get("X-Gitea-OTP")
|
||||
ok, err := twofa.ValidateTOTP(otpHeader)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
if !ok {
|
||||
ctx.JSON(http.StatusForbidden, map[string]string{
|
||||
"message": "Only signed in user is allowed to call APIs.",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.AdminRequired {
|
||||
@@ -824,6 +789,31 @@ func verifyAuthWithOptions(options *common.VerifyOptions) func(ctx *context.APIC
|
||||
}
|
||||
}
|
||||
|
||||
func individualPermsChecker(ctx *context.APIContext) {
|
||||
// org permissions have been checked in context.OrgAssignment(), but individual permissions haven't been checked.
|
||||
if ctx.ContextUser.IsIndividual() {
|
||||
switch {
|
||||
case ctx.ContextUser.Visibility == api.VisibleTypePrivate:
|
||||
if ctx.Doer == nil || (ctx.ContextUser.ID != ctx.Doer.ID && !ctx.Doer.IsAdmin) {
|
||||
ctx.NotFound("Visit Project", nil)
|
||||
return
|
||||
}
|
||||
case ctx.ContextUser.Visibility == api.VisibleTypeLimited:
|
||||
if ctx.Doer == nil {
|
||||
ctx.NotFound("Visit Project", nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check for and warn against deprecated authentication options
|
||||
func checkDeprecatedAuthMethods(ctx *context.APIContext) {
|
||||
if ctx.FormString("token") != "" || ctx.FormString("access_token") != "" {
|
||||
ctx.Resp.Header().Set("X-Gitea-Warning", "token and access_token API authentication is deprecated and will be removed in gitea 1.23. Please use AuthorizationHeaderToken instead. Existing queries will continue to work but without authorization.")
|
||||
}
|
||||
}
|
||||
|
||||
// Routes registers all v1 APIs routes to web application.
|
||||
func Routes() *web.Route {
|
||||
m := web.NewRoute()
|
||||
@@ -831,9 +821,7 @@ func Routes() *web.Route {
|
||||
m.Use(securityHeaders())
|
||||
if setting.CORSConfig.Enabled {
|
||||
m.Use(cors.Handler(cors.Options{
|
||||
// Scheme: setting.CORSConfig.Scheme, // FIXME: the cors middleware needs scheme option
|
||||
AllowedOrigins: setting.CORSConfig.AllowDomain,
|
||||
// setting.CORSConfig.AllowSubdomain // FIXME: the cors middleware needs allowSubdomain option
|
||||
AllowedOrigins: setting.CORSConfig.AllowDomain,
|
||||
AllowedMethods: setting.CORSConfig.Methods,
|
||||
AllowCredentials: setting.CORSConfig.AllowCredentials,
|
||||
AllowedHeaders: append([]string{"Authorization", "X-Gitea-OTP"}, setting.CORSConfig.Headers...),
|
||||
@@ -842,6 +830,8 @@ func Routes() *web.Route {
|
||||
}
|
||||
m.Use(context.APIContexter())
|
||||
|
||||
m.Use(checkDeprecatedAuthMethods)
|
||||
|
||||
// Get user from session if logged in.
|
||||
m.Use(apiAuth(buildAuthGroup()))
|
||||
|
||||
@@ -864,11 +854,11 @@ func Routes() *web.Route {
|
||||
m.Group("/user/{username}", func() {
|
||||
m.Get("", activitypub.Person)
|
||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||
}, context_service.UserAssignmentAPI())
|
||||
}, context.UserAssignmentAPI())
|
||||
m.Group("/user-id/{user-id}", func() {
|
||||
m.Get("", activitypub.Person)
|
||||
m.Post("/inbox", activitypub.ReqHTTPSignature(), activitypub.PersonInbox)
|
||||
}, context_service.UserIDAssignmentAPI())
|
||||
}, context.UserIDAssignmentAPI())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryActivityPub))
|
||||
}
|
||||
|
||||
@@ -924,7 +914,7 @@ func Routes() *web.Route {
|
||||
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())
|
||||
|
||||
m.Get("/activities/feeds", user.ListUserActivityFeeds)
|
||||
}, context_service.UserAssignmentAPI())
|
||||
}, context.UserAssignmentAPI(), individualPermsChecker)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))
|
||||
|
||||
// Users (requires user scope)
|
||||
@@ -942,7 +932,7 @@ func Routes() *web.Route {
|
||||
m.Get("/starred", user.GetStarredRepos)
|
||||
|
||||
m.Get("/subscriptions", user.GetWatchedRepos)
|
||||
}, context_service.UserAssignmentAPI())
|
||||
}, context.UserAssignmentAPI())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
|
||||
// Users (requires user scope)
|
||||
@@ -957,11 +947,26 @@ func Routes() *web.Route {
|
||||
Post(bind(api.CreateEmailOption{}), user.AddEmail).
|
||||
Delete(bind(api.DeleteEmailOption{}), user.DeleteEmail)
|
||||
|
||||
// create or update a user's actions secrets
|
||||
m.Group("/actions/secrets", func() {
|
||||
m.Combo("/{secretname}").
|
||||
Put(bind(api.CreateOrUpdateSecretOption{}), user.CreateOrUpdateSecret).
|
||||
Delete(repo.DeleteSecret)
|
||||
// manage user-level actions features
|
||||
m.Group("/actions", func() {
|
||||
m.Group("/secrets", func() {
|
||||
m.Combo("/{secretname}").
|
||||
Put(bind(api.CreateOrUpdateSecretOption{}), user.CreateOrUpdateSecret).
|
||||
Delete(user.DeleteSecret)
|
||||
})
|
||||
|
||||
m.Group("/variables", func() {
|
||||
m.Get("", user.ListVariables)
|
||||
m.Combo("/{variablename}").
|
||||
Get(user.GetVariable).
|
||||
Delete(user.DeleteVariable).
|
||||
Post(bind(api.CreateVariableOption{}), user.CreateVariable).
|
||||
Put(bind(api.UpdateVariableOption{}), user.UpdateVariable)
|
||||
})
|
||||
|
||||
m.Group("/runners", func() {
|
||||
m.Get("/registration-token", reqToken(), user.GetRegistrationToken)
|
||||
})
|
||||
})
|
||||
|
||||
m.Get("/followers", user.ListMyFollowers)
|
||||
@@ -971,7 +976,7 @@ func Routes() *web.Route {
|
||||
m.Get("", user.CheckMyFollowing)
|
||||
m.Put("", user.Follow)
|
||||
m.Delete("", user.Unfollow)
|
||||
}, context_service.UserAssignmentAPI())
|
||||
}, context.UserAssignmentAPI())
|
||||
})
|
||||
|
||||
// (admin:public_key scope)
|
||||
@@ -1031,7 +1036,16 @@ func Routes() *web.Route {
|
||||
m.Group("/avatar", func() {
|
||||
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
|
||||
m.Delete("", user.DeleteAvatar)
|
||||
}, reqToken())
|
||||
})
|
||||
|
||||
m.Group("/blocks", func() {
|
||||
m.Get("", user.ListBlocks)
|
||||
m.Group("/{username}", func() {
|
||||
m.Get("", user.CheckUserBlock)
|
||||
m.Put("", user.BlockUser)
|
||||
m.Delete("", user.UnblockUser)
|
||||
}, context.UserAssignmentAPI())
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
|
||||
// Repositories (requires repo scope, org scope)
|
||||
@@ -1061,10 +1075,25 @@ func Routes() *web.Route {
|
||||
m.Post("/accept", repo.AcceptTransfer)
|
||||
m.Post("/reject", repo.RejectTransfer)
|
||||
}, reqToken())
|
||||
m.Group("/actions/secrets", func() {
|
||||
m.Combo("/{secretname}").
|
||||
Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret).
|
||||
Delete(reqToken(), reqOwner(), repo.DeleteSecret)
|
||||
m.Group("/actions", func() {
|
||||
m.Group("/secrets", func() {
|
||||
m.Combo("/{secretname}").
|
||||
Put(reqToken(), reqOwner(), bind(api.CreateOrUpdateSecretOption{}), repo.CreateOrUpdateSecret).
|
||||
Delete(reqToken(), reqOwner(), repo.DeleteSecret)
|
||||
})
|
||||
|
||||
m.Group("/variables", func() {
|
||||
m.Get("", reqToken(), reqOwner(), repo.ListVariables)
|
||||
m.Combo("/{variablename}").
|
||||
Get(reqToken(), reqOwner(), repo.GetVariable).
|
||||
Delete(reqToken(), reqOwner(), repo.DeleteVariable).
|
||||
Post(reqToken(), reqOwner(), bind(api.CreateVariableOption{}), repo.CreateVariable).
|
||||
Put(reqToken(), reqOwner(), bind(api.UpdateVariableOption{}), repo.UpdateVariable)
|
||||
})
|
||||
|
||||
m.Group("/runners", func() {
|
||||
m.Get("/registration-token", reqToken(), reqOwner(), repo.GetRegistrationToken)
|
||||
})
|
||||
})
|
||||
m.Group("/hooks/git", func() {
|
||||
m.Combo("").Get(repo.ListGitHooks)
|
||||
@@ -1156,9 +1185,9 @@ func Routes() *web.Route {
|
||||
m.Get("/subscribers", repo.ListSubscribers)
|
||||
m.Group("/subscription", func() {
|
||||
m.Get("", user.IsWatching)
|
||||
m.Put("", reqToken(), user.Watch)
|
||||
m.Delete("", reqToken(), user.Unwatch)
|
||||
})
|
||||
m.Put("", user.Watch)
|
||||
m.Delete("", user.Unwatch)
|
||||
}, reqToken())
|
||||
m.Group("/releases", func() {
|
||||
m.Combo("").Get(repo.ListReleases).
|
||||
Post(reqToken(), reqRepoWriter(unit.TypeReleases), context.ReferencesGitRepo(), bind(api.CreateReleaseOption{}), repo.CreateRelease)
|
||||
@@ -1181,13 +1210,13 @@ func Routes() *web.Route {
|
||||
Delete(reqToken(), reqRepoWriter(unit.TypeReleases), repo.DeleteReleaseByTag)
|
||||
})
|
||||
}, reqRepoReader(unit.TypeReleases))
|
||||
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), repo.MirrorSync)
|
||||
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), repo.PushMirrorSync)
|
||||
m.Post("/mirror-sync", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.MirrorSync)
|
||||
m.Post("/push_mirrors-sync", reqAdmin(), reqToken(), mustNotBeArchived, repo.PushMirrorSync)
|
||||
m.Group("/push_mirrors", func() {
|
||||
m.Combo("").Get(repo.ListPushMirrors).
|
||||
Post(bind(api.CreatePushMirrorOption{}), repo.AddPushMirror)
|
||||
Post(mustNotBeArchived, bind(api.CreatePushMirrorOption{}), repo.AddPushMirror)
|
||||
m.Combo("/{name}").
|
||||
Delete(repo.DeletePushMirrorByRemoteName).
|
||||
Delete(mustNotBeArchived, repo.DeletePushMirrorByRemoteName).
|
||||
Get(repo.GetPushMirrorByName)
|
||||
}, reqAdmin(), reqToken())
|
||||
|
||||
@@ -1225,6 +1254,7 @@ func Routes() *web.Route {
|
||||
Delete(bind(api.PullReviewRequestOptions{}), repo.DeleteReviewRequests).
|
||||
Post(bind(api.PullReviewRequestOptions{}), repo.CreateReviewRequests)
|
||||
})
|
||||
m.Get("/{base}/*", repo.GetPullRequestByBaseHead)
|
||||
}, mustAllowPulls, reqRepoReader(unit.TypeCode), context.ReferencesGitRepo())
|
||||
m.Group("/statuses", func() {
|
||||
m.Combo("/{sha}").Get(repo.GetCommitStatuses).
|
||||
@@ -1235,6 +1265,7 @@ func Routes() *web.Route {
|
||||
m.Group("/{ref}", func() {
|
||||
m.Get("/status", repo.GetCombinedCommitStatusByRef)
|
||||
m.Get("/statuses", repo.GetCommitStatusesByRef)
|
||||
m.Get("/pull", repo.GetCommitPullRequest)
|
||||
}, context.ReferencesGitRepo())
|
||||
}, reqRepoReader(unit.TypeCode))
|
||||
m.Group("/git", func() {
|
||||
@@ -1298,8 +1329,8 @@ func Routes() *web.Route {
|
||||
m.Group("/{username}/{reponame}", func() {
|
||||
m.Group("/issues", func() {
|
||||
m.Combo("").Get(repo.ListIssues).
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), repo.CreateIssue)
|
||||
m.Get("/pinned", repo.ListPinnedIssues)
|
||||
Post(reqToken(), mustNotBeArchived, bind(api.CreateIssueOption{}), reqRepoReader(unit.TypeIssues), repo.CreateIssue)
|
||||
m.Get("/pinned", reqRepoReader(unit.TypeIssues), repo.ListPinnedIssues)
|
||||
m.Group("/comments", func() {
|
||||
m.Get("", repo.ListRepoIssueComments)
|
||||
m.Group("/{id}", func() {
|
||||
@@ -1413,14 +1444,14 @@ func Routes() *web.Route {
|
||||
m.Get("/files", reqToken(), packages.ListPackageFiles)
|
||||
})
|
||||
m.Get("/", reqToken(), packages.ListPackages)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context_service.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryPackage), context.UserAssignmentAPI(), context.PackageAssignmentAPI(), reqPackageAccess(perm.AccessModeRead))
|
||||
|
||||
// Organizations
|
||||
m.Get("/user/orgs", reqToken(), tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), org.ListMyOrgs)
|
||||
m.Group("/users/{username}/orgs", func() {
|
||||
m.Get("", reqToken(), org.ListUserOrgs)
|
||||
m.Get("/{org}/permissions", reqToken(), org.GetUserOrgsPermissions)
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context_service.UserAssignmentAPI())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser, auth_model.AccessTokenScopeCategoryOrganization), context.UserAssignmentAPI())
|
||||
m.Post("/orgs", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), reqToken(), bind(api.CreateOrgOption{}), org.Create)
|
||||
m.Get("/orgs", org.GetAll, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization))
|
||||
m.Group("/orgs/{org}", func() {
|
||||
@@ -1434,11 +1465,26 @@ func Routes() *web.Route {
|
||||
m.Combo("/{username}").Get(reqToken(), org.IsMember).
|
||||
Delete(reqToken(), reqOrgOwnership(), org.DeleteMember)
|
||||
})
|
||||
m.Group("/actions/secrets", func() {
|
||||
m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets)
|
||||
m.Combo("/{secretname}").
|
||||
Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret).
|
||||
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
|
||||
m.Group("/actions", func() {
|
||||
m.Group("/secrets", func() {
|
||||
m.Get("", reqToken(), reqOrgOwnership(), org.ListActionsSecrets)
|
||||
m.Combo("/{secretname}").
|
||||
Put(reqToken(), reqOrgOwnership(), bind(api.CreateOrUpdateSecretOption{}), org.CreateOrUpdateSecret).
|
||||
Delete(reqToken(), reqOrgOwnership(), org.DeleteSecret)
|
||||
})
|
||||
|
||||
m.Group("/variables", func() {
|
||||
m.Get("", reqToken(), reqOrgOwnership(), org.ListVariables)
|
||||
m.Combo("/{variablename}").
|
||||
Get(reqToken(), reqOrgOwnership(), org.GetVariable).
|
||||
Delete(reqToken(), reqOrgOwnership(), org.DeleteVariable).
|
||||
Post(reqToken(), reqOrgOwnership(), bind(api.CreateVariableOption{}), org.CreateVariable).
|
||||
Put(reqToken(), reqOrgOwnership(), bind(api.UpdateVariableOption{}), org.UpdateVariable)
|
||||
})
|
||||
|
||||
m.Group("/runners", func() {
|
||||
m.Get("/registration-token", reqToken(), reqOrgOwnership(), org.GetRegistrationToken)
|
||||
})
|
||||
})
|
||||
m.Group("/public_members", func() {
|
||||
m.Get("", org.ListPublicMembers)
|
||||
@@ -1447,10 +1493,10 @@ func Routes() *web.Route {
|
||||
Delete(reqToken(), reqOrgMembership(), org.ConcealMember)
|
||||
})
|
||||
m.Group("/teams", func() {
|
||||
m.Get("", reqToken(), org.ListTeams)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||
m.Get("/search", reqToken(), org.SearchTeam)
|
||||
}, reqOrgMembership())
|
||||
m.Get("", org.ListTeams)
|
||||
m.Post("", reqOrgOwnership(), bind(api.CreateTeamOption{}), org.CreateTeam)
|
||||
m.Get("/search", org.SearchTeam)
|
||||
}, reqToken(), reqOrgMembership())
|
||||
m.Group("/labels", func() {
|
||||
m.Get("", org.ListLabels)
|
||||
m.Post("", reqToken(), reqOrgOwnership(), bind(api.CreateLabelOption{}), org.CreateLabel)
|
||||
@@ -1470,6 +1516,15 @@ func Routes() *web.Route {
|
||||
m.Delete("", org.DeleteAvatar)
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
|
||||
|
||||
m.Group("/blocks", func() {
|
||||
m.Get("", org.ListBlocks)
|
||||
m.Group("/{username}", func() {
|
||||
m.Get("", org.CheckUserBlock)
|
||||
m.Put("", org.BlockUser)
|
||||
m.Delete("", org.UnblockUser)
|
||||
})
|
||||
}, reqToken(), reqOrgOwnership())
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
|
||||
m.Group("/teams/{teamid}", func() {
|
||||
m.Combo("").Get(reqToken(), org.GetTeam).
|
||||
@@ -1512,7 +1567,10 @@ func Routes() *web.Route {
|
||||
m.Post("/orgs", bind(api.CreateOrgOption{}), admin.CreateOrg)
|
||||
m.Post("/repos", bind(api.CreateRepoOption{}), admin.CreateRepo)
|
||||
m.Post("/rename", bind(api.RenameUserOption{}), admin.RenameUser)
|
||||
}, context_service.UserAssignmentAPI())
|
||||
m.Get("/badges", admin.ListUserBadges)
|
||||
m.Post("/badges", bind(api.UserBadgeOption{}), admin.AddUserBadges)
|
||||
m.Delete("/badges", bind(api.UserBadgeOption{}), admin.DeleteUserBadges)
|
||||
}, context.UserAssignmentAPI())
|
||||
})
|
||||
m.Group("/emails", func() {
|
||||
m.Get("", admin.GetAllEmails)
|
||||
@@ -1530,6 +1588,9 @@ func Routes() *web.Route {
|
||||
Patch(bind(api.EditHookOption{}), admin.EditHook).
|
||||
Delete(admin.DeleteHook)
|
||||
})
|
||||
m.Group("/runners", func() {
|
||||
m.Get("/registration-token", admin.GetRegistrationToken)
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryAdmin), reqToken(), reqSiteAdmin())
|
||||
|
||||
m.Group("/topics", func() {
|
||||
|
||||
@@ -6,11 +6,11 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Shows a list of all Gitignore templates
|
||||
|
||||
@@ -6,9 +6,9 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/options"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Returns a list of all License templates
|
||||
|
||||
@@ -6,12 +6,12 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/markup/markdown"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Markup render markup document to HTML
|
||||
|
||||
@@ -10,19 +10,19 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/contexttest"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
AppURL = "http://localhost:3000/"
|
||||
Repo = "gogits/gogs"
|
||||
AppSubURL = AppURL + Repo + "/"
|
||||
AppURL = "http://localhost:3000/"
|
||||
Repo = "gogits/gogs"
|
||||
FullURL = AppURL + Repo + "/"
|
||||
)
|
||||
|
||||
func testRenderMarkup(t *testing.T, mode, filePath, text, responseBody string, responseCode int) {
|
||||
@@ -74,20 +74,20 @@ func TestAPI_RenderGFM(t *testing.T) {
|
||||
// rendered
|
||||
`<p>Wiki! Enjoy :)</p>
|
||||
<ul>
|
||||
<li><a href="` + AppSubURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||
<li><a href="` + AppSubURL + `wiki/Tips" rel="nofollow">Tips</a></li>
|
||||
<li><a href="` + FullURL + `wiki/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li>
|
||||
<li><a href="` + FullURL + `wiki/Tips" rel="nofollow">Tips</a></li>
|
||||
<li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="https://github.com/ocornut/imgui/issues/786" rel="nofollow">https://github.com/ocornut/imgui/issues/786</a></li>
|
||||
</ul>
|
||||
`,
|
||||
// Guard wiki sidebar: special syntax
|
||||
`[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`,
|
||||
// rendered
|
||||
`<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||
`<p><a href="` + FullURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p>
|
||||
`,
|
||||
// special syntax
|
||||
`[[Name|Link]]`,
|
||||
// rendered
|
||||
`<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p>
|
||||
`<p><a href="` + FullURL + `wiki/Link" rel="nofollow">Name</a></p>
|
||||
`,
|
||||
// empty
|
||||
``,
|
||||
@@ -111,8 +111,8 @@ Here are some links to the most important topics. You can find the full list of
|
||||
<p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p>
|
||||
<h2 id="user-content-quick-links">Quick Links</h2>
|
||||
<p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p>
|
||||
<p><a href="` + AppSubURL + `wiki/Configuration" rel="nofollow">Configuration</a>
|
||||
<a href="` + AppSubURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + AppSubURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
||||
<p><a href="` + FullURL + `wiki/Configuration" rel="nofollow">Configuration</a>
|
||||
<a href="` + FullURL + `wiki/raw/images/icon-bug.png" rel="nofollow"><img src="` + FullURL + `wiki/raw/images/icon-bug.png" title="icon-bug.png" alt="images/icon-bug.png"/></a></p>
|
||||
`,
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
const cacheKeyNodeInfoUsage = "API_NodeInfoUsage"
|
||||
@@ -29,10 +29,9 @@ func NodeInfo(ctx *context.APIContext) {
|
||||
|
||||
nodeInfoUsage := structs.NodeInfoUsage{}
|
||||
if setting.Federation.ShareUserStatistics {
|
||||
cached := false
|
||||
if setting.CacheService.Enabled {
|
||||
nodeInfoUsage, cached = ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage)
|
||||
}
|
||||
var cached bool
|
||||
nodeInfoUsage, cached = ctx.Cache.Get(cacheKeyNodeInfoUsage).(structs.NodeInfoUsage)
|
||||
|
||||
if !cached {
|
||||
usersTotal := int(user_model.CountUsers(ctx, nil))
|
||||
now := time.Now()
|
||||
@@ -53,11 +52,10 @@ func NodeInfo(ctx *context.APIContext) {
|
||||
LocalPosts: int(allIssues),
|
||||
LocalComments: int(allComments),
|
||||
}
|
||||
if setting.CacheService.Enabled {
|
||||
if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.Cache.Put(cacheKeyNodeInfoUsage, nodeInfoUsage, 180); err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// SigningKey returns the public key of the default signing key if it exists
|
||||
|
||||
@@ -6,9 +6,9 @@ package misc
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// Version shows the version of the Gitea server
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"strings"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// NewAvailable check if unread notifications exist
|
||||
@@ -21,7 +22,17 @@ func NewAvailable(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/NotificationCount"
|
||||
ctx.JSON(http.StatusOK, api.NotificationCount{New: activities_model.CountUnread(ctx, ctx.Doer.ID)})
|
||||
|
||||
total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
|
||||
UserID: ctx.Doer.ID,
|
||||
Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "db.Count[activities_model.Notification]", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, api.NotificationCount{New: total})
|
||||
}
|
||||
|
||||
func getFindNotificationOptions(ctx *context.APIContext) *activities_model.FindNotificationOptions {
|
||||
|
||||
@@ -9,9 +9,10 @@ import (
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
@@ -108,18 +109,18 @@ func ListRepoNotifications(ctx *context.APIContext) {
|
||||
}
|
||||
opts.RepoID = ctx.Repo.Repository.ID
|
||||
|
||||
totalCount, err := activities_model.CountNotifications(ctx, opts)
|
||||
totalCount, err := db.Count[activities_model.Notification](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
nl, err := activities_model.GetNotifications(ctx, opts)
|
||||
nl, err := db.Find[activities_model.Notification](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
err = nl.LoadAttributes(ctx)
|
||||
err = activities_model.NotificationList(nl).LoadAttributes(ctx)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -202,7 +203,7 @@ func ReadRepoNotifications(ctx *context.APIContext) {
|
||||
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
|
||||
log.Error("%v", opts.Status)
|
||||
}
|
||||
nl, err := activities_model.GetNotifications(ctx, opts)
|
||||
nl, err := db.Find[activities_model.Notification](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
@@ -68,18 +69,18 @@ func ListNotifications(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
totalCount, err := activities_model.CountNotifications(ctx, opts)
|
||||
totalCount, err := db.Count[activities_model.Notification](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
nl, err := activities_model.GetNotifications(ctx, opts)
|
||||
nl, err := db.Find[activities_model.Notification](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
err = nl.LoadAttributes(ctx)
|
||||
err = activities_model.NotificationList(nl).LoadAttributes(ctx)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -147,7 +148,7 @@ func ReadNotifications(ctx *context.APIContext) {
|
||||
statuses := ctx.FormStrings("status-types")
|
||||
opts.Status = statusStringsToNotificationStatuses(statuses, []string{"unread"})
|
||||
}
|
||||
nl, err := activities_model.GetNotifications(ctx, opts)
|
||||
nl, err := db.Find[activities_model.Notification](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2024 The Gitea Authors.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
func ListBlocks(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/blocks organization organizationListBlocks
|
||||
// ---
|
||||
// summary: List users blocked by the organization
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// produces:
|
||||
// - application/json
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/UserList"
|
||||
|
||||
shared.ListBlocks(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func CheckUserBlock(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/blocks/{username} organization organizationCheckUserBlock
|
||||
// ---
|
||||
// summary: Check if a user is blocked by the organization
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to check
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
shared.CheckUserBlock(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func BlockUser(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /orgs/{org}/blocks/{username} organization organizationBlockUser
|
||||
// ---
|
||||
// summary: Block a user
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to block
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: note
|
||||
// in: query
|
||||
// description: optional note for the block
|
||||
// type: string
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.BlockUser(ctx, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
|
||||
func UnblockUser(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /orgs/{org}/blocks/{username} organization organizationUnblockUser
|
||||
// ---
|
||||
// summary: Unblock a user
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: username
|
||||
// in: path
|
||||
// description: user to unblock
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
// "$ref": "#/responses/validationError"
|
||||
|
||||
shared.UnblockUser(ctx, ctx.Doer, ctx.Org.Organization.AsUser())
|
||||
}
|
||||
@@ -6,10 +6,10 @@ package org
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strings"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/label"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
@@ -318,7 +318,7 @@ func DeleteMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := models.RemoveOrgUser(ctx, ctx.Org.Organization.ID, member.ID); err != nil {
|
||||
if err := models.RemoveOrgUser(ctx, ctx.Org.Organization, member); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveOrgUser", err)
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
|
||||
+23
-24
@@ -12,13 +12,15 @@ import (
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/org"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
||||
@@ -30,14 +32,9 @@ func listUserOrgs(ctx *context.APIContext, u *user_model.User) {
|
||||
UserID: u.ID,
|
||||
IncludePrivate: showPrivate,
|
||||
}
|
||||
orgs, err := organization.FindOrgs(ctx, opts)
|
||||
orgs, maxResults, err := db.FindAndCount[organization.Organization](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindOrgs", err)
|
||||
return
|
||||
}
|
||||
maxResults, err := organization.CountOrgs(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CountOrgs", err)
|
||||
ctx.Error(http.StatusInternalServerError, "db.FindAndCount[organization.Organization]", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -342,28 +339,30 @@ func Edit(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/Organization"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditOrgOption)
|
||||
org := ctx.Org.Organization
|
||||
org.FullName = form.FullName
|
||||
org.Email = form.Email
|
||||
org.Description = form.Description
|
||||
org.Website = form.Website
|
||||
org.Location = form.Location
|
||||
if form.Visibility != "" {
|
||||
org.Visibility = api.VisibilityModes[form.Visibility]
|
||||
|
||||
if form.Email != "" {
|
||||
if err := user_service.ReplacePrimaryEmailAddress(ctx, ctx.Org.Organization.AsUser(), form.Email); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ReplacePrimaryEmailAddress", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if form.RepoAdminChangeTeamAccess != nil {
|
||||
org.RepoAdminChangeTeamAccess = *form.RepoAdminChangeTeamAccess
|
||||
|
||||
opts := &user_service.UpdateOptions{
|
||||
FullName: optional.Some(form.FullName),
|
||||
Description: optional.Some(form.Description),
|
||||
Website: optional.Some(form.Website),
|
||||
Location: optional.Some(form.Location),
|
||||
Visibility: optional.FromNonDefault(api.VisibilityModes[form.Visibility]),
|
||||
RepoAdminChangeTeamAccess: optional.FromPtr(form.RepoAdminChangeTeamAccess),
|
||||
}
|
||||
if err := user_model.UpdateUserCols(ctx, org.AsUser(),
|
||||
"full_name", "description", "website", "location",
|
||||
"visibility", "repo_admin_change_team_access",
|
||||
); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "EditOrganization", err)
|
||||
if err := user_service.UpdateUser(ctx, ctx.Org.Organization.AsUser(), opts); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, org))
|
||||
ctx.JSON(http.StatusOK, convert.ToOrganization(ctx, ctx.Org.Organization))
|
||||
}
|
||||
|
||||
// Delete an organization
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/routers/api/v1/shared"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#create-a-registration-token-for-an-organization
|
||||
|
||||
// GetRegistrationToken returns the token to register org runners
|
||||
func GetRegistrationToken(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/actions/runners/registration-token organization orgGetRunnerRegistrationToken
|
||||
// ---
|
||||
// summary: Get an organization's actions runner registration token
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/RegistrationToken"
|
||||
|
||||
shared.GetRegistrationToken(ctx, ctx.Org.Organization.ID, 0)
|
||||
}
|
||||
@@ -7,12 +7,13 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
secret_model "code.gitea.io/gitea/models/secret"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
secret_service "code.gitea.io/gitea/services/secrets"
|
||||
)
|
||||
|
||||
@@ -48,13 +49,7 @@ func ListActionsSecrets(ctx *context.APIContext) {
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
}
|
||||
|
||||
count, err := secret_model.CountSecrets(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
secrets, err := secret_model.FindSecrets(ctx, *opts)
|
||||
secrets, count, err := db.FindAndCount[secret_model.Secret](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -15,12 +15,13 @@ import (
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/user"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
org_service "code.gitea.io/gitea/services/org"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
@@ -486,6 +487,8 @@ func AddTeamMember(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
@@ -493,8 +496,12 @@ func AddTeamMember(ctx *context.APIContext) {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
if err := models.AddTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddMember", err)
|
||||
if err := models.AddTeamMember(ctx, ctx.Org.Team, u); err != nil {
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "AddTeamMember", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "AddTeamMember", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
ctx.Status(http.StatusNoContent)
|
||||
@@ -530,7 +537,7 @@ func RemoveTeamMember(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u.ID); err != nil {
|
||||
if err := models.RemoveTeamMember(ctx, ctx.Org.Team, u); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "RemoveTeamMember", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
// Copyright 2024 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package org
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// ListVariables list org-level variables
|
||||
func ListVariables(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/actions/variables organization getOrgVariablesList
|
||||
// ---
|
||||
// summary: Get an org-level variables list
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/VariableList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
|
||||
return
|
||||
}
|
||||
|
||||
variables := make([]*api.ActionVariable, len(vars))
|
||||
for i, v := range vars {
|
||||
variables[i] = &api.ActionVariable{
|
||||
OwnerID: v.OwnerID,
|
||||
RepoID: v.RepoID,
|
||||
Name: v.Name,
|
||||
Data: v.Data,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, variables)
|
||||
}
|
||||
|
||||
// GetVariable get an org-level variable
|
||||
func GetVariable(ctx *context.APIContext) {
|
||||
// swagger:operation GET /orgs/{org}/actions/variables/{variablename} organization getOrgVariable
|
||||
// ---
|
||||
// summary: Get an org-level variable
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ActionVariable"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
Name: ctx.Params("variablename"),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, "GetVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
variable := &api.ActionVariable{
|
||||
OwnerID: v.OwnerID,
|
||||
RepoID: v.RepoID,
|
||||
Name: v.Name,
|
||||
Data: v.Data,
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, variable)
|
||||
}
|
||||
|
||||
// DeleteVariable delete an org-level variable
|
||||
func DeleteVariable(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /orgs/{org}/actions/variables/{variablename} organization deleteOrgVariable
|
||||
// ---
|
||||
// summary: Delete an org-level variable
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ActionVariable"
|
||||
// "201":
|
||||
// description: response when deleting a variable
|
||||
// "204":
|
||||
// description: response when deleting a variable
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := actions_service.DeleteVariableByName(ctx, ctx.Org.Organization.ID, 0, ctx.Params("variablename")); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
|
||||
} else if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// CreateVariable create an org-level variable
|
||||
func CreateVariable(ctx *context.APIContext) {
|
||||
// swagger:operation POST /orgs/{org}/actions/variables/{variablename} organization createOrgVariable
|
||||
// ---
|
||||
// summary: Create an org-level variable
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateVariableOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// description: response when creating an org-level variable
|
||||
// "204":
|
||||
// description: response when creating an org-level variable
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opt := web.GetForm(ctx).(*api.CreateVariableOption)
|
||||
|
||||
ownerID := ctx.Org.Organization.ID
|
||||
variableName := ctx.Params("variablename")
|
||||
|
||||
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
||||
OwnerID: ownerID,
|
||||
Name: variableName,
|
||||
})
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
||||
return
|
||||
}
|
||||
if v != nil && v.ID > 0 {
|
||||
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := actions_service.CreateVariable(ctx, ownerID, 0, variableName, opt.Value); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateVariable update an org-level variable
|
||||
func UpdateVariable(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /orgs/{org}/actions/variables/{variablename} organization updateOrgVariable
|
||||
// ---
|
||||
// summary: Update an org-level variable
|
||||
// consumes:
|
||||
// - application/json
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: org
|
||||
// in: path
|
||||
// description: name of the organization
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UpdateVariableOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// description: response when updating an org-level variable
|
||||
// "204":
|
||||
// description: response when updating an org-level variable
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
|
||||
|
||||
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
||||
OwnerID: ctx.Org.Organization.ID,
|
||||
Name: ctx.Params("variablename"),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, "GetVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if opt.Name == "" {
|
||||
opt.Name = ctx.Params("variablename")
|
||||
}
|
||||
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
)
|
||||
@@ -60,7 +60,7 @@ func ListPackages(ctx *context.APIContext) {
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages.Type(packageType),
|
||||
Name: packages.SearchValue{Value: query},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
IsInternal: optional.Some(false),
|
||||
Paginator: &listOptions,
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -7,10 +7,14 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
actions_model "code.gitea.io/gitea/models/actions"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
actions_service "code.gitea.io/gitea/services/actions"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
secret_service "code.gitea.io/gitea/services/secrets"
|
||||
)
|
||||
|
||||
@@ -127,3 +131,295 @@ func DeleteSecret(ctx *context.APIContext) {
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// GetVariable get a repo-level variable
|
||||
func GetVariable(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/variables/{variablename} repository getRepoVariable
|
||||
// ---
|
||||
// summary: Get a repo-level variable
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ActionVariable"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: ctx.Params("variablename"),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, "GetVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
variable := &api.ActionVariable{
|
||||
OwnerID: v.OwnerID,
|
||||
RepoID: v.RepoID,
|
||||
Name: v.Name,
|
||||
Data: v.Data,
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, variable)
|
||||
}
|
||||
|
||||
// DeleteVariable delete a repo-level variable
|
||||
func DeleteVariable(ctx *context.APIContext) {
|
||||
// swagger:operation DELETE /repos/{owner}/{repo}/actions/variables/{variablename} repository deleteRepoVariable
|
||||
// ---
|
||||
// summary: Delete a repo-level variable
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/ActionVariable"
|
||||
// "201":
|
||||
// description: response when deleting a variable
|
||||
// "204":
|
||||
// description: response when deleting a variable
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
if err := actions_service.DeleteVariableByName(ctx, 0, ctx.Repo.Repository.ID, ctx.Params("variablename")); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "DeleteVariableByName", err)
|
||||
} else if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, "DeleteVariableByName", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteVariableByName", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// CreateVariable create a repo-level variable
|
||||
func CreateVariable(ctx *context.APIContext) {
|
||||
// swagger:operation POST /repos/{owner}/{repo}/actions/variables/{variablename} repository createRepoVariable
|
||||
// ---
|
||||
// summary: Create a repo-level variable
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/CreateVariableOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// description: response when creating a repo-level variable
|
||||
// "204":
|
||||
// description: response when creating a repo-level variable
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opt := web.GetForm(ctx).(*api.CreateVariableOption)
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
variableName := ctx.Params("variablename")
|
||||
|
||||
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
||||
RepoID: repoID,
|
||||
Name: variableName,
|
||||
})
|
||||
if err != nil && !errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
||||
return
|
||||
}
|
||||
if v != nil && v.ID > 0 {
|
||||
ctx.Error(http.StatusConflict, "VariableNameAlreadyExists", util.NewAlreadyExistErrorf("variable name %s already exists", variableName))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := actions_service.CreateVariable(ctx, 0, repoID, variableName, opt.Value); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "CreateVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UpdateVariable update a repo-level variable
|
||||
func UpdateVariable(ctx *context.APIContext) {
|
||||
// swagger:operation PUT /repos/{owner}/{repo}/actions/variables/{variablename} repository updateRepoVariable
|
||||
// ---
|
||||
// summary: Update a repo-level variable
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: variablename
|
||||
// in: path
|
||||
// description: name of the variable
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: body
|
||||
// in: body
|
||||
// schema:
|
||||
// "$ref": "#/definitions/UpdateVariableOption"
|
||||
// responses:
|
||||
// "201":
|
||||
// description: response when updating a repo-level variable
|
||||
// "204":
|
||||
// description: response when updating a repo-level variable
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opt := web.GetForm(ctx).(*api.UpdateVariableOption)
|
||||
|
||||
v, err := actions_service.GetVariable(ctx, actions_model.FindVariablesOpts{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Name: ctx.Params("variablename"),
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
ctx.Error(http.StatusNotFound, "GetVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if opt.Name == "" {
|
||||
opt.Name = ctx.Params("variablename")
|
||||
}
|
||||
if _, err := actions_service.UpdateVariable(ctx, v.ID, opt.Name, opt.Value); err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
ctx.Error(http.StatusBadRequest, "UpdateVariable", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateVariable", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ListVariables list repo-level variables
|
||||
func ListVariables(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/actions/variables repository getRepoVariablesList
|
||||
// ---
|
||||
// summary: Get repo-level variables list
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: name of the owner
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repository
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: page
|
||||
// in: query
|
||||
// description: page number of results to return (1-based)
|
||||
// type: integer
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: page size of results
|
||||
// type: integer
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/VariableList"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
vars, count, err := db.FindAndCount[actions_model.ActionVariable](ctx, &actions_model.FindVariablesOpts{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "FindVariables", err)
|
||||
return
|
||||
}
|
||||
|
||||
variables := make([]*api.ActionVariable, len(vars))
|
||||
for i, v := range vars {
|
||||
variables[i] = &api.ActionVariable{
|
||||
OwnerID: v.OwnerID,
|
||||
RepoID: v.RepoID,
|
||||
Name: v.Name,
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.JSON(http.StatusOK, variables)
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
|
||||
|
||||
@@ -10,16 +10,18 @@ import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
pull_service "code.gitea.io/gitea/services/pull"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
@@ -137,9 +139,9 @@ func DeleteBranch(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
// check whether branches of this repository has been synced
|
||||
totalNumOfBranches, err := git_model.CountBranches(ctx, git_model.FindBranchOptions{
|
||||
totalNumOfBranches, err := db.Count[git_model.Branch](ctx, git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: util.OptionalBoolFalse,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CountBranches", err)
|
||||
@@ -251,12 +253,11 @@ func CreateBranch(ctx *context.APIContext) {
|
||||
}
|
||||
}
|
||||
|
||||
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName)
|
||||
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, oldCommit.ID.String(), opt.BranchName)
|
||||
if err != nil {
|
||||
if git_model.IsErrBranchNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
|
||||
}
|
||||
if models.IsErrTagAlreadyExists(err) {
|
||||
} else if models.IsErrTagAlreadyExists(err) {
|
||||
ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.")
|
||||
} else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
|
||||
ctx.Error(http.StatusConflict, "", "The branch already exists.")
|
||||
@@ -339,10 +340,10 @@ func ListBranches(ctx *context.APIContext) {
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
ListOptions: listOptions,
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: util.OptionalBoolFalse,
|
||||
IsDeletedBranch: optional.Some(false),
|
||||
}
|
||||
var err error
|
||||
totalNumOfBranches, err = git_model.CountBranches(ctx, branchOpts)
|
||||
totalNumOfBranches, err = db.Count[git_model.Branch](ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CountBranches", err)
|
||||
return
|
||||
@@ -361,7 +362,7 @@ func ListBranches(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
branches, err := git_model.FindBranches(ctx, branchOpts)
|
||||
branches, err := db.Find[git_model.Branch](ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetBranches", err)
|
||||
return
|
||||
@@ -615,6 +616,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
BlockOnRejectedReviews: form.BlockOnRejectedReviews,
|
||||
BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
|
||||
DismissStaleApprovals: form.DismissStaleApprovals,
|
||||
IgnoreStaleApprovals: form.IgnoreStaleApprovals,
|
||||
RequireSignedCommits: form.RequireSignedCommits,
|
||||
ProtectedFilePatterns: form.ProtectedFilePatterns,
|
||||
UnprotectedFilePatterns: form.UnprotectedFilePatterns,
|
||||
@@ -642,7 +644,7 @@ func CreateBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
if !isPlainRule {
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
|
||||
ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
@@ -786,6 +788,10 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
protectBranch.DismissStaleApprovals = *form.DismissStaleApprovals
|
||||
}
|
||||
|
||||
if form.IgnoreStaleApprovals != nil {
|
||||
protectBranch.IgnoreStaleApprovals = *form.IgnoreStaleApprovals
|
||||
}
|
||||
|
||||
if form.RequireSignedCommits != nil {
|
||||
protectBranch.RequireSignedCommits = *form.RequireSignedCommits
|
||||
}
|
||||
@@ -915,7 +921,7 @@ func EditBranchProtection(ctx *context.APIContext) {
|
||||
} else {
|
||||
if !isPlainRule {
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
|
||||
ctx.Repo.GitRepo, err = gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
@@ -53,13 +53,10 @@ func ListCollaborators(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
count, err := repo_model.CountCollaborators(ctx, ctx.Repo.Repository.ID)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
collaborators, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, utils.GetListOptions(ctx))
|
||||
collaborators, total, err := repo_model.GetCollaborators(ctx, &repo_model.FindCollaborationOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "ListCollaborators", err)
|
||||
return
|
||||
@@ -70,7 +67,7 @@ func ListCollaborators(ctx *context.APIContext) {
|
||||
users[i] = convert.ToUser(ctx, collaborator.User, ctx.Doer)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(count)
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
@@ -156,6 +153,8 @@ func AddCollaborator(ctx *context.APIContext) {
|
||||
// responses:
|
||||
// "204":
|
||||
// "$ref": "#/responses/empty"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "422":
|
||||
@@ -179,7 +178,11 @@ func AddCollaborator(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if err := repo_module.AddCollaborator(ctx, ctx.Repo.Repository, collaborator); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "AddCollaborator", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "AddCollaborator", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -234,7 +237,7 @@ func DeleteCollaborator(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator.ID); err != nil {
|
||||
if err := repo_service.DeleteCollaboration(ctx, ctx.Repo.Repository, collaborator); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "DeleteCollaboration", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
@@ -205,7 +206,6 @@ func GetAllCommits(ctx *context.APIContext) {
|
||||
Not: not,
|
||||
Revision: []string{baseCommit.ID.String()},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetCommitsCount", err)
|
||||
return
|
||||
@@ -245,7 +245,6 @@ func GetAllCommits(ctx *context.APIContext) {
|
||||
Not: not,
|
||||
Page: listOptions.Page,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRange", err)
|
||||
return
|
||||
@@ -325,3 +324,53 @@ func DownloadCommitDiffOrPatch(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// GetCommitPullRequest returns the pull request of the commit
|
||||
func GetCommitPullRequest(ctx *context.APIContext) {
|
||||
// swagger:operation GET /repos/{owner}/{repo}/commits/{sha}/pull repository repoGetCommitPullRequest
|
||||
// ---
|
||||
// summary: Get the pull request of the commit
|
||||
// produces:
|
||||
// - application/json
|
||||
// parameters:
|
||||
// - name: owner
|
||||
// in: path
|
||||
// description: owner of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: repo
|
||||
// in: path
|
||||
// description: name of the repo
|
||||
// type: string
|
||||
// required: true
|
||||
// - name: sha
|
||||
// in: path
|
||||
// description: SHA of the commit to get
|
||||
// type: string
|
||||
// required: true
|
||||
// responses:
|
||||
// "200":
|
||||
// "$ref": "#/responses/PullRequest"
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
pr, err := issues_model.GetPullRequestByMergedCommit(ctx, ctx.Repo.Repository.ID, ctx.Params(":sha"))
|
||||
if err != nil {
|
||||
if issues_model.IsErrPullRequestNotExist(err) {
|
||||
ctx.Error(http.StatusNotFound, "GetPullRequestByMergedCommit", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "GetPullRequestByIndex", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadBaseRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadBaseRepo", err)
|
||||
return
|
||||
}
|
||||
if err = pr.LoadHeadRepo(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadHeadRepo", err)
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIPullRequest(ctx, pr, ctx.Doer))
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
git_model "code.gitea.io/gitea/models/git"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/gitrepo"
|
||||
"code.gitea.io/gitea/modules/httpcache"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/common"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
archiver_service "code.gitea.io/gitea/services/repository/archiver"
|
||||
files_service "code.gitea.io/gitea/services/repository/files"
|
||||
)
|
||||
@@ -144,7 +145,7 @@ func GetRawFileOrLFS(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
|
||||
// OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
|
||||
dataRc, err := blob.DataAsync()
|
||||
if err != nil {
|
||||
ctx.ServerError("DataAsync", err)
|
||||
@@ -279,9 +280,8 @@ func GetArchive(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
repoPath := repo_model.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
|
||||
if ctx.Repo.GitRepo == nil {
|
||||
gitRepo, err := git.OpenRepository(ctx, repoPath)
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||
return
|
||||
@@ -408,7 +408,7 @@ func canReadFiles(r *context.Repository) bool {
|
||||
return r.Permission.CanRead(unit.TypeCode)
|
||||
}
|
||||
|
||||
func base64Reader(s string) (io.Reader, error) {
|
||||
func base64Reader(s string) (io.ReadSeeker, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -655,6 +655,7 @@ func UpdateFile(ctx *context.APIContext) {
|
||||
apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
|
||||
if ctx.Repo.Repository.IsEmpty {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
|
||||
return
|
||||
}
|
||||
|
||||
if apiOpts.BranchName == "" {
|
||||
@@ -762,13 +763,13 @@ func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.Ch
|
||||
}
|
||||
message := ""
|
||||
if len(createFiles) != 0 {
|
||||
message += ctx.Tr("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
|
||||
message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
|
||||
}
|
||||
if len(updateFiles) != 0 {
|
||||
message += ctx.Tr("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
|
||||
message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
|
||||
}
|
||||
if len(deleteFiles) != 0 {
|
||||
message += ctx.Tr("repo.editor.delete", strings.Join(deleteFiles, ", "))
|
||||
message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
|
||||
}
|
||||
return strings.Trim(message, "\n")
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
@@ -149,6 +149,8 @@ func CreateFork(ctx *context.APIContext) {
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrAlreadyExist) || repo_model.IsErrReachLimitOfRepo(err) {
|
||||
ctx.Error(http.StatusConflict, "ForkRepository", err)
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "ForkRepository", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "ForkRepository", err)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -7,10 +7,10 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
// GetGitAllRefs get ref or an list all the refs of a repository
|
||||
|
||||
@@ -7,16 +7,17 @@ package repo
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
webhook_service "code.gitea.io/gitea/services/webhook"
|
||||
)
|
||||
@@ -58,13 +59,7 @@ func ListHooks(ctx *context.APIContext) {
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
}
|
||||
|
||||
count, err := webhook.CountWebhooksByOpts(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
hooks, err := webhook.ListWebhooksByOpts(ctx, opts)
|
||||
hooks, count, err := db.FindAndCount[webhook.Webhook](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/models/webhook"
|
||||
"code.gitea.io/gitea/modules/contexttest"
|
||||
"code.gitea.io/gitea/services/contexttest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
@@ -18,14 +19,14 @@ import (
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
issue_indexer "code.gitea.io/gitea/modules/indexer/issues"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
notify_service "code.gitea.io/gitea/services/notify"
|
||||
@@ -122,14 +123,14 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed util.OptionalBool
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = util.OptionalBoolTrue
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = util.OptionalBoolNone
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = util.OptionalBoolFalse
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -142,7 +143,7 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
Private: false,
|
||||
AllPublic: true,
|
||||
TopicOnly: false,
|
||||
Collaborate: util.OptionalBoolNone,
|
||||
Collaborate: optional.None[bool](),
|
||||
// This needs to be a column that is not nil in fixtures or
|
||||
// MySQL will return different results when sorting by null in some cases
|
||||
OrderBy: db.SearchOrderByAlphabetically,
|
||||
@@ -165,7 +166,7 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
opts.OwnerID = owner.ID
|
||||
opts.AllLimited = false
|
||||
opts.AllPublic = false
|
||||
opts.Collaborate = util.OptionalBoolFalse
|
||||
opts.Collaborate = optional.Some(false)
|
||||
}
|
||||
if ctx.FormString("team") != "" {
|
||||
if ctx.FormString("owner") == "" {
|
||||
@@ -204,14 +205,14 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
keyword = ""
|
||||
}
|
||||
|
||||
var isPull util.OptionalBool
|
||||
var isPull optional.Option[bool]
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = util.OptionalBoolFalse
|
||||
isPull = optional.Some(false)
|
||||
default:
|
||||
isPull = util.OptionalBoolNone
|
||||
isPull = optional.None[bool]()
|
||||
}
|
||||
|
||||
var includedAnyLabels []int64
|
||||
@@ -268,28 +269,28 @@ func SearchIssues(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if since != 0 {
|
||||
searchOpt.UpdatedAfterUnix = &since
|
||||
searchOpt.UpdatedAfterUnix = optional.Some(since)
|
||||
}
|
||||
if before != 0 {
|
||||
searchOpt.UpdatedBeforeUnix = &before
|
||||
searchOpt.UpdatedBeforeUnix = optional.Some(before)
|
||||
}
|
||||
|
||||
if ctx.IsSigned {
|
||||
ctxUserID := ctx.Doer.ID
|
||||
if ctx.FormBool("created") {
|
||||
searchOpt.PosterID = &ctxUserID
|
||||
searchOpt.PosterID = optional.Some(ctxUserID)
|
||||
}
|
||||
if ctx.FormBool("assigned") {
|
||||
searchOpt.AssigneeID = &ctxUserID
|
||||
searchOpt.AssigneeID = optional.Some(ctxUserID)
|
||||
}
|
||||
if ctx.FormBool("mentioned") {
|
||||
searchOpt.MentionID = &ctxUserID
|
||||
searchOpt.MentionID = optional.Some(ctxUserID)
|
||||
}
|
||||
if ctx.FormBool("review_requested") {
|
||||
searchOpt.ReviewRequestedID = &ctxUserID
|
||||
searchOpt.ReviewRequestedID = optional.Some(ctxUserID)
|
||||
}
|
||||
if ctx.FormBool("reviewed") {
|
||||
searchOpt.ReviewedID = &ctxUserID
|
||||
searchOpt.ReviewedID = optional.Some(ctxUserID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,7 +368,7 @@ func ListIssues(ctx *context.APIContext) {
|
||||
// required: false
|
||||
// - name: created_by
|
||||
// in: query
|
||||
// description: Only show items which were created by the the given user
|
||||
// description: Only show items which were created by the given user
|
||||
// type: string
|
||||
// - name: assigned_by
|
||||
// in: query
|
||||
@@ -396,14 +397,14 @@ func ListIssues(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var isClosed util.OptionalBool
|
||||
var isClosed optional.Option[bool]
|
||||
switch ctx.FormString("state") {
|
||||
case "closed":
|
||||
isClosed = util.OptionalBoolTrue
|
||||
isClosed = optional.Some(true)
|
||||
case "all":
|
||||
isClosed = util.OptionalBoolNone
|
||||
isClosed = optional.None[bool]()
|
||||
default:
|
||||
isClosed = util.OptionalBoolFalse
|
||||
isClosed = optional.Some(false)
|
||||
}
|
||||
|
||||
keyword := ctx.FormTrim("q")
|
||||
@@ -452,14 +453,30 @@ func ListIssues(ctx *context.APIContext) {
|
||||
|
||||
listOptions := utils.GetListOptions(ctx)
|
||||
|
||||
var isPull util.OptionalBool
|
||||
isPull := optional.None[bool]()
|
||||
switch ctx.FormString("type") {
|
||||
case "pulls":
|
||||
isPull = util.OptionalBoolTrue
|
||||
isPull = optional.Some(true)
|
||||
case "issues":
|
||||
isPull = util.OptionalBoolFalse
|
||||
default:
|
||||
isPull = util.OptionalBoolNone
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
|
||||
if isPull.Has() && !ctx.Repo.CanReadIssuesOrPulls(isPull.Value()) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !isPull.Has() {
|
||||
canReadIssues := ctx.Repo.CanRead(unit.TypeIssues)
|
||||
canReadPulls := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||
if !canReadIssues && !canReadPulls {
|
||||
ctx.NotFound()
|
||||
return
|
||||
} else if !canReadIssues {
|
||||
isPull = optional.Some(true)
|
||||
} else if !canReadPulls {
|
||||
isPull = optional.Some(false)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: we should be more efficient here
|
||||
@@ -485,10 +502,10 @@ func ListIssues(ctx *context.APIContext) {
|
||||
SortBy: issue_indexer.SortByCreatedDesc,
|
||||
}
|
||||
if since != 0 {
|
||||
searchOpt.UpdatedAfterUnix = &since
|
||||
searchOpt.UpdatedAfterUnix = optional.Some(since)
|
||||
}
|
||||
if before != 0 {
|
||||
searchOpt.UpdatedBeforeUnix = &before
|
||||
searchOpt.UpdatedBeforeUnix = optional.Some(before)
|
||||
}
|
||||
if len(labelIDs) == 1 && labelIDs[0] == 0 {
|
||||
searchOpt.NoLabelOnly = true
|
||||
@@ -509,13 +526,13 @@ func ListIssues(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if createdByID > 0 {
|
||||
searchOpt.PosterID = &createdByID
|
||||
searchOpt.PosterID = optional.Some(createdByID)
|
||||
}
|
||||
if assignedByID > 0 {
|
||||
searchOpt.AssigneeID = &assignedByID
|
||||
searchOpt.AssigneeID = optional.Some(assignedByID)
|
||||
}
|
||||
if mentionedByID > 0 {
|
||||
searchOpt.MentionID = &mentionedByID
|
||||
searchOpt.MentionID = optional.Some(mentionedByID)
|
||||
}
|
||||
|
||||
ids, total, err := issue_indexer.SearchIssues(ctx, searchOpt)
|
||||
@@ -593,6 +610,10 @@ func GetIssue(ctx *context.APIContext) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.JSON(http.StatusOK, convert.ToAPIIssue(ctx, issue))
|
||||
}
|
||||
|
||||
@@ -633,6 +654,7 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/validationError"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateIssueOption)
|
||||
var deadlineUnix timeutil.TimeStamp
|
||||
if form.Deadline != nil && ctx.Repo.CanWrite(unit.TypeIssues) {
|
||||
@@ -687,12 +709,14 @@ func CreateIssue(ctx *context.APIContext) {
|
||||
form.Labels = make([]int64, 0)
|
||||
}
|
||||
|
||||
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
|
||||
if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, 0); err != nil {
|
||||
if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
|
||||
ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err)
|
||||
return
|
||||
} else if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "NewIssue", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "NewIssue", err)
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, "NewIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -828,7 +852,11 @@ func EditIssue(ctx *context.APIContext) {
|
||||
|
||||
err = issue_service.UpdateAssignees(ctx, issue, oneAssignee, form.Assignees, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "UpdateAssignees", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateAssignees", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -844,10 +872,11 @@ func EditIssue(ctx *context.APIContext) {
|
||||
}
|
||||
if form.State != nil {
|
||||
if issue.IsPull {
|
||||
if pr, err := issue.GetPullRequest(ctx); err != nil {
|
||||
if err := issue.LoadPullRequest(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
|
||||
return
|
||||
} else if pr.HasMerged {
|
||||
}
|
||||
if issue.PullRequest.HasMerged {
|
||||
ctx.Error(http.StatusPreconditionFailed, "MergedPRState", "cannot change state of this pull request, it was already merged")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/attachment"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
|
||||
@@ -12,11 +12,13 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
@@ -71,6 +73,11 @@ func ListIssueComments(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "GetRawIssueByIndex", err)
|
||||
return
|
||||
}
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
issue.Repo = ctx.Repo.Repository
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
@@ -271,12 +278,27 @@ func ListRepoIssueComments(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var isPull optional.Option[bool]
|
||||
canReadIssue := ctx.Repo.CanRead(unit.TypeIssues)
|
||||
canReadPull := ctx.Repo.CanRead(unit.TypePullRequests)
|
||||
if canReadIssue && canReadPull {
|
||||
isPull = optional.None[bool]()
|
||||
} else if canReadIssue {
|
||||
isPull = optional.Some(false)
|
||||
} else if canReadPull {
|
||||
isPull = optional.Some(true)
|
||||
} else {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
opts := &issues_model.FindCommentsOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
Type: issues_model.CommentTypeComment,
|
||||
Since: since,
|
||||
Before: before,
|
||||
IsPull: isPull,
|
||||
}
|
||||
|
||||
comments, err := issues_model.FindComments(ctx, opts)
|
||||
@@ -301,10 +323,6 @@ func ListRepoIssueComments(ctx *context.APIContext) {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssues", err)
|
||||
return
|
||||
}
|
||||
if err := comments.LoadPosters(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadPosters", err)
|
||||
return
|
||||
}
|
||||
if err := comments.LoadAttachments(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttachments", err)
|
||||
return
|
||||
@@ -360,6 +378,7 @@ func CreateIssueComment(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.CreateIssueCommentOption)
|
||||
issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
|
||||
if err != nil {
|
||||
@@ -367,14 +386,23 @@ func CreateIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Tr("repo.issues.comment_on_locked")))
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", errors.New(ctx.Locale.TrString("repo.issues.comment_on_locked")))
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := issue_service.CreateIssueComment(ctx, ctx.Doer, ctx.Repo.Repository, issue, form.Body, nil)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "CreateIssueComment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -436,6 +464,11 @@ func GetIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Type != issues_model.CommentTypeComment {
|
||||
ctx.Status(http.StatusNoContent)
|
||||
return
|
||||
@@ -490,6 +523,7 @@ func EditIssueComment(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/notFound"
|
||||
// "423":
|
||||
// "$ref": "#/responses/repoArchivedError"
|
||||
|
||||
form := web.GetForm(ctx).(*api.EditIssueCommentOption)
|
||||
editIssueComment(ctx, *form)
|
||||
}
|
||||
@@ -555,7 +589,17 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
@@ -568,7 +612,11 @@ func editIssueComment(ctx *context.APIContext, form api.EditIssueCommentOption)
|
||||
oldContent := comment.Content
|
||||
comment.Content = form.Body
|
||||
if err := issue_service.UpdateComment(ctx, comment, ctx.Doer, oldContent); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "UpdateComment", err)
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "UpdateComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -658,7 +706,17 @@ func deleteIssueComment(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.IsAdmin()) {
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.IsSigned || (ctx.Doer.ID != comment.PosterID && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull)) {
|
||||
ctx.Status(http.StatusForbidden)
|
||||
return
|
||||
} else if comment.Type != issues_model.CommentTypeComment {
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/attachment"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
@@ -154,6 +156,8 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
||||
// "$ref": "#/responses/Attachment"
|
||||
// "400":
|
||||
// "$ref": "#/responses/error"
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
// "404":
|
||||
// "$ref": "#/responses/error"
|
||||
// "423":
|
||||
@@ -199,7 +203,11 @@ func CreateIssueCommentAttachment(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
if err = issue_service.UpdateComment(ctx, comment, ctx.Doer, comment.Content); err != nil {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
if errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, "UpdateComment", err)
|
||||
} else {
|
||||
ctx.ServerError("UpdateComment", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -329,6 +337,10 @@ func getIssueCommentSafe(ctx *context.APIContext) *issues_model.Comment {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
return nil
|
||||
}
|
||||
|
||||
comment.Issue.Repo = ctx.Repo.Repository
|
||||
|
||||
return comment
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
@@ -102,23 +102,24 @@ func GetIssueDependencies(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var lastRepoID int64
|
||||
var lastPerm access_model.Permission
|
||||
repoPerms := make(map[int64]access_model.Permission)
|
||||
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
|
||||
for _, blocker := range blockersInfo {
|
||||
// Get the permissions for this repository
|
||||
perm := lastPerm
|
||||
if lastRepoID != blocker.Repository.ID {
|
||||
if blocker.Repository.ID == ctx.Repo.Repository.ID {
|
||||
perm = ctx.Repo.Permission
|
||||
} else {
|
||||
var err error
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
// If the repo ID exists in the map, return the exist permissions
|
||||
// else get the permission and add it to the map
|
||||
var perm access_model.Permission
|
||||
existPerm, ok := repoPerms[blocker.RepoID]
|
||||
if ok {
|
||||
perm = existPerm
|
||||
} else {
|
||||
var err error
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, &blocker.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
lastRepoID = blocker.Repository.ID
|
||||
repoPerms[blocker.RepoID] = perm
|
||||
}
|
||||
|
||||
// check permission
|
||||
@@ -345,29 +346,31 @@ func GetIssueBlocks(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
var lastRepoID int64
|
||||
var lastPerm access_model.Permission
|
||||
|
||||
var issues []*issues_model.Issue
|
||||
|
||||
repoPerms := make(map[int64]access_model.Permission)
|
||||
repoPerms[ctx.Repo.Repository.ID] = ctx.Repo.Permission
|
||||
|
||||
for i, depMeta := range deps {
|
||||
if i < skip || i >= max {
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the permissions for this repository
|
||||
perm := lastPerm
|
||||
if lastRepoID != depMeta.Repository.ID {
|
||||
if depMeta.Repository.ID == ctx.Repo.Repository.ID {
|
||||
perm = ctx.Repo.Permission
|
||||
} else {
|
||||
var err error
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
// If the repo ID exists in the map, return the exist permissions
|
||||
// else get the permission and add it to the map
|
||||
var perm access_model.Permission
|
||||
existPerm, ok := repoPerms[depMeta.RepoID]
|
||||
if ok {
|
||||
perm = existPerm
|
||||
} else {
|
||||
var err error
|
||||
perm, err = access_model.GetUserRepoPermission(ctx, &depMeta.Repository, ctx.Doer)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetUserRepoPermission", err)
|
||||
return
|
||||
}
|
||||
lastRepoID = depMeta.Repository.ID
|
||||
repoPerms[depMeta.RepoID] = perm
|
||||
}
|
||||
|
||||
if !perm.CanReadIssuesOrPulls(depMeta.Issue.IsPull) {
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
@@ -240,18 +240,12 @@ func ListPinnedPullRequests(ctx *context.APIContext) {
|
||||
}
|
||||
|
||||
apiPrs := make([]*api.PullRequest, len(issues))
|
||||
if err := issues.LoadPullRequests(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadPullRequests", err)
|
||||
return
|
||||
}
|
||||
for i, currentIssue := range issues {
|
||||
pr, err := currentIssue.GetPullRequest(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetPullRequest", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = pr.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
pr := currentIssue.PullRequest
|
||||
if err = pr.LoadAttributes(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "LoadAttributes", err)
|
||||
return
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
issue_service "code.gitea.io/gitea/services/issue"
|
||||
)
|
||||
|
||||
// GetIssueCommentReactions list reactions of a comment from an issue
|
||||
@@ -61,6 +63,12 @@ func GetIssueCommentReactions(ctx *context.APIContext) {
|
||||
|
||||
if err := comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
@@ -190,9 +198,19 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
||||
return
|
||||
}
|
||||
|
||||
err = comment.LoadIssue(ctx)
|
||||
if err != nil {
|
||||
if err = comment.LoadIssue(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "comment.LoadIssue() failed", err)
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if !ctx.Repo.CanReadIssuesOrPulls(comment.Issue.IsPull) {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if comment.Issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(comment.Issue.IsPull) {
|
||||
@@ -202,9 +220,9 @@ func changeIssueCommentReaction(ctx *context.APIContext, form api.EditReactionOp
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueCommentReaction part
|
||||
reaction, err := issues_model.CreateCommentReaction(ctx, ctx.Doer.ID, comment.Issue.ID, comment.ID, form.Reaction)
|
||||
reaction, err := issue_service.CreateCommentReaction(ctx, ctx.Doer, comment, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
@@ -418,9 +436,9 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
||||
|
||||
if isCreateType {
|
||||
// PostIssueReaction part
|
||||
reaction, err := issues_model.CreateIssueReaction(ctx, ctx.Doer.ID, issue.ID, form.Reaction)
|
||||
reaction, err := issue_service.CreateIssueReaction(ctx, ctx.Doer, issue, form.Reaction)
|
||||
if err != nil {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) {
|
||||
if issues_model.IsErrForbiddenIssueReaction(err) || errors.Is(err, user_model.ErrBlockedUser) {
|
||||
ctx.Error(http.StatusForbidden, err.Error(), err)
|
||||
} else if issues_model.IsErrReactionAlreadyExist(err) {
|
||||
ctx.JSON(http.StatusOK, api.Reaction{
|
||||
@@ -429,7 +447,7 @@ func changeIssueReaction(ctx *context.APIContext, form api.EditReactionOption, i
|
||||
Created: reaction.CreatedUnix.AsTime(),
|
||||
})
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "CreateCommentReaction", err)
|
||||
ctx.Error(http.StatusInternalServerError, "CreateIssueReaction", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"net/http"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
@@ -83,20 +83,14 @@ func ListDeployKeys(ctx *context.APIContext) {
|
||||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
opts := &asymkey_model.ListDeployKeysOptions{
|
||||
opts := asymkey_model.ListDeployKeysOptions{
|
||||
ListOptions: utils.GetListOptions(ctx),
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
KeyID: ctx.FormInt64("key_id"),
|
||||
Fingerprint: ctx.FormString("fingerprint"),
|
||||
}
|
||||
|
||||
keys, err := asymkey_model.ListDeployKeys(ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
}
|
||||
|
||||
count, err := asymkey_model.CountDeployKeys(ctx, opts)
|
||||
keys, count, err := db.FindAndCount[asymkey_model.DeployKey](ctx, opts)
|
||||
if err != nil {
|
||||
ctx.InternalServerError(err)
|
||||
return
|
||||
@@ -159,6 +153,12 @@ func GetDeployKey(ctx *context.APIContext) {
|
||||
return
|
||||
}
|
||||
|
||||
// this check make it more consistent
|
||||
if key.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
|
||||
if err = key.GetContent(ctx); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetContent", err)
|
||||
return
|
||||
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"strconv"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/label"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/v1/utils"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
)
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"strconv"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
)
|
||||
|
||||
type languageResponse []*repo_model.LanguageStat
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
access_model "code.gitea.io/gitea/models/perm/access"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/graceful"
|
||||
"code.gitea.io/gitea/modules/lfs"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -26,6 +25,7 @@ import (
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/convert"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
"code.gitea.io/gitea/services/migrations"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user