ops design and app

add more comments feature, more tags feature, and other frontend updates
This commit is contained in:
2026-06-09 18:27:59 -04:00
parent 1e939f307d
commit 04a1f2bb6d
33 changed files with 3276 additions and 113 deletions

View File

@@ -6,6 +6,8 @@ import (
"encoding/json"
"errors"
"fmt"
"path"
"strings"
"time"
"github.com/go-webauthn/webauthn/webauthn"
@@ -284,6 +286,349 @@ func (r *Repository) UpdateUserAccess(ctx context.Context, updates []UserAccessU
return nil
}
type resourceMatch struct {
resourceType ResourceType
resourceID string
}
func (r *Repository) ListResourcePermissions(ctx context.Context, resourceType ResourceType, resourceID string) ([]ResourcePermission, error) {
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return nil, err
}
rows, err := r.db.QueryContext(ctx, `
SELECT p.id, p.user_id, COALESCE(u.email, ''), COALESCE(u.display_name, ''), p.resource_type, COALESCE(p.resource_id, ''), p.permission, COALESCE(p.granted_by, ''), p.created_at
FROM permissions p
LEFT JOIN users u ON u.id = p.user_id
WHERE p.resource_type = ? AND COALESCE(p.resource_id, '') = ?
ORDER BY u.email ASC, p.created_at ASC
`, string(resourceType), resourceID)
if err != nil {
return nil, fmt.Errorf("list resource permissions: %w", err)
}
defer rows.Close()
var permissions []ResourcePermission
for rows.Next() {
permission, err := scanResourcePermission(rows)
if err != nil {
return nil, err
}
permissions = append(permissions, permission)
}
return permissions, rows.Err()
}
func (r *Repository) ReplaceResourcePermissions(ctx context.Context, resourceType ResourceType, resourceID string, actorID string, updates []ResourcePermissionUpdate) error {
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return err
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin resource permissions update: %w", err)
}
defer func() { _ = tx.Rollback() }()
if _, err := tx.ExecContext(ctx, `
DELETE FROM permissions
WHERE resource_type = ? AND COALESCE(resource_id, '') = ?
`, string(resourceType), resourceID); err != nil {
return fmt.Errorf("clear resource permissions: %w", err)
}
now := time.Now().UTC().Format(time.RFC3339)
for _, update := range updates {
update.UserID = strings.TrimSpace(update.UserID)
if update.UserID == "" || update.Permission == PermissionNone {
continue
}
if !validPermission(update.Permission) {
return fmt.Errorf("invalid permission")
}
var exists int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE id = ? AND disabled = 0`, update.UserID).Scan(&exists); err != nil {
return fmt.Errorf("check permission user: %w", err)
}
if exists == 0 {
return fmt.Errorf("user %s does not exist", update.UserID)
}
idPart, err := randomToken(18)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO permissions (id, user_id, resource_type, resource_id, permission, granted_by, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, "perm:"+idPart, update.UserID, string(resourceType), nullString(resourceID), string(update.Permission), nullString(actorID), now); err != nil {
return fmt.Errorf("insert resource permission: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit resource permissions update: %w", err)
}
return nil
}
func (r *Repository) EnsureResourcePermission(ctx context.Context, resourceType ResourceType, resourceID string, actorID string, update ResourcePermissionUpdate) error {
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return err
}
update.UserID = strings.TrimSpace(update.UserID)
if update.UserID == "" {
return fmt.Errorf("user id is required")
}
if !validPermission(update.Permission) || update.Permission == PermissionNone {
return fmt.Errorf("invalid permission")
}
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin resource permission ensure: %w", err)
}
defer func() { _ = tx.Rollback() }()
var exists int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(*) FROM users WHERE id = ? AND disabled = 0`, update.UserID).Scan(&exists); err != nil {
return fmt.Errorf("check permission user: %w", err)
}
if exists == 0 {
return fmt.Errorf("user %s does not exist", update.UserID)
}
best := PermissionNone
rows, err := tx.QueryContext(ctx, `
SELECT permission
FROM permissions
WHERE user_id = ? AND resource_type = ? AND COALESCE(resource_id, '') = ?
`, update.UserID, string(resourceType), resourceID)
if err != nil {
return fmt.Errorf("lookup existing resource permission: %w", err)
}
for rows.Next() {
var raw string
if err := rows.Scan(&raw); err != nil {
rows.Close()
return fmt.Errorf("scan existing resource permission: %w", err)
}
best = maxPermission(best, Permission(raw))
}
if err := rows.Close(); err != nil {
return err
}
if PermissionAllows(best, update.Permission) {
return tx.Commit()
}
if _, err := tx.ExecContext(ctx, `
DELETE FROM permissions
WHERE user_id = ? AND resource_type = ? AND COALESCE(resource_id, '') = ?
`, update.UserID, string(resourceType), resourceID); err != nil {
return fmt.Errorf("clear user resource permission: %w", err)
}
idPart, err := randomToken(18)
if err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO permissions (id, user_id, resource_type, resource_id, permission, granted_by, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, "perm:"+idPart, update.UserID, string(resourceType), nullString(resourceID), string(update.Permission), nullString(actorID), time.Now().UTC().Format(time.RFC3339)); err != nil {
return fmt.Errorf("insert resource permission: %w", err)
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit resource permission ensure: %w", err)
}
return nil
}
func (r *Repository) EffectiveResourcePermission(ctx context.Context, principal Principal, resourceType ResourceType, resourceID string, collectionIDs []string) (Permission, error) {
if principal.UserID == "" {
return PermissionNone, nil
}
if principal.Role == RoleAdmin {
return PermissionAdmin, nil
}
resourceType, resourceID, err := normalizeResource(resourceType, resourceID)
if err != nil {
return PermissionNone, err
}
matches := []resourceMatch{{resourceType: ResourceGlobal, resourceID: ""}}
switch resourceType {
case ResourceDocument:
for _, folder := range folderMatchesForDocument(resourceID) {
matches = append(matches, resourceMatch{resourceType: ResourceFolder, resourceID: folder})
}
for _, collectionID := range collectionIDs {
normalized := normalizeCollectionID(collectionID)
if normalized != "" {
matches = append(matches, resourceMatch{resourceType: ResourceCollection, resourceID: normalized})
}
}
matches = append(matches, resourceMatch{resourceType: ResourceDocument, resourceID: resourceID})
case ResourceFolder:
for _, folder := range folderMatches(resourceID) {
matches = append(matches, resourceMatch{resourceType: ResourceFolder, resourceID: folder})
}
case ResourceCollection:
matches = append(matches, resourceMatch{resourceType: ResourceCollection, resourceID: resourceID})
default:
matches = append(matches, resourceMatch{resourceType: resourceType, resourceID: resourceID})
}
best := PermissionNone
for _, match := range matches {
rows, err := r.db.QueryContext(ctx, `
SELECT permission
FROM permissions
WHERE user_id = ? AND resource_type = ? AND COALESCE(resource_id, '') = ?
`, principal.UserID, string(match.resourceType), match.resourceID)
if err != nil {
return PermissionNone, fmt.Errorf("lookup resource permission: %w", err)
}
for rows.Next() {
var raw string
if err := rows.Scan(&raw); err != nil {
rows.Close()
return PermissionNone, fmt.Errorf("scan resource permission: %w", err)
}
best = maxPermission(best, Permission(raw))
}
if err := rows.Close(); err != nil {
return PermissionNone, err
}
}
return best, nil
}
func scanResourcePermission(rows *sql.Rows) (ResourcePermission, error) {
var (
permission ResourcePermission
resourceType string
resourceID string
rawPermission string
created string
)
if err := rows.Scan(&permission.ID, &permission.UserID, &permission.UserEmail, &permission.UserName, &resourceType, &resourceID, &rawPermission, &permission.GrantedBy, &created); err != nil {
return ResourcePermission{}, fmt.Errorf("scan resource permission: %w", err)
}
permission.ResourceType = ResourceType(resourceType)
permission.ResourceID = resourceID
permission.Permission = Permission(rawPermission)
createdAt, err := time.Parse(time.RFC3339, created)
if err != nil {
return ResourcePermission{}, fmt.Errorf("parse resource permission created_at: %w", err)
}
permission.CreatedAt = createdAt
return permission, nil
}
func normalizeResource(resourceType ResourceType, resourceID string) (ResourceType, string, error) {
resourceType = ResourceType(strings.TrimSpace(strings.ToLower(string(resourceType))))
switch resourceType {
case ResourceGlobal:
return resourceType, "", nil
case ResourceDocument:
resourceID = normalizeDocumentPath(resourceID)
if resourceID == "" {
return "", "", fmt.Errorf("document resource id is required")
}
return resourceType, resourceID, nil
case ResourceFolder:
return resourceType, normalizeFolderPath(resourceID), nil
case ResourceCollection:
resourceID = normalizeCollectionID(resourceID)
if resourceID == "" {
return "", "", fmt.Errorf("collection resource id is required")
}
return resourceType, resourceID, nil
default:
return "", "", fmt.Errorf("invalid resource type")
}
}
func normalizeDocumentPath(raw string) string {
clean := normalizeFolderPath(raw)
if clean == "" {
return ""
}
if !strings.HasSuffix(strings.ToLower(clean), ".md") {
clean += ".md"
}
return clean
}
func normalizeFolderPath(raw string) string {
raw = strings.TrimSpace(strings.Trim(raw, "/"))
if raw == "" {
return ""
}
clean := path.Clean(strings.ReplaceAll(raw, "\\", "/"))
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") {
return ""
}
return strings.Trim(clean, "/")
}
func normalizeCollectionID(raw string) string {
return strings.Trim(strings.TrimSpace(raw), "#/")
}
func folderMatchesForDocument(documentPath string) []string {
folder := path.Dir(documentPath)
if folder == "." {
return []string{""}
}
return folderMatches(folder)
}
func folderMatches(folder string) []string {
folder = normalizeFolderPath(folder)
matches := []string{""}
if folder == "" {
return matches
}
parts := strings.Split(folder, "/")
for index := range parts {
matches = append(matches, strings.Join(parts[:index+1], "/"))
}
return matches
}
func validPermission(permission Permission) bool {
switch permission {
case PermissionNone, PermissionRead, PermissionWrite, PermissionAdmin:
return true
default:
return false
}
}
func maxPermission(left Permission, right Permission) Permission {
if permissionRank(right) > permissionRank(left) {
return right
}
return left
}
func permissionRank(permission Permission) int {
switch permission {
case PermissionRead:
return 1
case PermissionWrite:
return 2
case PermissionAdmin:
return 3
default:
return 0
}
}
func (r *Repository) DeleteUser(ctx context.Context, userID string) error {
result, err := r.db.ExecContext(ctx, `DELETE FROM users WHERE id = ?`, userID)
if err != nil {