Compare commits

...

13 Commits

Author SHA1 Message Date
hyung-hwan 37dfa6edc4 no request-wide transaction for some repo endpoints 2026-06-21 02:21:38 +09:00
hyung-hwan 8b0435f84b more transaction fix in repo and project creation/deletion 2026-06-21 02:16:06 +09:00
hyung-hwan 205c71e1f8 more trasaction fix 2026-06-21 02:08:51 +09:00
hyung-hwan 555f3dcd34 updated UploadFile for transaction 2026-06-21 01:55:23 +09:00
hyung-hwan 7ea5dfe058 updated login for transaction 2026-06-21 01:40:10 +09:00
hyung-hwan 9a7fac0a90 updated oidc callback for improved transaction handling 2026-06-21 00:39:31 +09:00
hyung-hwan 76b33e904d updated acme order finalization flow 2026-06-21 00:23:58 +09:00
hyung-hwan 3645cc14e0 compacted database migration files 2026-06-20 22:12:32 +09:00
hyung-hwan 72011a1221 defined constants for role names - admin, writer, viewer 2026-06-20 22:07:21 +09:00
hyung-hwan 54f0e75b1f no request-wide transaction for large streaming of rpm file, block attachment, pki cet bundle download 2026-06-20 21:55:41 +09:00
hyung-hwan 7f3a8b24ff no request-wide transaction for api.RepoRPMUpload and api.RepoRPMRebuildSubdirMetadata
defined some more constants and places them in models/constannts.go
2026-06-20 21:46:58 +09:00
hyung-hwan d54770112f no requeset-wide transaction for Upload/Download/Copy for a ssh session 2026-06-20 21:33:36 +09:00
hyung-hwan f7d94f7aa9 removed transaction acquisition for api.InspectSSHCertficate, api.DiscoverSSHServerHostKeyForSelf and api.DiscoverSSHServerHostKeyAdmin when registring them to the api routes 2026-06-20 21:22:55 +09:00
35 changed files with 963 additions and 650 deletions
+7 -13
View File
@@ -10,15 +10,9 @@ import "time"
import "codit/internal/models"
import "codit/internal/util"
const AuthProviderTypeDB = "db"
const AuthProviderTypeLDAP = "ldap"
const AuthProviderTypeOIDC = "oidc"
const BuiltinDBProviderPublicID = "db"
const GroupSyncModeOff string = "off"
const GroupSyncModeFirstLogin string = "first_login"
const GroupSyncModeSync string = "sync"
func scanAuthProvider(row interface {
Scan(dest ...any) error
@@ -81,9 +75,9 @@ func normalizeAuthProviderGroupSyncMode(value string) (string, error) {
mode = strings.TrimSpace(value)
if mode == "" {
return GroupSyncModeOff, nil
return models.GroupSyncModeOff, nil
}
if mode == GroupSyncModeOff || mode == GroupSyncModeFirstLogin || mode == GroupSyncModeSync {
if mode == models.GroupSyncModeOff || mode == models.GroupSyncModeFirstLogin || mode == models.GroupSyncModeSync {
return mode, nil
}
return "", errors.New("invalid group_sync_mode")
@@ -160,7 +154,7 @@ func insertAuthProviderGroupMappings(exec sqlExecutor, providerID string, mappin
FROM auth_providers p
JOIN user_groups g ON g.public_id = ?
WHERE p.public_id = ? AND g.scope = ?`,
claimValue, now, now, groupID, strings.TrimSpace(providerID), UserGroupScopeExplicit)
claimValue, now, now, groupID, strings.TrimSpace(providerID), models.UserGroupScopeExplicit)
if err != nil {
return err
}
@@ -287,10 +281,10 @@ func (s *Store) CreateAuthProvider(ctx context.Context, item models.AuthProvider
item.ID, err = util.NewID()
if err != nil { return item, err }
}
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
if item.Type == models.AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
item.LDAPUserFilter = "(uid={username})"
}
if item.Type == AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
if item.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
item.OIDCScopes = "openid profile email"
}
item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode)
@@ -343,10 +337,10 @@ func (s *Store) UpdateAuthProvider(ctx context.Context, item models.AuthProvider
now = time.Now().Unix()
item.UpdatedAt = now
if item.Type == AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
if item.Type == models.AuthProviderTypeLDAP && strings.TrimSpace(item.LDAPUserFilter) == "" {
item.LDAPUserFilter = "(uid={username})"
}
if item.Type == AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
if item.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(item.OIDCScopes) == "" {
item.OIDCScopes = "openid profile email"
}
item.GroupSyncMode, err = normalizeAuthProviderGroupSyncMode(item.GroupSyncMode)
+1 -3
View File
@@ -498,9 +498,7 @@ func (s *Store) CreateBlockAttachment(ctx context.Context, boardID, blockID stri
return item, err
}
err = commitIfOwned(tx, owned)
if err != nil {
return item, err
}
if err != nil { return item, err }
return item, nil
}
+11 -13
View File
@@ -9,16 +9,14 @@ import "time"
import "codit/internal/models"
import "codit/internal/util"
const UserGroupScopeExplicit string = "explicit"
const UserGroupScopeAllUsers string = "all_users"
func NormalizeUserGroupScope(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
switch value {
case UserGroupScopeAllUsers:
return UserGroupScopeAllUsers
case models.UserGroupScopeAllUsers:
return models.UserGroupScopeAllUsers
default:
return UserGroupScopeExplicit
return models.UserGroupScopeExplicit
}
}
@@ -100,7 +98,7 @@ func (s *Store) UpdateUserGroup(ctx context.Context, item models.UserGroup) (mod
item.Scope = NormalizeUserGroupScope(item.Scope)
tx, owned, err = s.beginImmediateContext(ctx)
if err != nil { return item, err }
if item.Scope != UserGroupScopeExplicit {
if item.Scope != models.UserGroupScopeExplicit {
err = tx.QueryRow(`SELECT COUNT(*)
FROM auth_provider_group_mappings m
JOIN user_groups g ON g.id = m.group_id
@@ -157,7 +155,7 @@ func (s *Store) DeleteUserGroup(ctx context.Context, groupID string) error {
// TODO: this part needs to be re-thought through. This is still user created group. If it can't be deleted, that's horrible
// above all creating another user group with this scope gives the unique constraint error... so Creating a user group
// should translate it to human friendly message.
if NormalizeUserGroupScope(scope) == UserGroupScopeAllUsers {
if NormalizeUserGroupScope(scope) == models.UserGroupScopeAllUsers {
rollbackIfOwned(tx, owned)
return errors.New("all users group cannot be deleted")
}
@@ -196,7 +194,7 @@ func (s *Store) ListUserGroupMembers(groupID string) ([]models.UserGroupMember,
if err != nil {
return nil, err
}
if NormalizeUserGroupScope(group.Scope) == UserGroupScopeAllUsers {
if NormalizeUserGroupScope(group.Scope) == models.UserGroupScopeAllUsers {
rows, err = s.Query(`SELECT ? AS group_public_id, u.public_id, u.username, u.display_name, 0 AS created_at
FROM users u
WHERE u.disabled = 0
@@ -242,7 +240,7 @@ func (s *Store) AddUserGroupMember(ctx context.Context, groupID string, userID s
rollbackIfOwned(tx, owned)
return err
}
if NormalizeUserGroupScope(scope) != UserGroupScopeExplicit {
if NormalizeUserGroupScope(scope) != models.UserGroupScopeExplicit {
rollbackIfOwned(tx, owned)
return errors.New("virtual groups do not accept explicit members")
}
@@ -272,7 +270,7 @@ func (s *Store) RemoveUserGroupMember(ctx context.Context, groupID string, userI
rollbackIfOwned(tx, owned)
return err
}
if NormalizeUserGroupScope(scope) != UserGroupScopeExplicit {
if NormalizeUserGroupScope(scope) != models.UserGroupScopeExplicit {
rollbackIfOwned(tx, owned)
return errors.New("virtual groups do not have explicit members")
}
@@ -433,13 +431,13 @@ func (s *Store) GetProjectRoleForPrincipal(projectID string, principalID string)
func projectRoleRank(value string) int {
var v string
v = strings.ToLower(strings.TrimSpace(value))
if v == "admin" {
if v == models.RoleAdmin {
return 3
}
if v == "writer" {
if v == models.RoleWriter {
return 2
}
if v == "viewer" {
if v == models.RoleViewer {
return 1
}
return 0
+4 -6
View File
@@ -8,8 +8,6 @@ import "time"
import "codit/internal/models"
import "codit/internal/util"
const RPMRepoModeLocal string = "local"
const RPMRepoModeMirror string = "mirror"
func (s *Store) ListRPMRepoDirs(repoID string) ([]models.RPMRepoDir, error) {
var rows *sql.Rows
@@ -74,7 +72,7 @@ func (s *Store) UpsertRPMRepoDir(item models.RPMRepoDir) error {
item.TLSInsecureSkipVerify,
item.SyncIntervalSec,
item.SyncEnabled,
NormalizeRPMRepoMode(item.Mode) == RPMRepoModeMirror,
NormalizeRPMRepoMode(item.Mode) == models.RPMRepoModeMirror,
int64(0),
now,
now)
@@ -524,10 +522,10 @@ func (s *Store) CleanupRPMMirrorRunsRetention(repoID string, path string, keepCo
func NormalizeRPMRepoMode(mode string) string {
var v string
v = strings.ToLower(strings.TrimSpace(mode))
if v == RPMRepoModeMirror {
return RPMRepoModeMirror
if v == models.RPMRepoModeMirror {
return models.RPMRepoModeMirror
}
return RPMRepoModeLocal
return models.RPMRepoModeLocal
}
func (s *Store) DeleteRPMRepoDir(repoID string, path string) error {
+1 -1
View File
@@ -513,7 +513,7 @@ func (s *Store) CreateProject(ctx context.Context, project models.Project) (mode
}
_, err = tx.Exec(`INSERT INTO project_role_bindings (project_id, subject_type, subject_id, role, created_at, updated_at)
VALUES ((SELECT id FROM projects WHERE public_id = ?), 'user', (SELECT id FROM users WHERE public_id = ?), ?, ?, ?)`,
project.ID, project.CreatedBy, "admin", project.CreatedAt, project.UpdatedAt)
project.ID, project.CreatedBy, models.RoleAdmin, project.CreatedAt, project.UpdatedAt)
if err != nil {
rollbackIfOwned(tx, owned)
return project, err
+106 -36
View File
@@ -21,6 +21,7 @@ import "time"
import "golang.org/x/crypto/acme"
import "codit/internal/db"
import "codit/internal/models"
type acmeProfileRequest struct {
@@ -466,7 +467,6 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
var certDER [][]byte
var certURL string
var cert models.PKICert
var existing models.PKICert
var certBlock *pem.Block
var leaf *x509.Certificate
var challenge *acme.Challenge
@@ -478,6 +478,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
var createdBySubjectID string
var createdBySubjectName string
var issuanceSource string
var persistErr error
var err error
if !api.requireAdmin(w, r) {
return
@@ -521,8 +522,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=account err=%v", order.ID, err)
order.Status = "failed"
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
_ = api.store(r).UpdateACMEProfileLastError(profile.ID, err.Error())
api.persistACMEFinalizeFailure(order, profile.ID, true)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -532,7 +532,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if err != nil {
order.Status = "failed"
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -548,8 +548,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
order.Status = "failed"
order.LastError = err.Error()
order.Challenges[i].Status = "failed"
_, _ = api.store(r).UpdateACMEOrder(order)
_ = api.store(r).UpdateACMEProfileLastError(profile.ID, err.Error())
api.persistACMEFinalizeFailure(order, profile.ID, true)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -561,17 +560,23 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
order.Status = "failed"
order.LastError = err.Error()
order.Challenges[i].Status = "failed"
_, _ = api.store(r).UpdateACMEOrder(order)
api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
// [NOTE]
// It may take a long time (30-120 seconds) for ACME CA to confirm DNS propagation.
// If the user closes the connection during this window, the wait is aborted. The
// upstream CA may have already verified the domain, but the order status on our
// side is still "pending". To recover this, you will have to Finalize order again.
authz, err = client.WaitAuthorization(r.Context(), order.Challenges[i].AuthorizationURL)
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "challenge wait failed order=%s domain=%s err=%v", order.ID, order.Challenges[i].Identifier, err)
order.Status = "failed"
order.LastError = err.Error()
order.Challenges[i].Status = "failed"
_, _ = api.store(r).UpdateACMEOrder(order)
api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -589,7 +594,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=create_cert err=%v", order.ID, err)
order.Status = "failed"
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -598,7 +603,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if err != nil {
order.Status = "failed"
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -607,7 +612,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if certBlock == nil {
order.Status = "failed"
order.LastError = "invalid issued certificate"
_, _ = api.store(r).UpdateACMEOrder(order)
api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": order.LastError})
return
}
@@ -615,7 +620,7 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
if err != nil {
order.Status = "failed"
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
api.persistACMEFinalizeFailure(order, profile.ID, false)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
}
@@ -640,46 +645,111 @@ func (api *API) FinalizeACMEOrder(w http.ResponseWriter, r *http.Request, params
RevocationReason: "",
}
replaceExisting = req.RenewMode == "override"
order.Status = "valid"
order.LastError = ""
order, cert, err = api.persistACMEFinalizeSuccess(order, profile.ID, cert, replaceExisting)
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=persist_success err=%v", order.ID, err)
order.Status = "failed"
order.LastError = err.Error()
persistErr = api.persistACMEFinalizeFailure(order, profile.ID, false)
if persistErr != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist failed id=%s err=%v", order.ID, persistErr)
}
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
_ = certURL
api.Logger.Write(acmeLogID, codit_logger.LOG_INFO, "order finalize success id=%s cert_id=%s cert_url=%s", order.ID, order.CertID, certURL)
WriteJSON(w, http.StatusOK, order)
}
func (api *API) persistACMEFinalizeFailure(order models.ACMEOrder, profileID string, updateProfileError bool) error {
var ctx context.Context
var cancel context.CancelFunc
var store *db.Store
var err error
ctx, cancel = context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
store, err = api.Store.BeginImmediateStore(ctx)
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist failed id=%s err=%v", order.ID, err)
return err
}
_, err = store.UpdateACMEOrder(order)
if err != nil {
_ = store.Rollback()
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist failed id=%s err=%v", order.ID, err)
return err
}
if updateProfileError {
err = store.UpdateACMEProfileLastError(profileID, order.LastError)
if err != nil {
_ = store.Rollback()
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize profile error persist failed id=%s profile=%s err=%v", order.ID, profileID, err)
return err
}
}
err = store.Commit()
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failure persist commit failed id=%s err=%v", order.ID, err)
return err
}
return nil
}
func (api *API) persistACMEFinalizeSuccess(order models.ACMEOrder, profileID string, cert models.PKICert, replaceExisting bool) (models.ACMEOrder, models.PKICert, error) {
var ctx context.Context
var cancel context.CancelFunc
var store *db.Store
var existing models.PKICert
var err error
ctx, cancel = context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
store, err = api.Store.BeginImmediateStore(ctx)
if err != nil {
return order, cert, err
}
if replaceExisting && strings.TrimSpace(order.CertID) != "" {
existing, err = api.store(r).GetPKICert(order.CertID)
existing, err = store.GetPKICert(order.CertID)
if err == nil {
cert.ID = existing.ID
cert.CAID = existing.CAID
cert, err = api.store(r).ReplacePKICert(cert)
cert, err = store.ReplacePKICert(cert)
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=replace_cert err=%v", order.ID, err)
order.Status = "failed"
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
_ = store.Rollback()
return order, cert, err
}
api.Logger.Write(acmeLogID, codit_logger.LOG_INFO, "order finalize replaced existing cert id=%s", cert.ID)
}
}
if strings.TrimSpace(cert.ID) == "" {
cert, err = api.store(r).CreatePKICert(cert)
cert, err = store.CreatePKICert(cert)
if err != nil {
api.Logger.Write(acmeLogID, codit_logger.LOG_WARN, "order finalize failed id=%s step=store_cert err=%v", order.ID, err)
order.Status = "failed"
order.LastError = err.Error()
_, _ = api.store(r).UpdateACMEOrder(order)
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
return
_ = store.Rollback()
return order, cert, err
}
}
order.Status = "valid"
order.LastError = ""
order.CertID = cert.ID
order, err = api.store(r).UpdateACMEOrder(order)
order, err = store.UpdateACMEOrder(order)
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
_ = store.Rollback()
return order, cert, err
}
_ = api.store(r).UpdateACMEProfileLastError(profile.ID, "")
_ = certURL
api.Logger.Write(acmeLogID, codit_logger.LOG_INFO, "order finalize success id=%s cert_id=%s cert_url=%s", order.ID, order.CertID, certURL)
WriteJSON(w, http.StatusOK, order)
err = store.UpdateACMEProfileLastError(profileID, "")
if err != nil {
_ = store.Rollback()
return order, cert, err
}
err = store.Commit()
if err != nil {
return order, cert, err
}
return order, cert, nil
}
func (api *API) DeleteACMEOrder(w http.ResponseWriter, r *http.Request, params map[string]string) {
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -52,11 +52,11 @@ func (api *API) CreateAuthProvider(w http.ResponseWriter, r *http.Request, _ map
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "name is required")
return
}
if req.Type != db.AuthProviderTypeLDAP && req.Type != db.AuthProviderTypeOIDC {
if req.Type != models.AuthProviderTypeLDAP && req.Type != models.AuthProviderTypeOIDC {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "type must be ldap or oidc")
return
}
if req.Type == db.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" {
if req.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" {
err = oidcCompileAdmissionExpr(strings.TrimSpace(req.OIDCAdmissionExpr))
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid admission expression: "+err.Error())
@@ -91,7 +91,7 @@ func (api *API) UpdateAuthProvider(w http.ResponseWriter, r *http.Request, param
}
req.ID = existing.ID
req.Type = existing.Type
if req.Type == db.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" {
if req.Type == models.AuthProviderTypeOIDC && strings.TrimSpace(req.OIDCAdmissionExpr) != "" {
err = oidcCompileAdmissionExpr(strings.TrimSpace(req.OIDCAdmissionExpr))
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "invalid admission expression: "+err.Error())
@@ -165,7 +165,7 @@ func (api *API) TestAuthProvider(w http.ResponseWriter, r *http.Request, params
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "auth provider not found")
return
}
if provider.Type != db.AuthProviderTypeLDAP {
if provider.Type != models.AuthProviderTypeLDAP {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "test is only supported for ldap providers")
return
}
+18 -17
View File
@@ -24,7 +24,7 @@ func (api *API) ListBlockComments(w http.ResponseWriter, r *http.Request, params
var role string
var i int
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"])
@@ -42,7 +42,7 @@ func (api *API) ListBlockComments(w http.ResponseWriter, r *http.Request, params
role = ""
}
for i = 0; i < len(comments); i++ {
comments[i].CanDelete = user.IsAdmin || comments[i].CreatedBy == user.ID || boardRoleAllows(role, "admin")
comments[i].CanDelete = user.IsAdmin || comments[i].CreatedBy == user.ID || boardRoleAllows(role, models.RoleAdmin)
}
}
WriteJSON(w, http.StatusOK, comments)
@@ -64,7 +64,7 @@ func (api *API) CreateBlockComment(w http.ResponseWriter, r *http.Request, param
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -99,7 +99,7 @@ func (api *API) DeleteBlockComment(w http.ResponseWriter, r *http.Request, param
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
comments, err = api.store(r).ListBlockComments(params["boardId"], params["blockId"])
@@ -124,7 +124,7 @@ func (api *API) DeleteBlockComment(w http.ResponseWriter, r *http.Request, param
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin")
return
}
canDelete = user.IsAdmin || boardRoleAllows(role, "admin")
canDelete = user.IsAdmin || boardRoleAllows(role, models.RoleAdmin)
}
if !canDelete {
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "comment deletion requires the comment author or board admin")
@@ -146,7 +146,7 @@ func (api *API) ListBlockChecklistItems(w http.ResponseWriter, r *http.Request,
var items []models.BlockChecklistItem
var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
items, err = api.store(r).ListBlockChecklistItems(params["boardId"], params["blockId"])
@@ -176,7 +176,7 @@ func (api *API) CreateBlockChecklistItem(w http.ResponseWriter, r *http.Request,
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -213,7 +213,7 @@ func (api *API) UpdateBlockChecklistItem(w http.ResponseWriter, r *http.Request,
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "requires user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -236,7 +236,7 @@ func (api *API) UpdateBlockChecklistItem(w http.ResponseWriter, r *http.Request,
func (api *API) DeleteBlockChecklistItem(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = api.store(r).DeleteBlockChecklistItem(params["boardId"], params["blockId"], params["itemId"])
@@ -258,7 +258,7 @@ func (api *API) ReorderBlockChecklistItems(w http.ResponseWriter, r *http.Reques
var items []models.BlockChecklistItem
var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -289,7 +289,7 @@ func (api *API) ListBlockAttachments(w http.ResponseWriter, r *http.Request, par
var items []models.BlockAttachment
var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
items, err = api.store(r).ListBlockAttachments(params["boardId"], params["blockId"])
@@ -320,7 +320,7 @@ func (api *API) CreateBlockAttachment(w http.ResponseWriter, r *http.Request, pa
var created models.BlockAttachment
var ts string
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
user, ok = middleware.UserFromContext(r.Context())
@@ -345,6 +345,7 @@ func (api *API) CreateBlockAttachment(w http.ResponseWriter, r *http.Request, pa
tempName = storedName + ".uploading-" + ts
tempPath, size, err = api.Uploads.Save(tempName, file)
if err != nil {
_ = os.Remove(tempPath)
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
@@ -388,7 +389,7 @@ func (api *API) DownloadBlockAttachment(w http.ResponseWriter, r *http.Request,
var inline bool
var stat os.FileInfo
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
item, err = api.store(r).GetBlockAttachment(params["boardId"], params["blockId"], params["attachmentId"])
@@ -420,7 +421,7 @@ func (api *API) DeleteBlockAttachment(w http.ResponseWriter, r *http.Request, pa
var item models.BlockAttachment
var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
item, err = api.store(r).DeleteBlockAttachment(r.Context(), params["boardId"], params["blockId"], params["attachmentId"])
@@ -440,7 +441,7 @@ func (api *API) ListBoardBlockProperties(w http.ResponseWriter, r *http.Request,
var props []models.BlockProperties
var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
props, err = api.store(r).ListAllBlockProperties(params["boardId"])
@@ -458,7 +459,7 @@ func (api *API) GetBlockProperties(w http.ResponseWriter, r *http.Request, param
var bp models.BlockProperties
var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
bp, err = api.store(r).GetBlockProperties(params["boardId"], params["blockId"])
@@ -489,7 +490,7 @@ func (api *API) UpsertBlockProperties(w http.ResponseWriter, r *http.Request, pa
var bp models.BlockProperties
var err error
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -11,7 +11,7 @@ func (api *API) ListBoardFieldValues(w http.ResponseWriter, r *http.Request, par
var values []models.BoardFieldValue
var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
values, err = api.store(r).ListBoardFieldValues(params["boardId"], params["field"])
@@ -39,7 +39,7 @@ func (api *API) CreateBoardFieldValue(w http.ResponseWriter, r *http.Request, pa
var created models.BoardFieldValue
var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
err = DecodeJSON(r, &req)
@@ -73,7 +73,7 @@ func (api *API) UpdateBoardFieldValue(w http.ResponseWriter, r *http.Request, pa
var updated models.BoardFieldValue
var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
err = DecodeJSON(r, &req)
@@ -106,7 +106,7 @@ func (api *API) ReorderBoardFieldValues(w http.ResponseWriter, r *http.Request,
var values []models.BoardFieldValue
var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
err = DecodeJSON(r, &req)
@@ -132,7 +132,7 @@ func (api *API) ReorderBoardFieldValues(w http.ResponseWriter, r *http.Request,
func (api *API) DeleteBoardFieldValue(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
err = api.store(r).DeleteBoardFieldValue(r.Context(), params["boardId"], params["valueId"])
+29 -29
View File
@@ -12,11 +12,11 @@ import "codit/internal/models"
// boardRoleRank returns a numeric rank for board roles (higher = more access).
func boardRoleRank(role string) int {
switch role {
case "admin":
case models.RoleAdmin:
return 3
case "editor":
case models.RoleWriter:
return 2
case "viewer":
case models.RoleViewer:
return 1
default:
return 0
@@ -24,7 +24,7 @@ func boardRoleRank(role string) int {
}
// boardRoleAllows checks whether actual satisfies the required board role.
// Hierarchy: admin > editor > viewer.
// Hierarchy: admin > writer > viewer.
func boardRoleAllows(actual, required string) bool {
return boardRoleRank(actual) >= boardRoleRank(required)
}
@@ -38,7 +38,7 @@ func (api *API) boardRoleForUser(r *http.Request, boardID string, user models.Us
var err error
if user.IsAdmin {
return "admin", nil
return models.RoleAdmin, nil
}
projectID, err = api.store(r).GetBoardProjectID(boardID)
if err != nil {
@@ -61,12 +61,12 @@ func (api *API) boardRoleForUser(r *http.Request, boardID string, user models.Us
func projectRoleToBoardRole(projectRole string) string {
switch projectRole {
case "admin":
return "admin"
case "writer":
return "editor"
case models.RoleAdmin:
return models.RoleAdmin
case models.RoleWriter:
return models.RoleWriter
default:
return "viewer"
return models.RoleViewer
}
}
@@ -74,7 +74,7 @@ func projectRoleToBoardRole(projectRole string) string {
// Returns "" for unknown roles.
func normalizeBoardRole(role string) string {
switch role {
case "admin", "editor", "viewer":
case models.RoleAdmin, models.RoleWriter, models.RoleViewer:
return role
default:
return ""
@@ -240,7 +240,7 @@ func (api *API) CreateBoard(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "board creation requires a user account")
return
}
if !api.requireProjectRole(w, r, params["projectId"], "writer") {
if !api.requireProjectRole(w, r, params["projectId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -286,7 +286,7 @@ func (api *API) GetBoard(w http.ResponseWriter, r *http.Request, params map[stri
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
return
}
if !api.requireBoardRole(w, r, board.ID, "viewer") {
if !api.requireBoardRole(w, r, board.ID, models.RoleViewer) {
return
}
WriteJSON(w, http.StatusOK, board)
@@ -310,7 +310,7 @@ func (api *API) UpdateBoard(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
return
}
if !api.requireBoardRole(w, r, board.ID, "editor") {
if !api.requireBoardRole(w, r, board.ID, models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -358,7 +358,7 @@ func (api *API) DeleteBoard(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "board not found")
return
}
if !api.requireBoardRole(w, r, board.ID, "admin") {
if !api.requireBoardRole(w, r, board.ID, models.RoleAdmin) {
return
}
err = api.store(r).DeleteBoard(board.ID, user.ID)
@@ -390,7 +390,7 @@ func (api *API) ListBlocks(w http.ResponseWriter, r *http.Request, params map[st
var err error
var blockType string
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
blockType = r.URL.Query().Get("type")
@@ -422,7 +422,7 @@ func (api *API) CreateBlock(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block creation requires a user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &req)
@@ -459,7 +459,7 @@ func (api *API) GetBlock(w http.ResponseWriter, r *http.Request, params map[stri
var block models.Block
var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
block, err = api.store(r).GetBlock(params["boardId"], params["blockId"])
@@ -483,7 +483,7 @@ func (api *API) UpdateBlock(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
block, err = api.store(r).GetBlock(params["boardId"], params["blockId"])
@@ -538,7 +538,7 @@ func (api *API) PatchBlocks(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block updates require a user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = DecodeJSON(r, &patches)
@@ -572,7 +572,7 @@ func (api *API) DeleteBlock(w http.ResponseWriter, r *http.Request, params map[s
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "block deletion requires a user account")
return
}
if !api.requireBoardRole(w, r, params["boardId"], "editor") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleWriter) {
return
}
err = api.store(r).DeleteBlock(r.Context(), params["boardId"], params["blockId"], user.ID)
@@ -598,7 +598,7 @@ func (api *API) ListBoardMembers(w http.ResponseWriter, r *http.Request, params
var members []models.BoardMember
var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
members, err = api.store(r).ListBoardMembers(params["boardId"])
@@ -616,7 +616,7 @@ func (api *API) ListBoardAssignableUsers(w http.ResponseWriter, r *http.Request,
var users []models.BoardAssignableUser
var err error
if !api.requireBoardRole(w, r, params["boardId"], "viewer") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleViewer) {
return
}
users, err = api.store(r).ListBoardAssignableUsers(params["boardId"])
@@ -636,7 +636,7 @@ func (api *API) AddBoardMember(w http.ResponseWriter, r *http.Request, params ma
var member models.BoardMember
var role string
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
err = DecodeJSON(r, &req)
@@ -649,11 +649,11 @@ func (api *API) AddBoardMember(w http.ResponseWriter, r *http.Request, params ma
return
}
if req.Role == "" {
req.Role = "editor"
req.Role = models.RoleWriter
}
role = normalizeBoardRole(req.Role)
if role == "" {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, editor, or viewer")
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, writer, or viewer")
return
}
member = models.BoardMember{
@@ -679,7 +679,7 @@ func (api *API) UpdateBoardMember(w http.ResponseWriter, r *http.Request, params
var member models.BoardMember
var role string
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
err = DecodeJSON(r, &req)
@@ -689,7 +689,7 @@ func (api *API) UpdateBoardMember(w http.ResponseWriter, r *http.Request, params
}
role = normalizeBoardRole(req.Role)
if role == "" {
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, editor, or viewer")
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "role must be admin, writer, or viewer")
return
}
member = models.BoardMember{
@@ -712,7 +712,7 @@ func (api *API) UpdateBoardMember(w http.ResponseWriter, r *http.Request, params
func (api *API) RemoveBoardMember(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireBoardRole(w, r, params["boardId"], "admin") {
if !api.requireBoardRole(w, r, params["boardId"], models.RoleAdmin) {
return
}
err = api.store(r).DeleteBoardMember(params["boardId"], params["userId"])
+52 -27
View File
@@ -58,7 +58,7 @@ func (api *API) ListPublicAuthProviders(w http.ResponseWriter, r *http.Request,
}
result = make([]map[string]string, 0, len(providers))
for _, p = range providers {
if p.Type == db.AuthProviderTypeDB {
if p.Type == models.AuthProviderTypeDB {
continue
}
result = append(result, map[string]string{
@@ -76,7 +76,7 @@ func (api *API) OIDCLoginWithProvider(w http.ResponseWriter, r *http.Request, pa
var err error
var authURL string
provider, err = api.store(r).GetAuthProvider(params["id"])
if err != nil || provider.Type != db.AuthProviderTypeOIDC {
if err != nil || provider.Type != models.AuthProviderTypeOIDC {
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found")
return
}
@@ -117,8 +117,12 @@ func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request,
var user models.User
var required bool
var sessionVerified bool
var txStore *db.Store
var tokenValue string
var expires time.Time
provider, err = api.store(r).GetAuthProvider(params["id"])
if err != nil || provider.Type != db.AuthProviderTypeOIDC {
if err != nil || provider.Type != models.AuthProviderTypeOIDC {
WriteJSONWithErrorReason(w, r, http.StatusNotFound, "oidc provider not found")
return
}
@@ -142,42 +146,63 @@ func (api *API) OIDCCallbackWithProvider(w http.ResponseWriter, r *http.Request,
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, "missing authorization code")
return
}
// this is an external http call - it must stay outside a transaction
token, err = api.oidcExchangeCodeFromProvider(r.Context(), provider, code)
if err != nil {
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc token exchange failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, err.Error())
return
}
// this is another external http call - it must stay outside a transaction
claims, err = api.oidcResolveClaimsFromProvider(r.Context(), provider, token)
if err != nil {
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc claims fetch failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, "oidc claims fetch failed")
return
}
user, err = api.oidcGetOrCreateUser(r.Context(), claims, provider)
if err != nil {
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc user mapping failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, fmt.Sprintf("oidc user mapping failed - %s", err.Error()))
return
}
required, err = api.store(r).UserRequiresTOTP(user.ID)
txStore, err = api.Store.BeginImmediateStore(r.Context())
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
user, err = api.store(r).GetUserByID(user.ID)
user, err = api.oidcGetOrCreateUser(r.Context(), txStore, claims, provider)
if err != nil {
_ = txStore.Rollback()
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "oidc user mapping failed provider=%s err=%v", provider.Name, err)
WriteJSONWithErrorReason(w, r, http.StatusUnauthorized, fmt.Sprintf("oidc user mapping failed - %s", err.Error()))
return
}
required, err = txStore.UserRequiresTOTP(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
user, err = txStore.GetUserByID(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
user.TOTPEffectiveRequired = required
user.TOTPSetupRequired = required && !user.TOTPEnabled
sessionVerified = !required
err = api.issueSession(w, r, user, sessionVerified, token.IDToken)
tokenValue, expires, err = api.createSessionWithStore(txStore, user, sessionVerified, token.IDToken)
if err != nil {
_ = txStore.Rollback()
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "session create failed user=%s err=%v", user.Username, err)
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
err = txStore.Commit()
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
api.setSessionCookie(w, tokenValue, expires)
api.Logger.Write(logIDAuth, codit_logger.LOG_INFO, "oidc login success username=%s provider=%s", user.Username, provider.Name)
http.Redirect(w, r, "/", http.StatusFound)
}
@@ -373,7 +398,7 @@ func (api *API) oidcUserInfoFromProvider(ctx context.Context, p models.AuthProvi
return claims, nil
}
func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims, provider models.AuthProvider) (models.User, error) {
func (api *API) oidcGetOrCreateUser(ctx context.Context, store *db.Store, claims oidcUserClaims, provider models.AuthProvider) (models.User, error) {
var username string
var displayName string
var email string
@@ -386,12 +411,12 @@ func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims,
return user, err
}
user, err = api.storeContext(ctx).GetUserByProviderAndSub(provider.ID, claims.Sub)
user, err = store.GetUserByProviderAndSub(provider.ID, claims.Sub)
if err == nil {
if user.Disabled {
return user, errors.New("user disabled")
}
err = api.oidcSyncUserGroups(ctx, provider, user, claims, false)
err = api.oidcSyncUserGroups(ctx, store, provider, user, claims, false)
if err != nil { return user, err }
return user, nil
}
@@ -411,15 +436,15 @@ func (api *API) oidcGetOrCreateUser(ctx context.Context, claims oidcUserClaims,
AuthProviderID: provider.ID,
ExternalSubject: strings.TrimSpace(claims.Sub),
}
created, err = api.storeContext(ctx).CreateUser(user, "")
created, err = store.CreateUser(user, "")
if err != nil { return created, err }
err = api.oidcSyncUserGroups(ctx, provider, created, claims, true)
err = api.oidcSyncUserGroups(ctx, store, provider, created, claims, true)
if err != nil { return created, err }
return created, nil
}
func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProvider, user models.User, claims oidcUserClaims, isNew bool) error {
func (api *API) oidcSyncUserGroups(ctx context.Context, store *db.Store, provider models.AuthProvider, user models.User, claims oidcUserClaims, isNew bool) error {
var mode string
var groupsClaim string
var claimValues []string
@@ -433,17 +458,17 @@ func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProv
var matched bool
mode = strings.TrimSpace(provider.GroupSyncMode)
if mode == db.GroupSyncModeOff || mode == "" {
if mode == models.GroupSyncModeOff || mode == "" {
if isNew && strings.TrimSpace(provider.DefaultUserGroupID) != "" {
err = api.storeContext(ctx).AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID)
err = store.AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID)
if err != nil { return fmt.Errorf("oidc default user group assignment failed: %w", err) }
}
return nil
}
if mode == db.GroupSyncModeFirstLogin && !isNew {
if mode == models.GroupSyncModeFirstLogin && !isNew {
return nil
}
if mode != db.GroupSyncModeFirstLogin && mode != db.GroupSyncModeSync {
if mode != models.GroupSyncModeFirstLogin && mode != models.GroupSyncModeSync {
return fmt.Errorf("invalid oidc group sync mode: %s", mode)
}
@@ -457,7 +482,7 @@ func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProv
claimSet[claimValues[i]] = true
}
mappings, err = api.storeContext(ctx).ListAuthProviderGroupMappings(provider.ID)
mappings, err = store.ListAuthProviderGroupMappings(provider.ID)
if err != nil {
return fmt.Errorf("oidc group mapping lookup failed: %w", err)
}
@@ -466,17 +491,17 @@ func (api *API) oidcSyncUserGroups(ctx context.Context, provider models.AuthProv
assignedAny = false
for groupID, matched = range groupMatched {
if matched {
err = api.storeContext(ctx).AddUserGroupMember(ctx, groupID, user.ID)
err = store.AddUserGroupMember(ctx, groupID, user.ID)
if err != nil { return fmt.Errorf("oidc sso group assignment failed: %w", err) }
assignedAny = true
} else if mode == db.GroupSyncModeSync {
err = api.storeContext(ctx).RemoveUserGroupMember(ctx, groupID, user.ID)
} else if mode == models.GroupSyncModeSync {
err = store.RemoveUserGroupMember(ctx, groupID, user.ID)
if err != nil { return fmt.Errorf("oidc sso group removal failed: %w", err) }
}
}
if isNew && !assignedAny && strings.TrimSpace(provider.DefaultUserGroupID) != "" {
err = api.storeContext(ctx).AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID)
err = store.AddUserGroupMember(ctx, provider.DefaultUserGroupID, user.ID)
if err != nil { return fmt.Errorf("oidc default user group assignment failed: %w", err) }
}
@@ -159,13 +159,18 @@ func (api *API) startSSHFileTransferAudit(store *db.Store, r *http.Request, oper
}
func (api *API) finishSSHFileTransferAudit(audit *sshFileTransferAudit, status string) {
var ctx context.Context
var cancel context.CancelFunc
var err error
if audit == nil || audit.Finished { return }
audit.Finished = true
// i don't bind this to a request context because i want the audit record write to persist regardless of client connection.
err = audit.Store.FinishSSHFileTransfer(context.Background(), audit.Item.ID, status, audit.Paths, audit.Bytes, audit.ErrorText)
// Use an independent bounded context so client cancellation does not skip the final audit update.
ctx, cancel = context.WithTimeout(context.Background(), 10 * time.Second)
defer cancel()
err = audit.Store.FinishSSHFileTransfer(ctx, audit.Item.ID, status, audit.Paths, audit.Bytes, audit.ErrorText)
if err != nil {
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file transfer audit finish failed id=%s op=%s err=%s", audit.Item.ID, audit.Item.Operation, err.Error())
return
+8 -15
View File
@@ -1600,9 +1600,8 @@ func (api *API) DiscoverSSHServerHostKeyAdmin(w http.ResponseWriter, r *http.Req
var discovered models.SSHServerHostKey
var err error
if !api.requireAdmin(w, r) {
return
}
if !api.requireAdmin(w, r) { return }
item, err = api.store(r).GetSSHServer(params["id"])
if err != nil {
if err == sql.ErrNoRows {
@@ -2540,20 +2539,14 @@ func discoverSSHServerHostKey(host string, port int, defaultUser string) (models
Timeout: 10 * time.Second,
}
conn, err = net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return item, err
}
if err != nil { return item, err }
defer conn.Close()
clientConn, _, reqs, err = ssh.NewClientConn(conn, addr, config)
if clientConn != nil {
clientConn.Close()
}
if reqs != nil {
go ssh.DiscardRequests(reqs)
}
if discovered == nil {
return item, errors.New("failed to discover ssh host key")
}
if clientConn != nil { clientConn.Close() }
if reqs != nil { go ssh.DiscardRequests(reqs) }
if discovered == nil { return item, errors.New("failed to discover ssh host key") }
item.Algorithm = discovered.Type()
item.PublicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(discovered)))
item.Fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(discovered))
+1
View File
@@ -556,6 +556,7 @@ func (api *API) InspectSSHCertificate(w http.ResponseWriter, r *http.Request, _
var cert *ssh.Certificate
var dump string
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
+40 -4
View File
@@ -1,5 +1,6 @@
package handlers
import codit_logger "codit/logger"
import "database/sql"
import "net/http"
import "net/url"
@@ -37,19 +38,42 @@ func (api *API) finishLogin(w http.ResponseWriter, r *http.Request, user models.
var err error
var required bool
var sessionVerified bool
required, err = api.store(r).UserRequiresTOTP(user.ID)
var txStore *db.Store
var token string
var expires time.Time
txStore, err = api.Store.BeginImmediateStore(r.Context())
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
required, err = txStore.UserRequiresTOTP(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
user, err = txStore.GetUserByID(user.ID)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
if user.Disabled {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusForbidden, "user disabled")
return
}
user.TOTPEffectiveRequired = required
user.TOTPSetupRequired = required && !user.TOTPEnabled
ok, err = api.verifyUserTOTPForLogin(r, user, otpCode)
ok, err = api.verifyUserTOTPForLoginWithStore(txStore, user, otpCode)
if err != nil {
_ = txStore.Rollback()
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
if !ok {
_ = txStore.Rollback()
if strings.TrimSpace(otpCode) == "" {
WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": "totp_required", "totp_required": true})
return
@@ -61,19 +85,31 @@ func (api *API) finishLogin(w http.ResponseWriter, r *http.Request, user models.
if required && !user.TOTPEnabled {
sessionVerified = false
}
err = api.issueSession(w, r, user, sessionVerified, "")
token, expires, err = api.createSessionWithStore(txStore, user, sessionVerified, "")
if err != nil {
_ = txStore.Rollback()
api.Logger.Write(logIDAuth, codit_logger.LOG_WARN, "session create failed user=%s err=%v", user.Username, err)
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
err = txStore.Commit()
if err != nil {
WriteJSONWithErrorReason(w, r, http.StatusInternalServerError, err.Error())
return
}
api.setSessionCookie(w, token, expires)
WriteJSON(w, http.StatusOK, user)
}
func (api *API) verifyUserTOTPForLogin(r *http.Request, user models.User, otpCode string) (bool, error) {
return api.verifyUserTOTPForLoginWithStore(api.store(r), user, otpCode)
}
func (api *API) verifyUserTOTPForLoginWithStore(store *db.Store, user models.User, otpCode string) (bool, error) {
var item db.UserTOTP
var secret string
var err error
item, err = api.store(r).GetUserTOTP(user.ID)
item, err = store.GetUserTOTP(user.ID)
if err != nil {
if err == sql.ErrNoRows {
return true, nil
+23
View File
@@ -0,0 +1,23 @@
package models
const RepoTypeGit string = "git"
const RepoTypeRPM string = "rpm"
const RepoTypeDocker string = "docker"
const RoleAdmin string = "admin"
const RoleWriter string = "writer"
const RoleViewer string = "viewer"
const AuthProviderTypeDB string = "db"
const AuthProviderTypeLDAP string = "ldap"
const AuthProviderTypeOIDC string = "oidc"
const GroupSyncModeOff string = "off"
const GroupSyncModeFirstLogin string = "first_login"
const GroupSyncModeSync string = "sync"
const UserGroupScopeExplicit string = "explicit"
const UserGroupScopeAllUsers string = "all_users"
const RPMRepoModeLocal string = "local"
const RPMRepoModeMirror string = "mirror"
+130 -1
View File
@@ -28,6 +28,8 @@ CREATE TABLE blocks (
type TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
fields TEXT NOT NULL DEFAULT '{}',
completed_at INTEGER NOT NULL DEFAULT 0,
completed_by INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
@@ -47,6 +49,8 @@ CREATE TABLE blocks_history (
type TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
fields TEXT NOT NULL DEFAULT '{}',
completed_at INTEGER NOT NULL DEFAULT 0,
completed_by INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
@@ -56,16 +60,141 @@ CREATE TABLE blocks_history (
CREATE TABLE board_members (
board_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
role TEXT NOT NULL DEFAULT 'editor',
role TEXT NOT NULL DEFAULT 'writer',
created_at INTEGER NOT NULL,
PRIMARY KEY (board_id, user_id),
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE block_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE block_properties (
id INTEGER PRIMARY KEY AUTOINCREMENT,
block_id INTEGER NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT '',
card_type TEXT NOT NULL DEFAULT '',
priority TEXT NOT NULL DEFAULT '',
due_date TEXT NOT NULL DEFAULT '',
assignee_id INTEGER,
sprint TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE TABLE board_field_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
board_id INTEGER NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
field TEXT NOT NULL,
value TEXT NOT NULL,
label TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '',
display_order INTEGER NOT NULL DEFAULT 0,
start_date TEXT NOT NULL DEFAULT '',
end_date TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(board_id, field, value)
);
CREATE TABLE block_checklist_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
display_order INTEGER NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL,
updated_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id),
FOREIGN KEY (updated_by) REFERENCES users(id)
);
CREATE TABLE block_attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size INTEGER NOT NULL DEFAULT 0,
storage_path TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE block_assignees (
block_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
assigned_at INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (block_id, user_id),
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS auth_provider_group_mappings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
auth_provider_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
claim_value TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (auth_provider_id) REFERENCES auth_providers(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
UNIQUE (auth_provider_id, claim_value, group_id)
);
ALTER TABLE repos ADD COLUMN description TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_storage_path TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_content_type TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_updated_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE auth_providers ADD COLUMN oidc_end_session_url TEXT NOT NULL DEFAULT '';
ALTER TABLE auth_providers ADD COLUMN group_sync_mode TEXT NOT NULL DEFAULT 'off';
ALTER TABLE auth_providers ADD COLUMN oidc_post_logout_redirect INTEGER NOT NULL DEFAULT 0;
ALTER TABLE sessions ADD COLUMN oidc_id_token TEXT NOT NULL DEFAULT '';
INSERT OR IGNORE INTO block_properties (block_id, status, description)
SELECT bl.id,
COALESCE(json_extract(bl.fields, '$.status'), ''),
COALESCE(json_extract(bl.fields, '$.description'), '')
FROM blocks bl
WHERE bl.type = 'card' AND bl.delete_at = 0;
INSERT OR IGNORE INTO block_assignees (block_id, user_id, assigned_at)
SELECT block_id, assignee_id, strftime('%s', 'now')
FROM block_properties
WHERE assignee_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_boards_project ON boards(project_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_blocks_board ON blocks(board_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_blocks_parent ON blocks(parent_id);
CREATE INDEX IF NOT EXISTS idx_blocks_type ON blocks(board_id, type, delete_at);
CREATE INDEX IF NOT EXISTS idx_blocks_history_block ON blocks_history(block_id, insert_at DESC);
CREATE INDEX IF NOT EXISTS idx_board_members_user ON board_members(user_id);
CREATE INDEX IF NOT EXISTS idx_block_comments_block ON block_comments(block_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_block_properties_block ON block_properties(block_id);
CREATE INDEX IF NOT EXISTS idx_board_field_values_board_field ON board_field_values(board_id, field);
CREATE INDEX IF NOT EXISTS idx_block_checklist_items_block ON block_checklist_items(block_id, delete_at, display_order);
CREATE INDEX IF NOT EXISTS idx_block_attachments_block ON block_attachments(block_id, delete_at, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_block_assignees_user ON block_assignees(user_id, block_id);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_provider ON auth_provider_group_mappings(auth_provider_id);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_claim ON auth_provider_group_mappings(auth_provider_id, claim_value);
-36
View File
@@ -1,36 +0,0 @@
CREATE TABLE block_comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
content TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE TABLE block_properties (
id INTEGER PRIMARY KEY AUTOINCREMENT,
block_id INTEGER NOT NULL UNIQUE,
status TEXT NOT NULL DEFAULT '',
card_type TEXT NOT NULL DEFAULT '',
priority TEXT NOT NULL DEFAULT '',
due_date TEXT NOT NULL DEFAULT '',
assignee_id INTEGER,
sprint TEXT NOT NULL DEFAULT '',
description TEXT NOT NULL DEFAULT '',
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL
);
INSERT OR IGNORE INTO block_properties (block_id, status, description)
SELECT bl.id,
COALESCE(json_extract(bl.fields, '$.status'), ''),
COALESCE(json_extract(bl.fields, '$.description'), '')
FROM blocks bl
WHERE bl.type = 'card' AND bl.delete_at = 0;
CREATE INDEX IF NOT EXISTS idx_block_comments_block ON block_comments(block_id, delete_at);
CREATE INDEX IF NOT EXISTS idx_block_properties_block ON block_properties(block_id);
-15
View File
@@ -1,15 +0,0 @@
CREATE TABLE board_sprints (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
board_id INTEGER NOT NULL,
name TEXT NOT NULL,
start_date TEXT NOT NULL DEFAULT '',
end_date TEXT NOT NULL DEFAULT '',
display_order INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (board_id) REFERENCES boards(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_board_sprints_board ON board_sprints(board_id, delete_at);
@@ -1,66 +0,0 @@
-- Migration 005: unified board_field_values (replaces board_sprints; adds custom status/type/priority)
CREATE TABLE board_field_values (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
board_id INTEGER NOT NULL REFERENCES boards(id) ON DELETE CASCADE,
field TEXT NOT NULL,
value TEXT NOT NULL,
label TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '',
display_order INTEGER NOT NULL DEFAULT 0,
start_date TEXT NOT NULL DEFAULT '',
end_date TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(board_id, field, value)
);
CREATE INDEX idx_board_field_values_board_field ON board_field_values(board_id, field);
-- Seed default status values for every existing board
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'status', 'todo', 'To Do', '#1976d2', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'status', 'in_progress','In Progress', '#f59e0b', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'status', 'done', 'Done', '#16a34a', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
-- Seed default type values
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'task', 'Task', '#1976d2', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'bug', 'Bug', '#d32f2f', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'story', 'Story', '#388e3c', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'type', 'epic', 'Epic', '#7b1fa2', 3, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
-- Seed default priority values
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'low', 'Low', '#4caf50', 0, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'medium', 'Medium', '#2196f3', 1, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'high', 'High', '#ff9800', 2, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, created_at, updated_at)
SELECT lower(hex(randomblob(8))), b.id, 'priority', 'urgent', 'Urgent', '#f44336', 3, strftime('%s','now'), strftime('%s','now') FROM boards b WHERE b.delete_at = 0;
-- Migrate existing board_sprints rows (value = public_id for rename safety)
INSERT INTO board_field_values (public_id, board_id, field, value, label, color, display_order, start_date, end_date, created_at, updated_at)
SELECT
s.public_id,
s.board_id,
'sprint',
s.public_id,
s.name,
'#7c3aed',
s.display_order,
COALESCE(s.start_date, ''),
COALESCE(s.end_date, ''),
s.created_at,
s.updated_at
FROM board_sprints s
WHERE s.delete_at = 0;
DROP TABLE board_sprints;
@@ -1 +0,0 @@
ALTER TABLE repos ADD COLUMN description TEXT NOT NULL DEFAULT '';
@@ -1,4 +0,0 @@
ALTER TABLE blocks ADD COLUMN completed_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE blocks ADD COLUMN completed_by INTEGER;
ALTER TABLE blocks_history ADD COLUMN completed_at INTEGER NOT NULL DEFAULT 0;
ALTER TABLE blocks_history ADD COLUMN completed_by INTEGER;
@@ -1,18 +0,0 @@
CREATE TABLE block_checklist_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
title TEXT NOT NULL,
done INTEGER NOT NULL DEFAULT 0,
display_order INTEGER NOT NULL DEFAULT 0,
created_by INTEGER NOT NULL,
updated_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id),
FOREIGN KEY (updated_by) REFERENCES users(id)
);
CREATE INDEX idx_block_checklist_items_block ON block_checklist_items(block_id, delete_at, display_order);
@@ -1,16 +0,0 @@
CREATE TABLE block_attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
public_id TEXT NOT NULL UNIQUE,
block_id INTEGER NOT NULL,
filename TEXT NOT NULL,
content_type TEXT NOT NULL DEFAULT 'application/octet-stream',
size INTEGER NOT NULL DEFAULT 0,
storage_path TEXT NOT NULL,
created_by INTEGER NOT NULL,
created_at INTEGER NOT NULL,
delete_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (created_by) REFERENCES users(id)
);
CREATE INDEX idx_block_attachments_block ON block_attachments(block_id, delete_at, created_at DESC);
@@ -1,15 +0,0 @@
CREATE TABLE block_assignees (
block_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
assigned_at INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (block_id, user_id),
FOREIGN KEY (block_id) REFERENCES blocks(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
INSERT OR IGNORE INTO block_assignees (block_id, user_id, assigned_at)
SELECT block_id, assignee_id, strftime('%s', 'now')
FROM block_properties
WHERE assignee_id IS NOT NULL;
CREATE INDEX idx_block_assignees_user ON block_assignees(user_id, block_id);
-3
View File
@@ -1,3 +0,0 @@
ALTER TABLE users ADD COLUMN avatar_storage_path TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_content_type TEXT NOT NULL DEFAULT '';
ALTER TABLE users ADD COLUMN avatar_updated_at INTEGER NOT NULL DEFAULT 0;
@@ -1 +0,0 @@
ALTER TABLE auth_providers ADD COLUMN oidc_end_session_url TEXT NOT NULL DEFAULT '';
@@ -1,16 +0,0 @@
ALTER TABLE auth_providers ADD COLUMN group_sync_mode TEXT NOT NULL DEFAULT 'off';
CREATE TABLE IF NOT EXISTS auth_provider_group_mappings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
auth_provider_id INTEGER NOT NULL,
group_id INTEGER NOT NULL,
claim_value TEXT NOT NULL,
created_at INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (auth_provider_id) REFERENCES auth_providers(id) ON DELETE CASCADE,
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
UNIQUE (auth_provider_id, claim_value, group_id)
);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_provider ON auth_provider_group_mappings(auth_provider_id);
CREATE INDEX IF NOT EXISTS idx_auth_provider_group_mappings_claim ON auth_provider_group_mappings(auth_provider_id, claim_value);
@@ -1 +0,0 @@
ALTER TABLE auth_providers ADD COLUMN oidc_post_logout_redirect INTEGER NOT NULL DEFAULT 0;
@@ -1 +0,0 @@
ALTER TABLE sessions ADD COLUMN oidc_id_token TEXT NOT NULL DEFAULT '';
+42 -42
View File
@@ -745,11 +745,11 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/health", api.Health)
router.Handle("GET", "/api/app-info", txRead(api.AppInfo))
router.Handle("POST", "/api/login", txWrite(api.Login))
router.Handle("POST", "/api/login", api.Login)
router.Handle("POST", "/api/logout", txWrite(api.Logout))
router.Handle("GET", "/api/auth/providers", txRead(api.ListPublicAuthProviders))
router.Handle("GET", "/api/auth/oidc/:id/login", txRead(api.OIDCLoginWithProvider))
router.Handle("GET", "/api/auth/oidc/:id/callback", txRead(api.OIDCCallbackWithProvider))
router.Handle("GET", "/api/auth/oidc/:id/callback", api.OIDCCallbackWithProvider)
router.Handle("GET", "/api/me", txRead(api.Me))
router.Handle("PATCH", "/api/me", txWrite(api.UpdateMe))
router.Handle("POST", "/api/me/avatar", txWrite(api.UploadMyAvatar))
@@ -761,7 +761,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/me/totp/disable", txWrite(api.DisableMyTOTP))
router.Handle("GET", "/api/users", txRead(api.ListUsers))
router.Handle("GET", "/api/users/:id/avatar", txRead(api.GetUserAvatar))
router.Handle("GET", "/api/users/:id/avatar", api.GetUserAvatar)
router.Handle("POST", "/api/users", txWrite(api.CreateUser))
router.Handle("PATCH", "/api/users/:id", txWrite(api.UpdateUser))
router.Handle("DELETE", "/api/users/:id", txWrite(api.DeleteUser))
@@ -818,7 +818,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/admin/pki/cas", txRead(api.ListPKICAs))
router.Handle("GET", "/api/admin/pki/cas/:id", txRead(api.GetPKICA))
router.Handle("PATCH", "/api/admin/pki/cas/:id", txWrite(api.UpdatePKICA))
router.Handle("GET", "/api/admin/pki/cas/:id/bundle", txRead(api.DownloadPKICABundle))
router.Handle("GET", "/api/admin/pki/cas/:id/bundle", api.DownloadPKICABundle)
router.Handle("POST", "/api/admin/pki/cas/root", txWrite(api.CreatePKIRootCA))
router.Handle("POST", "/api/admin/pki/cas/intermediate", txWrite(api.CreatePKIIntermediateCA))
router.Handle("GET", "/api/admin/pki/cas/:id/crl", txRead(api.GetPKICRL))
@@ -827,7 +827,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/admin/pki/certs/:id", txRead(api.GetPKICert))
router.Handle("GET", "/api/admin/pki/certs/:id/inspect", txRead(api.GetPKICertInspect))
router.Handle("POST", "/api/admin/pki/certs/:id/renew-acme", txWrite(api.RenewPKICertWithACME))
router.Handle("GET", "/api/admin/pki/certs/:id/bundle", txRead(api.DownloadPKICertBundle))
router.Handle("GET", "/api/admin/pki/certs/:id/bundle", api.DownloadPKICertBundle)
router.Handle("POST", "/api/admin/pki/certs", txWrite(api.IssuePKICert))
router.Handle("POST", "/api/admin/pki/certs/import", txWrite(api.ImportPKICert))
router.Handle("POST", "/api/admin/pki/certs/:id/revoke", txWrite(api.RevokePKICert))
@@ -842,7 +842,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/admin/pki/acme/profiles/:id", txWrite(api.DeleteACMEProfile))
router.Handle("GET", "/api/admin/pki/acme/orders", txRead(api.ListACMEOrders))
router.Handle("POST", "/api/admin/pki/acme/orders", txWrite(api.CreateACMEOrder))
router.Handle("POST", "/api/admin/pki/acme/orders/:id/finalize", txWrite(api.FinalizeACMEOrder))
router.Handle("POST", "/api/admin/pki/acme/orders/:id/finalize", api.FinalizeACMEOrder)
router.Handle("DELETE", "/api/admin/pki/acme/orders/:id", txWrite(api.DeleteACMEOrder))
router.Handle("GET", "/api/admin/rpm/mirrors", txRead(api.ListAdminRPMMirrors))
router.Handle("GET", "/api/admin/ssh/user-cas", txRead(api.ListSSHUserCAs))
@@ -867,7 +867,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/admin/ssh/servers/:id", txWrite(api.DeleteSSHServerAdmin))
router.Handle("GET", "/api/admin/ssh/servers/:id/host-keys", txRead(api.ListSSHServerHostKeysAdmin))
router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys", txWrite(api.CreateSSHServerHostKeyAdmin))
router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys/discover", txWrite(api.DiscoverSSHServerHostKeyAdmin))
router.Handle("POST", "/api/admin/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyAdmin)
router.Handle("DELETE", "/api/admin/ssh/servers/:id/host-keys/:hostKeyId", txWrite(api.DeleteSSHServerHostKeyAdmin))
router.Handle("GET", "/api/admin/ssh/server-groups", txRead(api.ListSSHServerGroupsAdmin))
router.Handle("POST", "/api/admin/ssh/server-groups", txWrite(api.CreateSSHServerGroupAdmin))
@@ -887,7 +887,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/admin/ssh/access-profiles/:id", txWrite(api.DeleteSSHAccessProfileAdmin))
router.Handle("GET", "/api/admin/ssh/sessions", txRead(api.ListSSHSessionsAdmin))
router.Handle("GET", "/api/admin/ssh/file-transfers", txRead(api.ListSSHFileTransfersAdmin))
router.Handle("GET", "/api/admin/ssh/sessions/:id/transcript", txRead(api.GetSSHSessionTranscriptAdmin))
router.Handle("GET", "/api/admin/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptAdmin)
router.Handle("GET", "/api/ssh/user-cas", txRead(api.ListSSHUserCAsForSelf))
router.Handle("GET", "/api/ssh/user-cas/:id", txRead(api.GetSSHUserCAForSelf))
router.Handle("GET", "/api/ssh/user-cas/:id/public-key", txRead(api.DownloadSSHUserCAPublicKeyForSelf))
@@ -899,7 +899,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/ssh/servers/:id", txWrite(api.DeleteSSHServerForSelf))
router.Handle("GET", "/api/ssh/servers/:id/host-keys", txRead(api.ListSSHServerHostKeysForSelf))
router.Handle("POST", "/api/ssh/servers/:id/host-keys", txWrite(api.CreateSSHServerHostKeyForSelf))
router.Handle("POST", "/api/ssh/servers/:id/host-keys/discover", txWrite(api.DiscoverSSHServerHostKeyForSelf))
router.Handle("POST", "/api/ssh/servers/:id/host-keys/discover", api.DiscoverSSHServerHostKeyForSelf)
router.Handle("DELETE", "/api/ssh/servers/:id/host-keys/:hostKeyId", txWrite(api.DeleteSSHServerHostKeyForSelf))
router.Handle("GET", "/api/ssh/credentials", txRead(api.ListSSHCredentialsForSelf))
router.Handle("POST", "/api/ssh/credentials", txWrite(api.CreateSSHCredentialForSelf))
@@ -921,12 +921,12 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("GET", "/api/ssh/file-transfers", txRead(api.ListSSHFileTransfersForSelf))
router.Handle("GET", "/api/ssh/stream", txRead(api.StreamSSHWorkspaceForSelf))
router.Handle("GET", "/api/ssh/sessions/:id", txRead(api.GetSSHSessionForSelf))
router.Handle("GET", "/api/ssh/sessions/:id/transcript", txRead(api.GetSSHSessionTranscriptForSelf))
router.Handle("GET", "/api/ssh/sessions/:id/transcript", api.GetSSHSessionTranscriptForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/disconnect", txWrite(api.DisconnectSSHSessionForSelf))
router.Handle("POST", "/api/ssh/sessions/:id/files/upload", txWrite(api.UploadSSHSessionFilesForSelf))
router.Handle("POST", "/api/ssh/sessions/:id/files/download", txWrite(api.DownloadSSHSessionFilesForSelf))
router.Handle("POST", "/api/ssh/sessions/:id/files/copy-to", txWrite(api.CopySSHSessionFilesForSelf))
router.Handle("POST", "/api/ssh/cert/inspect", txWrite(api.InspectSSHCertificate))
router.Handle("POST", "/api/ssh/sessions/:id/files/upload", api.UploadSSHSessionFilesForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/files/download", api.DownloadSSHSessionFilesForSelf)
router.Handle("POST", "/api/ssh/sessions/:id/files/copy-to", api.CopySSHSessionFilesForSelf)
router.Handle("POST", "/api/ssh/cert/inspect", api.InspectSSHCertificate)
router.Handle("POST", "/api/ssh/user-cas/:id/sign", txWrite(api.SignSSHUserKeyForSelf))
router.Handle("GET", "/api/ssh/issuances", txRead(api.ListSSHUserCAIssuancesForSelf))
router.Handle("GET", "/api/pki/cas/:id/crl", txRead(api.GetPKICRLForSelf))
@@ -936,14 +936,14 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/pki/client/certs", txWrite(api.IssuePKIClientCertForSelf))
router.Handle("GET", "/api/pki/client/certs/:id", txRead(api.GetPKIClientCertForSelf))
router.Handle("GET", "/api/pki/client/certs/:id/inspect", txRead(api.GetPKIClientCertInspectForSelf))
router.Handle("GET", "/api/pki/client/certs/:id/bundle", txRead(api.DownloadPKIClientCertBundleForSelf))
router.Handle("GET", "/api/pki/client/certs/:id/bundle", api.DownloadPKIClientCertBundleForSelf)
router.Handle("POST", "/api/pki/client/certs/:id/revoke", txWrite(api.RevokePKIClientCertForSelf))
router.Handle("GET", "/api/projects", txRead(api.ListProjects))
router.Handle("POST", "/api/projects", txWrite(api.CreateProject))
router.Handle("GET", "/api/projects/:id", txRead(api.GetProject))
router.Handle("PATCH", "/api/projects/:id", txWrite(api.UpdateProject))
router.Handle("DELETE", "/api/projects/:id", txWrite(api.DeleteProject))
router.Handle("DELETE", "/api/projects/:id", api.DeleteProject)
router.Handle("GET", "/api/projects/:projectId/members", txRead(api.ListProjectMembers))
router.Handle("GET", "/api/projects/:projectId/member-candidates", txRead(api.ListProjectMemberCandidates))
@@ -983,8 +983,8 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("PUT", "/api/boards/:boardId/blocks/:blockId/checklist/:itemId", txWrite(api.UpdateBlockChecklistItem))
router.Handle("DELETE", "/api/boards/:boardId/blocks/:blockId/checklist/:itemId", txWrite(api.DeleteBlockChecklistItem))
router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/attachments", txRead(api.ListBlockAttachments))
router.Handle("POST", "/api/boards/:boardId/blocks/:blockId/attachments", txWrite(api.CreateBlockAttachment))
router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/attachments/:attachmentId/download", txRead(api.DownloadBlockAttachment))
router.Handle("POST", "/api/boards/:boardId/blocks/:blockId/attachments", api.CreateBlockAttachment)
router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/attachments/:attachmentId/download", api.DownloadBlockAttachment)
router.Handle("DELETE", "/api/boards/:boardId/blocks/:blockId/attachments/:attachmentId", txWrite(api.DeleteBlockAttachment))
router.Handle("GET", "/api/boards/:boardId/blocks/:blockId/properties", txRead(api.GetBlockProperties))
router.Handle("PUT", "/api/boards/:boardId/blocks/:blockId/properties", txWrite(api.UpsertBlockProperties))
@@ -994,7 +994,7 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/boards/:boardId/members/:userId", txWrite(api.RemoveBoardMember))
router.Handle("GET", "/api/projects/:projectId/repos", txRead(api.ListRepos))
router.Handle("POST", "/api/projects/:projectId/repos", txWrite(api.CreateRepo))
router.Handle("POST", "/api/projects/:projectId/repos", api.CreateRepo)
router.Handle("GET", "/api/repos/types", txRead(api.RepoTypes))
router.Handle("GET", "/api/repos", txRead(api.ListAllRepos))
router.Handle("GET", "/api/projects/:projectId/foreign-repos/available", txRead(api.ListAvailableRepos))
@@ -1002,23 +1002,23 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("DELETE", "/api/projects/:projectId/foreign-repos/:repoId", txWrite(api.DetachForeignRepo))
router.Handle("GET", "/api/repos/:id", txRead(api.GetRepo))
router.Handle("PATCH", "/api/repos/:id", txWrite(api.UpdateRepo))
router.Handle("DELETE", "/api/repos/:id", txWrite(api.DeleteRepo))
router.Handle("GET", "/api/repos/:id/branches", txRead(api.RepoBranches))
router.Handle("GET", "/api/repos/:id/branches/info", txRead(api.RepoBranchesInfo))
router.Handle("PUT", "/api/repos/:id/branches/default", txWrite(api.RepoSetDefaultBranch))
router.Handle("POST", "/api/repos/:id/branches/rename", txWrite(api.RepoRenameBranch))
router.Handle("POST", "/api/repos/:id/branches/delete", txWrite(api.RepoDeleteBranch))
router.Handle("POST", "/api/repos/:id/branches/create", txWrite(api.RepoCreateBranch))
router.Handle("GET", "/api/repos/:id/commits", txRead(api.RepoCommits))
router.Handle("GET", "/api/repos/:id/tree", txRead(api.RepoTree))
router.Handle("GET", "/api/repos/:id/blob", txRead(api.RepoBlob))
router.Handle("GET", "/api/repos/:id/blob/raw", txRead(api.RepoBlobRaw))
router.Handle("GET", "/api/repos/:id/history", txRead(api.RepoFileHistory))
router.Handle("GET", "/api/repos/:id/diff", txRead(api.RepoFileDiff))
router.Handle("GET", "/api/repos/:id/commit", txRead(api.RepoCommitDetail))
router.Handle("GET", "/api/repos/:id/commit/diff", txRead(api.RepoCommitDiff))
router.Handle("GET", "/api/repos/:id/compare", txRead(api.RepoCompare))
router.Handle("GET", "/api/repos/:id/stats", txRead(api.RepoStats))
router.Handle("DELETE", "/api/repos/:id", api.DeleteRepo)
router.Handle("GET", "/api/repos/:id/branches", api.RepoBranches)
router.Handle("GET", "/api/repos/:id/branches/info", api.RepoBranchesInfo)
router.Handle("PUT", "/api/repos/:id/branches/default", api.RepoSetDefaultBranch)
router.Handle("POST", "/api/repos/:id/branches/rename", api.RepoRenameBranch)
router.Handle("POST", "/api/repos/:id/branches/delete", api.RepoDeleteBranch)
router.Handle("POST", "/api/repos/:id/branches/create", api.RepoCreateBranch)
router.Handle("GET", "/api/repos/:id/commits", api.RepoCommits)
router.Handle("GET", "/api/repos/:id/tree", api.RepoTree)
router.Handle("GET", "/api/repos/:id/blob", api.RepoBlob)
router.Handle("GET", "/api/repos/:id/blob/raw", api.RepoBlobRaw)
router.Handle("GET", "/api/repos/:id/history", api.RepoFileHistory)
router.Handle("GET", "/api/repos/:id/diff", api.RepoFileDiff)
router.Handle("GET", "/api/repos/:id/commit", api.RepoCommitDetail)
router.Handle("GET", "/api/repos/:id/commit/diff", api.RepoCommitDiff)
router.Handle("GET", "/api/repos/:id/compare", api.RepoCompare)
router.Handle("GET", "/api/repos/:id/stats", api.RepoStats)
router.Handle("GET", "/api/repos/:id/rpm/packages", txRead(api.RepoRPMPackages))
router.Handle("GET", "/api/repos/:id/rpm/package", txRead(api.RepoRPMPackage))
router.Handle("POST", "/api/repos/:id/rpm/subdirs", txWrite(api.RepoRPMCreateSubdir))
@@ -1028,15 +1028,15 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/repos/:id/rpm/subdir/sync", txWrite(api.RepoRPMSyncSubdir))
router.Handle("POST", "/api/repos/:id/rpm/subdir/suspend", txWrite(api.RepoRPMSuspendSubdir))
router.Handle("POST", "/api/repos/:id/rpm/subdir/resume", txWrite(api.RepoRPMResumeSubdir))
router.Handle("POST", "/api/repos/:id/rpm/subdir/rebuild-metadata", txWrite(api.RepoRPMRebuildSubdirMetadata))
router.Handle("POST", "/api/repos/:id/rpm/subdir/rebuild-metadata", api.RepoRPMRebuildSubdirMetadata)
router.Handle("POST", "/api/repos/:id/rpm/subdir/cancel", txWrite(api.RepoRPMCancelSubdirSync))
router.Handle("GET", "/api/repos/:id/rpm/subdir/runs", txRead(api.RepoRPMMirrorRuns))
router.Handle("DELETE", "/api/repos/:id/rpm/subdir/runs", txWrite(api.RepoRPMClearMirrorRuns))
router.Handle("DELETE", "/api/repos/:id/rpm/subdir", txWrite(api.RepoRPMDeleteSubdir))
router.Handle("DELETE", "/api/repos/:id/rpm/file", txWrite(api.RepoRPMDeleteFile))
router.Handle("GET", "/api/repos/:id/rpm/file", txRead(api.RepoRPMFile))
router.Handle("GET", "/api/repos/:id/rpm/file", api.RepoRPMFile)
router.Handle("GET", "/api/repos/:id/rpm/tree", txRead(api.RepoRPMTree))
router.Handle("POST", "/api/repos/:id/rpm/upload", txWrite(api.RepoRPMUpload))
router.Handle("POST", "/api/repos/:id/rpm/upload", api.RepoRPMUpload)
router.Handle("GET", "/api/repos/:id/docker/images", txRead(api.RepoDockerImages))
router.Handle("GET", "/api/repos/:id/docker/tags", txRead(api.RepoDockerTags))
router.Handle("GET", "/api/repos/:id/docker/manifest", txRead(api.RepoDockerManifest))
@@ -1063,9 +1063,9 @@ func (app *Server) initHandlers() (*handlers.API, *http.ServeMux, error) {
router.Handle("POST", "/api/projects/:projectId/wiki/pages", txWrite(api.CreateWikiPage))
router.Handle("PATCH", "/api/wiki/pages/:id", txWrite(api.UpdateWikiPage))
router.Handle("POST", "/api/projects/:projectId/uploads", txWrite(api.UploadFile))
router.Handle("POST", "/api/projects/:projectId/uploads", api.UploadFile)
router.Handle("GET", "/api/projects/:projectId/uploads", txRead(api.ListUploads))
router.Handle("GET", "/api/uploads/:id", txRead(api.DownloadFile))
router.Handle("GET", "/api/uploads/:id", api.DownloadFile)
// ---------------------------------------------------------------
@@ -1920,7 +1920,7 @@ func principalCanWriteService(service serviceKind, servicePrefix string, princip
func roleAllowsWrite(role string) bool {
var r string
r = strings.ToLower(strings.TrimSpace(role))
return r == "writer" || r == "admin"
return r == models.RoleWriter || r == models.RoleAdmin
}
func resolveProjectIDForServiceWrite(service serviceKind, servicePrefix string, r *http.Request, store *db.Store) (string, error) {
+1 -1
View File
@@ -17,7 +17,7 @@ func createTestProject(t *testing.T, store *db.Store, user models.User, slug str
Description: slug + " project",
CreatedBy: user.ID,
UpdatedBy: user.ID,
HomePage: "info",
HomePage: "settings",
}
project, err = store.CreateProject(context.Background(), project)
if err != nil {
+1 -1
View File
@@ -458,7 +458,7 @@ Stores explicit per-board user sharing roles.
| --- | --- |
| `board_id` | Internal board FK. |
| `user_id` | Internal user FK. |
| `role` | Board role: `admin`, `editor`, or `viewer`. |
| `role` | Board role: `admin`, `writer`, or `viewer`. |
| `created_at` | Assignment timestamp. |
Primary key: `(board_id, user_id)`.
+2 -2
View File
@@ -11318,7 +11318,7 @@ components:
type: string
enum:
- admin
- editor
- writer
- viewer
created_at:
type: integer
@@ -11333,7 +11333,7 @@ components:
type: string
enum:
- admin
- editor
- writer
- viewer
additionalProperties: true
RepositoryObject: