0
0
Fork 0
mirror of https://github.com/go-gitea/gitea synced 2025-03-03 07:41:48 +01:00

Add API for changing Avatars ()

This adds an API for uploading and Deleting Avatars for of Users, Repos
and Organisations. I'm not sure, if this should also be added to the
Admin API.

Resolves 

---------

Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
JakobDev 2023-06-30 01:22:55 +02:00 committed by GitHub
parent 9fd63aaad1
commit 254a82842a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 666 additions and 1 deletions

View file

@ -380,3 +380,9 @@ type NewIssuePinsAllowed struct {
Issues bool `json:"issues"`
PullRequests bool `json:"pull_requests"`
}
// UpdateRepoAvatarUserOption options when updating the repo avatar
type UpdateRepoAvatarOption struct {
// image must be base64 encoded
Image string `json:"image" binding:"Required"`
}

View file

@ -102,3 +102,9 @@ type RenameUserOption struct {
// unique: true
NewName string `json:"new_username" binding:"Required"`
}
// UpdateUserAvatarUserOption options when updating the user avatar
type UpdateUserAvatarOption struct {
// image must be base64 encoded
Image string `json:"image" binding:"Required"`
}

View file

@ -899,6 +899,11 @@ func Routes() *web.Route {
Patch(bind(api.EditHookOption{}), user.EditHook).
Delete(user.DeleteHook)
}, reqWebhooksEnabled())
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), user.UpdateAvatar)
m.Delete("", user.DeleteAvatar)
}, reqToken())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
// Repositories (requires repo scope, org scope)
@ -1134,6 +1139,10 @@ func Routes() *web.Route {
m.Get("/languages", reqRepoReader(unit.TypeCode), repo.GetLanguages)
m.Get("/activities/feeds", repo.ListRepoActivityFeeds)
m.Get("/new_pin_allowed", repo.AreNewIssuePinsAllowed)
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateRepoAvatarOption{}), repo.UpdateAvatar)
m.Delete("", repo.DeleteAvatar)
}, reqAdmin(), reqToken())
}, repoAssignment())
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryRepository))
@ -1314,6 +1323,10 @@ func Routes() *web.Route {
Patch(bind(api.EditHookOption{}), org.EditHook).
Delete(org.DeleteHook)
}, reqToken(), reqOrgOwnership(), reqWebhooksEnabled())
m.Group("/avatar", func() {
m.Post("", bind(api.UpdateUserAvatarOption{}), org.UpdateAvatar)
m.Delete("", org.DeleteAvatar)
}, reqToken(), reqOrgOwnership())
m.Get("/activities/feeds", org.ListOrgActivityFeeds)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization), orgAssignment(true))
m.Group("/teams/{teamid}", func() {

View file

@ -0,0 +1,74 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package org
import (
"encoding/base64"
"net/http"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
user_service "code.gitea.io/gitea/services/user"
)
// UpdateAvatarupdates the Avatar of an Organisation
func UpdateAvatar(ctx *context.APIContext) {
// swagger:operation POST /orgs/{org}/avatar organization orgUpdateAvatar
// ---
// summary: Update Avatar
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateUserAvatarOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
form := web.GetForm(ctx).(*api.UpdateUserAvatarOption)
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.Error(http.StatusBadRequest, "DecodeImage", err)
return
}
err = user_service.UploadAvatar(ctx.Org.Organization.AsUser(), content)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAvatar", err)
}
ctx.Status(http.StatusNoContent)
}
// DeleteAvatar deletes the Avatar of an Organisation
func DeleteAvatar(ctx *context.APIContext) {
// swagger:operation DELETE /orgs/{org}/avatar organization orgDeleteAvatar
// ---
// summary: Delete Avatar
// produces:
// - application/json
// parameters:
// - name: org
// in: path
// description: name of the organization
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
err := user_service.DeleteAvatar(ctx.Org.Organization.AsUser())
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err)
}
ctx.Status(http.StatusNoContent)
}

View file

@ -0,0 +1,84 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package repo
import (
"encoding/base64"
"net/http"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
repo_service "code.gitea.io/gitea/services/repository"
)
// UpdateVatar updates the Avatar of an Repo
func UpdateAvatar(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/avatar repository repoUpdateAvatar
// ---
// summary: Update avatar
// 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: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateRepoAvatarOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
form := web.GetForm(ctx).(*api.UpdateRepoAvatarOption)
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.Error(http.StatusBadRequest, "DecodeImage", err)
return
}
err = repo_service.UploadAvatar(ctx, ctx.Repo.Repository, content)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAvatar", err)
}
ctx.Status(http.StatusNoContent)
}
// UpdateAvatar deletes the Avatar of an Repo
func DeleteAvatar(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/avatar repository repoDeleteAvatar
// ---
// summary: Delete avatar
// 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
// responses:
// "204":
// "$ref": "#/responses/empty"
err := repo_service.DeleteAvatar(ctx, ctx.Repo.Repository)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err)
}
ctx.Status(http.StatusNoContent)
}

View file

@ -181,4 +181,10 @@ type swaggerParameterBodies struct {
// in:body
CreatePushMirrorOption api.CreatePushMirrorOption
// in:body
UpdateUserAvatarOptions api.UpdateUserAvatarOption
// in:body
UpdateRepoAvatarOptions api.UpdateRepoAvatarOption
}

View file

@ -0,0 +1,63 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"encoding/base64"
"net/http"
"code.gitea.io/gitea/modules/context"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/web"
user_service "code.gitea.io/gitea/services/user"
)
// UpdateAvatar updates the Avatar of an User
func UpdateAvatar(ctx *context.APIContext) {
// swagger:operation POST /user/avatar user userUpdateAvatar
// ---
// summary: Update Avatar
// produces:
// - application/json
// parameters:
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/UpdateUserAvatarOption"
// responses:
// "204":
// "$ref": "#/responses/empty"
form := web.GetForm(ctx).(*api.UpdateUserAvatarOption)
content, err := base64.StdEncoding.DecodeString(form.Image)
if err != nil {
ctx.Error(http.StatusBadRequest, "DecodeImage", err)
return
}
err = user_service.UploadAvatar(ctx.Doer, content)
if err != nil {
ctx.Error(http.StatusInternalServerError, "UploadAvatar", err)
}
ctx.Status(http.StatusNoContent)
}
// DeleteAvatar deletes the Avatar of an User
func DeleteAvatar(ctx *context.APIContext) {
// swagger:operation DELETE /user/avatar user userDeleteAvatar
// ---
// summary: Delete Avatar
// produces:
// - application/json
// responses:
// "204":
// "$ref": "#/responses/empty"
err := user_service.DeleteAvatar(ctx.Doer)
if err != nil {
ctx.Error(http.StatusInternalServerError, "DeleteAvatar", err)
}
ctx.Status(http.StatusNoContent)
}

View file

@ -1595,6 +1595,63 @@
}
}
},
"/orgs/{org}/avatar": {
"post": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Update Avatar",
"operationId": "orgUpdateAvatar",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateUserAvatarOption"
}
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"organization"
],
"summary": "Delete Avatar",
"operationId": "orgDeleteAvatar",
"parameters": [
{
"type": "string",
"description": "name of the organization",
"name": "org",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
}
},
"/orgs/{org}/hooks": {
"get": {
"produces": [
@ -3174,6 +3231,77 @@
}
}
},
"/repos/{owner}/{repo}/avatar": {
"post": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Update avatar",
"operationId": "repoUpdateAvatar",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
},
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateRepoAvatarOption"
}
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"repository"
],
"summary": "Delete avatar",
"operationId": "repoDeleteAvatar",
"parameters": [
{
"type": "string",
"description": "owner of the repo",
"name": "owner",
"in": "path",
"required": true
},
{
"type": "string",
"description": "name of the repo",
"name": "repo",
"in": "path",
"required": true
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
}
},
"/repos/{owner}/{repo}/branch_protections": {
"get": {
"produces": [
@ -13787,6 +13915,47 @@
}
}
},
"/user/avatar": {
"post": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Update Avatar",
"operationId": "userUpdateAvatar",
"parameters": [
{
"name": "body",
"in": "body",
"schema": {
"$ref": "#/definitions/UpdateUserAvatarOption"
}
}
],
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
},
"delete": {
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Delete Avatar",
"operationId": "userDeleteAvatar",
"responses": {
"204": {
"$ref": "#/responses/empty"
}
}
}
},
"/user/emails": {
"get": {
"produces": [
@ -21548,6 +21717,30 @@
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"UpdateRepoAvatarOption": {
"description": "UpdateRepoAvatarUserOption options when updating the repo avatar",
"type": "object",
"properties": {
"image": {
"description": "image must be base64 encoded",
"type": "string",
"x-go-name": "Image"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"UpdateUserAvatarOption": {
"description": "UpdateUserAvatarUserOption options when updating the user avatar",
"type": "object",
"properties": {
"image": {
"description": "image must be base64 encoded",
"type": "string",
"x-go-name": "Image"
}
},
"x-go-package": "code.gitea.io/gitea/modules/structs"
},
"User": {
"description": "User represents a user",
"type": "object",
@ -22837,7 +23030,7 @@
"parameterBodies": {
"description": "parameterBodies",
"schema": {
"$ref": "#/definitions/CreatePushMirrorOption"
"$ref": "#/definitions/UpdateRepoAvatarOption"
}
},
"redirect": {

View file

@ -0,0 +1,72 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"net/http"
"os"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUpdateOrgAvatar(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
// Test what happens if you use a valid image
avatar, err := os.ReadFile("tests/integration/avatar.png")
assert.NoError(t, err)
if err != nil {
assert.FailNow(t, "Unable to open avatar.png")
}
opts := api.UpdateUserAvatarOption{
Image: base64.StdEncoding.EncodeToString(avatar),
}
req := NewRequestWithJSON(t, "POST", "/api/v1/orgs/user3/avatar?token="+token, &opts)
MakeRequest(t, req, http.StatusNoContent)
// Test what happens if you don't have a valid Base64 string
opts = api.UpdateUserAvatarOption{
Image: "Invalid",
}
req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/user3/avatar?token="+token, &opts)
MakeRequest(t, req, http.StatusBadRequest)
// Test what happens if you use a file that is not an image
text, err := os.ReadFile("tests/integration/README.md")
assert.NoError(t, err)
if err != nil {
assert.FailNow(t, "Unable to open README.md")
}
opts = api.UpdateUserAvatarOption{
Image: base64.StdEncoding.EncodeToString(text),
}
req = NewRequestWithJSON(t, "POST", "/api/v1/orgs/user3/avatar?token="+token, &opts)
MakeRequest(t, req, http.StatusInternalServerError)
}
func TestAPIDeleteOrgAvatar(t *testing.T) {
defer tests.PrepareTestEnv(t)()
session := loginUser(t, "user1")
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteOrganization)
req := NewRequest(t, "DELETE", "/api/v1/orgs/user3/avatar?token="+token)
MakeRequest(t, req, http.StatusNoContent)
}

View file

@ -0,0 +1,76 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"os"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUpdateRepoAvatar(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository)
// Test what happens if you use a valid image
avatar, err := os.ReadFile("tests/integration/avatar.png")
assert.NoError(t, err)
if err != nil {
assert.FailNow(t, "Unable to open avatar.png")
}
opts := api.UpdateRepoAvatarOption{
Image: base64.StdEncoding.EncodeToString(avatar),
}
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/avatar?token=%s", repo.OwnerName, repo.Name, token), &opts)
MakeRequest(t, req, http.StatusNoContent)
// Test what happens if you don't have a valid Base64 string
opts = api.UpdateRepoAvatarOption{
Image: "Invalid",
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/avatar?token=%s", repo.OwnerName, repo.Name, token), &opts)
MakeRequest(t, req, http.StatusBadRequest)
// Test what happens if you use a file that is not an image
text, err := os.ReadFile("tests/integration/README.md")
assert.NoError(t, err)
if err != nil {
assert.FailNow(t, "Unable to open README.md")
}
opts = api.UpdateRepoAvatarOption{
Image: base64.StdEncoding.EncodeToString(text),
}
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/avatar?token=%s", repo.OwnerName, repo.Name, token), &opts)
MakeRequest(t, req, http.StatusInternalServerError)
}
func TestAPIDeleteRepoAvatar(t *testing.T) {
defer tests.PrepareTestEnv(t)()
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
token := getUserToken(t, user2.LowerName, auth_model.AccessTokenScopeWriteRepository)
req := NewRequest(t, "DELETE", fmt.Sprintf("/api/v1/repos/%s/%s/avatar?token=%s", repo.OwnerName, repo.Name, token))
MakeRequest(t, req, http.StatusNoContent)
}

View file

@ -0,0 +1,72 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"net/http"
"os"
"testing"
auth_model "code.gitea.io/gitea/models/auth"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert"
)
func TestAPIUpdateUserAvatar(t *testing.T) {
defer tests.PrepareTestEnv(t)()
normalUsername := "user2"
session := loginUser(t, normalUsername)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
// Test what happens if you use a valid image
avatar, err := os.ReadFile("tests/integration/avatar.png")
assert.NoError(t, err)
if err != nil {
assert.FailNow(t, "Unable to open avatar.png")
}
// Test what happens if you don't have a valid Base64 string
opts := api.UpdateUserAvatarOption{
Image: base64.StdEncoding.EncodeToString(avatar),
}
req := NewRequestWithJSON(t, "POST", "/api/v1/user/avatar?token="+token, &opts)
MakeRequest(t, req, http.StatusNoContent)
opts = api.UpdateUserAvatarOption{
Image: "Invalid",
}
req = NewRequestWithJSON(t, "POST", "/api/v1/user/avatar?token="+token, &opts)
MakeRequest(t, req, http.StatusBadRequest)
// Test what happens if you use a file that is not an image
text, err := os.ReadFile("tests/integration/README.md")
assert.NoError(t, err)
if err != nil {
assert.FailNow(t, "Unable to open README.md")
}
opts = api.UpdateUserAvatarOption{
Image: base64.StdEncoding.EncodeToString(text),
}
req = NewRequestWithJSON(t, "POST", "/api/v1/user/avatar?token="+token, &opts)
MakeRequest(t, req, http.StatusInternalServerError)
}
func TestAPIDeleteUserAvatar(t *testing.T) {
defer tests.PrepareTestEnv(t)()
normalUsername := "user2"
session := loginUser(t, normalUsername)
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteUser)
req := NewRequest(t, "DELETE", "/api/v1/user/avatar?token="+token)
MakeRequest(t, req, http.StatusNoContent)
}

Binary file not shown.

After

(image error) Size: 7.6 KiB