Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions models/user/contributor_agreement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

// BLENDER: contributor agreement

package user

import (
"code.gitea.io/gitea/models/db"
"code.gitea.io/gitea/modules/timeutil"

"xorm.io/builder"
)

type ContributorAgreement struct {
ID int64 `xorm:"pk autoincr"`
Slug string `xorm:"UNIQUE"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
Content string `xorm:"TEXT NOT NULL"`
}

type SignedContributorAgreement struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(s)"`
ContributorAgreementID int64 `xorm:"UNIQUE(s)"`
CreatedUnix timeutil.TimeStamp `xorm:"created"`
Comment string `xorm:"TEXT DEFAULT NULL"`
}

type FindSignedContributorAgreementsOptions struct {
db.ListOptions
ContributorAgreementID int64
UserID int64
UserIDs []int64
}

func (opts *FindSignedContributorAgreementsOptions) ToConds() builder.Cond {
cond := builder.NewCond()
if opts.ContributorAgreementID != 0 {
cond = cond.And(builder.Eq{"contributor_agreement_id": opts.ContributorAgreementID})
}
if opts.UserID != 0 {
cond = cond.And(builder.Eq{"user_id": opts.UserID})
}
if opts.UserIDs != nil {
cond = cond.And(builder.In("user_id", opts.UserIDs))
}
return cond
}

func (opts *FindSignedContributorAgreementsOptions) ToOrders() string {
return "id DESC"
}

func init() {
// These tables don't exist in the upstream code.
// We don't introduce migrations for it to avoid migration id clashes.
// Gitea will create the tables in the database during startup,
// so no manual action is required until we start modifying the tables.
db.RegisterModel(new(ContributorAgreement))
db.RegisterModel(new(SignedContributorAgreement))
}
21 changes: 21 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,7 @@ account_link = Linked Accounts
organization = Organizations
uid = UID
webauthn = Two-Factor Authentication (Security Keys)
contributor_agreements = Contributor Agreements

public_profile = Public Profile
biography_placeholder = Tell us a little bit about yourself! (You can use Markdown)
Expand Down Expand Up @@ -3008,6 +3009,7 @@ last_page = Last
total = Total: %d
settings = Admin Settings
spamreports = Spam Reports
signed_contributor_agreements = Signed CLA

dashboard.new_version_hint = Gitea %s is now available, you are running %s. Check <a target="_blank" rel="noreferrer" href="%s">the blog</a> for more details.
dashboard.statistic = Summary
Expand Down Expand Up @@ -3180,6 +3182,18 @@ spamreports.created = Report Created
spamreports.updated = Report Updated
spamreports.status = Report Status

signed_contributor_agreements.all_option = All
signed_contributor_agreements.batch_sign = Batch sign contributor agreements
signed_contributor_agreements.batch_sign_button = Batch sign
signed_contributor_agreements.comment = Comment
signed_contributor_agreements.panel = Signed Contributor Agreements
signed_contributor_agreements.search_button = Search
signed_contributor_agreements.search_user = Search users
signed_contributor_agreements.signed_at = Signed At
signed_contributor_agreements.slug = CLA
signed_contributor_agreements.user = User
signed_contributor_agreements.usernames_input = Usernames, one per line

orgs.org_manage_panel = Organization Management
orgs.name = Name
orgs.teams = Teams
Expand Down Expand Up @@ -3947,3 +3961,10 @@ normal_file = Normal file
executable_file = Executable file
symbolic_link = Symbolic link
submodule = Submodule

[contributor_agreements]
signed_at = Signed by you at %s
confirm = I agree to the terms of the contributor agreement.
sign = Sign
view_and_sign = View text and sign
view = View text
2 changes: 2 additions & 0 deletions routers/api/v1/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,8 @@ func Routes() *web.Router {
}, reqSelfOrAdmin(), reqBasicOrRevProxyAuth())

m.Get("/activities/feeds", user.ListUserActivityFeeds)
// BLENDER: contributor agreement
m.Get("/contributor-agreements/{slug}", user.CheckContributorAgreement)
}, context.UserAssignmentAPI(), checkTokenPublicOnly(), individualPermsChecker)
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser))

Expand Down
83 changes: 83 additions & 0 deletions routers/api/v1/user/contributor_agreement.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

// BLENDER: contributor agreement

package user

import (
"fmt"
"net/http"
"strings"

"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/httplib"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/services/context"

"xorm.io/builder"
)

func CheckContributorAgreement(ctx *context.APIContext) {
// swagger:operation GET /users/{username}/contributor-agreements/{slug} user userCheckContributorAgreement
// ---
// summary: Check if contributor agreement is signed by the user
// produces:
// - application/json
// parameters:
// - name: username
// in: path
// description: username of user
// type: string
// required: true
// - name: slug
// in: path
// description: slug of a contributor agreement to check
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/string"
// "404":
// "$ref": "#/responses/notFound"

slug := ctx.PathParam("slug")
ca, exist, err := db.Get[user_model.ContributorAgreement](ctx, builder.Eq{"slug": slug})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !exist {
ctx.APIError(http.StatusNotFound, fmt.Sprintf("Contributor agreement <%s> is not found.", slug))
return
}
_, exist, err = db.Get[user_model.SignedContributorAgreement](ctx, builder.Eq{
"contributor_agreement_id": ca.ID,
"user_id": ctx.ContextUser.ID,
})
if err != nil {
ctx.APIErrorInternal(err)
return
}
if !exist {
url := httplib.MakeAbsoluteURL(ctx, setting.AppSubURL + "/user/settings/contributor_agreements")
message := []string{
fmt.Sprintf("Contributor agreement <%s> is not signed.", slug),
fmt.Sprintf("Sign at %s", url),
}
// Add a visible *** decoration.
maxlen := 0
for i := range message {
l := len(message[i])
if l > maxlen {
maxlen = l
}
}
hr := strings.Repeat("*", maxlen)
message = append(append([]string{"", "", hr}, message...), hr, "")
ctx.PlainText(http.StatusNotFound, strings.Join(message, "\n"))
return
}
ctx.PlainText(http.StatusOK, "OK")
}
Loading