Changeset 0.17.1 (#76)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -63,14 +64,14 @@ func (h *TeamHandler) ListTeams(c *gin.Context) {
|
||||
offset := (page - 1) * perPage
|
||||
|
||||
var total int
|
||||
if err := database.DB.QueryRow(`SELECT COUNT(*) FROM teams`).Scan(&total); err != nil {
|
||||
if err := database.DB.QueryRow(database.Q(`SELECT COUNT(*) FROM teams`)).Scan(&total); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to count teams"})
|
||||
return
|
||||
}
|
||||
|
||||
rows, err := database.DB.Query(`
|
||||
rows, err := database.DB.Query(database.Q(`
|
||||
SELECT t.id, t.name, t.description, t.created_by, t.is_active,
|
||||
COALESCE(t.settings::text, '{}'), t.created_at, t.updated_at,
|
||||
COALESCE(t.settings, '{}'), t.created_at, t.updated_at,
|
||||
COALESCE(mc.cnt, 0) AS member_count
|
||||
FROM teams t
|
||||
LEFT JOIN (
|
||||
@@ -78,7 +79,7 @@ func (h *TeamHandler) ListTeams(c *gin.Context) {
|
||||
) mc ON mc.team_id = t.id
|
||||
ORDER BY t.name ASC
|
||||
LIMIT $1 OFFSET $2
|
||||
`, perPage, offset)
|
||||
`), perPage, offset)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||
return
|
||||
@@ -90,9 +91,9 @@ func (h *TeamHandler) ListTeams(c *gin.Context) {
|
||||
var id, name, desc, createdBy, settings string
|
||||
var isActive bool
|
||||
var memberCount int
|
||||
var createdAt, updatedAt sql.NullTime
|
||||
var createdAt, updatedAt time.Time
|
||||
if err := rows.Scan(&id, &name, &desc, &createdBy, &isActive, &settings,
|
||||
&createdAt, &updatedAt, &memberCount); err != nil {
|
||||
database.ST(&createdAt), database.ST(&updatedAt), &memberCount); err != nil {
|
||||
continue
|
||||
}
|
||||
teams = append(teams, gin.H{
|
||||
@@ -103,8 +104,8 @@ func (h *TeamHandler) ListTeams(c *gin.Context) {
|
||||
"is_active": isActive,
|
||||
"settings": settings,
|
||||
"member_count": memberCount,
|
||||
"created_at": createdAt.Time,
|
||||
"updated_at": updatedAt.Time,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
})
|
||||
}
|
||||
if teams == nil {
|
||||
@@ -130,14 +131,13 @@ func (h *TeamHandler) CreateTeam(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
err := database.DB.QueryRow(`
|
||||
id, err := database.InsertReturningID(`
|
||||
INSERT INTO teams (name, description, created_by)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`, req.Name, req.Description, adminID).Scan(&id)
|
||||
`, req.Name, req.Description, adminID)
|
||||
if err != nil {
|
||||
if isUniqueViolation(err) {
|
||||
if database.IsUniqueViolation(err) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "team name already exists"})
|
||||
return
|
||||
}
|
||||
@@ -157,15 +157,15 @@ func (h *TeamHandler) GetTeam(c *gin.Context) {
|
||||
var name, desc, createdBy, settings string
|
||||
var isActive bool
|
||||
var memberCount int
|
||||
var createdAt, updatedAt sql.NullTime
|
||||
var createdAt, updatedAt time.Time
|
||||
|
||||
err := database.DB.QueryRow(`
|
||||
err := database.DB.QueryRow(database.Q(`
|
||||
SELECT t.name, t.description, t.created_by, t.is_active,
|
||||
COALESCE(t.settings::text, '{}'), t.created_at, t.updated_at,
|
||||
COALESCE(t.settings, '{}'), t.created_at, t.updated_at,
|
||||
(SELECT COUNT(*) FROM team_members WHERE team_id = t.id)
|
||||
FROM teams t WHERE t.id = $1
|
||||
`, teamID).Scan(&name, &desc, &createdBy, &isActive, &settings,
|
||||
&createdAt, &updatedAt, &memberCount)
|
||||
`), teamID).Scan(&name, &desc, &createdBy, &isActive, &settings,
|
||||
database.ST(&createdAt), database.ST(&updatedAt), &memberCount)
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "team not found"})
|
||||
return
|
||||
@@ -183,8 +183,8 @@ func (h *TeamHandler) GetTeam(c *gin.Context) {
|
||||
"is_active": isActive,
|
||||
"settings": settings,
|
||||
"member_count": memberCount,
|
||||
"created_at": createdAt.Time,
|
||||
"updated_at": updatedAt.Time,
|
||||
"created_at": createdAt,
|
||||
"updated_at": updatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,23 +204,32 @@ func (h *TeamHandler) UpdateTeam(c *gin.Context) {
|
||||
args := []interface{}{}
|
||||
argN := 1
|
||||
|
||||
if req.Name != nil {
|
||||
sets = append(sets, "name = $"+strconv.Itoa(argN))
|
||||
args = append(args, *req.Name)
|
||||
addArg := func(col string, val interface{}) {
|
||||
if database.IsSQLite() {
|
||||
sets = append(sets, col+" = ?")
|
||||
} else {
|
||||
sets = append(sets, col+" = $"+strconv.Itoa(argN))
|
||||
}
|
||||
args = append(args, val)
|
||||
argN++
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
addArg("name", *req.Name)
|
||||
}
|
||||
if req.Description != nil {
|
||||
sets = append(sets, "description = $"+strconv.Itoa(argN))
|
||||
args = append(args, *req.Description)
|
||||
argN++
|
||||
addArg("description", *req.Description)
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
sets = append(sets, "is_active = $"+strconv.Itoa(argN))
|
||||
args = append(args, *req.IsActive)
|
||||
argN++
|
||||
addArg("is_active", *req.IsActive)
|
||||
}
|
||||
if req.Settings != nil {
|
||||
sets = append(sets, "settings = COALESCE(settings, '{}'::jsonb) || $"+strconv.Itoa(argN)+"::jsonb")
|
||||
if database.IsSQLite() {
|
||||
// SQLite: json_patch for merge
|
||||
sets = append(sets, "settings = json_patch(COALESCE(settings, '{}'), ?)")
|
||||
} else {
|
||||
sets = append(sets, "settings = COALESCE(settings, '{}'::jsonb) || $"+strconv.Itoa(argN)+"::jsonb")
|
||||
}
|
||||
args = append(args, *req.Settings)
|
||||
argN++
|
||||
}
|
||||
@@ -230,19 +239,19 @@ func (h *TeamHandler) UpdateTeam(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
query := "UPDATE teams SET "
|
||||
for i, s := range sets {
|
||||
if i > 0 {
|
||||
query += ", "
|
||||
}
|
||||
query += s
|
||||
var whereClause string
|
||||
if database.IsSQLite() {
|
||||
whereClause = " WHERE id = ?"
|
||||
} else {
|
||||
whereClause = " WHERE id = $" + strconv.Itoa(argN)
|
||||
}
|
||||
query += " WHERE id = $" + strconv.Itoa(argN)
|
||||
args = append(args, teamID)
|
||||
|
||||
query := "UPDATE teams SET " + strings.Join(sets, ", ") + whereClause
|
||||
|
||||
res, err := database.DB.Exec(query, args...)
|
||||
if err != nil {
|
||||
if isUniqueViolation(err) {
|
||||
if database.IsUniqueViolation(err) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "team name already exists"})
|
||||
return
|
||||
}
|
||||
@@ -263,7 +272,7 @@ func (h *TeamHandler) UpdateTeam(c *gin.Context) {
|
||||
func (h *TeamHandler) DeleteTeam(c *gin.Context) {
|
||||
teamID := c.Param("id")
|
||||
|
||||
res, err := database.DB.Exec(`DELETE FROM teams WHERE id = $1`, teamID)
|
||||
res, err := database.DB.Exec(database.Q(`DELETE FROM teams WHERE id = $1`), teamID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"})
|
||||
return
|
||||
@@ -282,14 +291,14 @@ func (h *TeamHandler) DeleteTeam(c *gin.Context) {
|
||||
func (h *TeamHandler) ListMembers(c *gin.Context) {
|
||||
teamID := getTeamID(c)
|
||||
|
||||
rows, err := database.DB.Query(`
|
||||
rows, err := database.DB.Query(database.Q(`
|
||||
SELECT tm.id, tm.user_id, tm.role, tm.joined_at,
|
||||
u.email, COALESCE(u.display_name, '') AS display_name, u.role AS user_role
|
||||
FROM team_members tm
|
||||
JOIN users u ON u.id = tm.user_id
|
||||
WHERE tm.team_id = $1
|
||||
ORDER BY tm.role ASC, u.email ASC
|
||||
`, teamID)
|
||||
`), teamID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||
return
|
||||
@@ -298,8 +307,7 @@ func (h *TeamHandler) ListMembers(c *gin.Context) {
|
||||
|
||||
var members []gin.H
|
||||
for rows.Next() {
|
||||
var id, userID, role, email, displayName, userRole string
|
||||
var joinedAt sql.NullTime
|
||||
var id, userID, role, email, displayName, userRole, joinedAt string
|
||||
if err := rows.Scan(&id, &userID, &role, &joinedAt, &email, &displayName, &userRole); err != nil {
|
||||
continue
|
||||
}
|
||||
@@ -307,7 +315,7 @@ func (h *TeamHandler) ListMembers(c *gin.Context) {
|
||||
"id": id,
|
||||
"user_id": userID,
|
||||
"role": role,
|
||||
"joined_at": joinedAt.Time,
|
||||
"joined_at": joinedAt,
|
||||
"email": email,
|
||||
"display_name": displayName,
|
||||
"user_role": userRole,
|
||||
@@ -333,27 +341,26 @@ func (h *TeamHandler) AddMember(c *gin.Context) {
|
||||
|
||||
// Verify team exists
|
||||
var exists bool
|
||||
database.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM teams WHERE id = $1)`, teamID).Scan(&exists)
|
||||
database.DB.QueryRow(database.Q(`SELECT EXISTS(SELECT 1 FROM teams WHERE id = $1)`), teamID).Scan(&exists)
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "team not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify user exists
|
||||
database.DB.QueryRow(`SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`, req.UserID).Scan(&exists)
|
||||
database.DB.QueryRow(database.Q(`SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)`), req.UserID).Scan(&exists)
|
||||
if !exists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
|
||||
return
|
||||
}
|
||||
|
||||
var id string
|
||||
err := database.DB.QueryRow(`
|
||||
id, err := database.InsertReturningID(`
|
||||
INSERT INTO team_members (team_id, user_id, role)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id
|
||||
`, teamID, req.UserID, req.Role).Scan(&id)
|
||||
`, teamID, req.UserID, req.Role)
|
||||
if err != nil {
|
||||
if isUniqueViolation(err) {
|
||||
if database.IsUniqueViolation(err) {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "user is already a member"})
|
||||
return
|
||||
}
|
||||
@@ -378,9 +385,9 @@ func (h *TeamHandler) UpdateMember(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
res, err := database.DB.Exec(`
|
||||
res, err := database.DB.Exec(database.Q(`
|
||||
UPDATE team_members SET role = $1 WHERE id = $2
|
||||
`, req.Role, memberID)
|
||||
`), req.Role, memberID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"})
|
||||
return
|
||||
@@ -401,7 +408,7 @@ func (h *TeamHandler) UpdateMember(c *gin.Context) {
|
||||
func (h *TeamHandler) RemoveMember(c *gin.Context) {
|
||||
memberID := c.Param("memberId")
|
||||
|
||||
res, err := database.DB.Exec(`DELETE FROM team_members WHERE id = $1`, memberID)
|
||||
res, err := database.DB.Exec(database.Q(`DELETE FROM team_members WHERE id = $1`), memberID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "remove failed"})
|
||||
return
|
||||
@@ -422,16 +429,16 @@ func (h *TeamHandler) RemoveMember(c *gin.Context) {
|
||||
func (h *TeamHandler) MyTeams(c *gin.Context) {
|
||||
userID := getUserID(c)
|
||||
|
||||
rows, err := database.DB.Query(`
|
||||
rows, err := database.DB.Query(database.Q(`
|
||||
SELECT t.id, t.name, t.description, t.is_active,
|
||||
COALESCE(t.settings::text, '{}'),
|
||||
COALESCE(t.settings, '{}'),
|
||||
tm.role AS my_role,
|
||||
(SELECT COUNT(*) FROM team_members WHERE team_id = t.id) AS member_count
|
||||
FROM teams t
|
||||
JOIN team_members tm ON tm.team_id = t.id AND tm.user_id = $1
|
||||
WHERE t.is_active = true
|
||||
ORDER BY t.name ASC
|
||||
`, userID)
|
||||
`), userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||
return
|
||||
@@ -484,7 +491,7 @@ func (h *TeamHandler) ListAvailableModels(c *gin.Context) {
|
||||
models := make([]availableModel, 0)
|
||||
|
||||
// ── 1. Global admin models (synced in model_catalog) ──
|
||||
rows, err := database.DB.Query(`
|
||||
rows, err := database.DB.Query(database.Q(`
|
||||
SELECT mc.id, mc.model_id, mc.display_name, mc.visibility,
|
||||
ac.provider, ac.name as provider_name
|
||||
FROM model_catalog mc
|
||||
@@ -492,7 +499,7 @@ func (h *TeamHandler) ListAvailableModels(c *gin.Context) {
|
||||
WHERE mc.visibility IN ('enabled', 'team')
|
||||
AND ac.is_active = true AND ac.scope = 'global'
|
||||
ORDER BY ac.name, mc.model_id
|
||||
`)
|
||||
`))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
|
||||
return
|
||||
@@ -510,11 +517,11 @@ func (h *TeamHandler) ListAvailableModels(c *gin.Context) {
|
||||
}
|
||||
|
||||
// ── 2. Team provider models (live query) ──
|
||||
teamRows, err := database.DB.Query(`
|
||||
teamRows, err := database.DB.Query(database.Q(`
|
||||
SELECT id, name, provider, endpoint, api_key_enc, headers
|
||||
FROM provider_configs
|
||||
WHERE scope = 'team' AND owner_id = $1 AND is_active = true
|
||||
`, teamID)
|
||||
`), teamID)
|
||||
if err == nil {
|
||||
defer teamRows.Close()
|
||||
for teamRows.Next() {
|
||||
@@ -574,29 +581,26 @@ func getTeamID(c *gin.Context) string {
|
||||
return c.Param("id")
|
||||
}
|
||||
|
||||
// isUniqueViolation checks if a PG error is a unique constraint violation.
|
||||
// isUniqueViolation checks if a PG/SQLite error is a unique constraint violation.
|
||||
func isUniqueViolation(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(err.Error(), "duplicate key")
|
||||
return database.IsUniqueViolation(err)
|
||||
}
|
||||
|
||||
// IsTeamAdmin checks if a user is an admin of the given team.
|
||||
func IsTeamAdmin(userID, teamID string) bool {
|
||||
var role string
|
||||
err := database.DB.QueryRow(`
|
||||
err := database.DB.QueryRow(database.Q(`
|
||||
SELECT role FROM team_members WHERE team_id = $1 AND user_id = $2
|
||||
`, teamID, userID).Scan(&role)
|
||||
`), teamID, userID).Scan(&role)
|
||||
return err == nil && role == "admin"
|
||||
}
|
||||
|
||||
// IsTeamMember checks if a user belongs to the given team (any role).
|
||||
func IsTeamMember(userID, teamID string) bool {
|
||||
var exists bool
|
||||
database.DB.QueryRow(`
|
||||
database.DB.QueryRow(database.Q(`
|
||||
SELECT EXISTS(SELECT 1 FROM team_members WHERE team_id = $1 AND user_id = $2)
|
||||
`, teamID, userID).Scan(&exists)
|
||||
`), teamID, userID).Scan(&exists)
|
||||
return exists
|
||||
}
|
||||
|
||||
@@ -610,24 +614,36 @@ func enforcePrivateProviderPolicy(userID, configID string) error {
|
||||
|
||||
// Check if user belongs to any team with require_private_providers policy
|
||||
var requiresPrivate bool
|
||||
err := database.DB.QueryRow(`
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM team_members tm
|
||||
JOIN teams t ON t.id = tm.team_id
|
||||
WHERE tm.user_id = $1
|
||||
AND t.is_active = true
|
||||
AND t.settings->>'require_private_providers' = 'true'
|
||||
)
|
||||
`, userID).Scan(&requiresPrivate)
|
||||
var query string
|
||||
if database.IsSQLite() {
|
||||
query = `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM team_members tm
|
||||
JOIN teams t ON t.id = tm.team_id
|
||||
WHERE tm.user_id = ?
|
||||
AND t.is_active = 1
|
||||
AND json_extract(t.settings, '$.require_private_providers') = 'true'
|
||||
)`
|
||||
} else {
|
||||
query = `
|
||||
SELECT EXISTS(
|
||||
SELECT 1 FROM team_members tm
|
||||
JOIN teams t ON t.id = tm.team_id
|
||||
WHERE tm.user_id = $1
|
||||
AND t.is_active = true
|
||||
AND t.settings->>'require_private_providers' = 'true'
|
||||
)`
|
||||
}
|
||||
err := database.DB.QueryRow(query, userID).Scan(&requiresPrivate)
|
||||
if err != nil || !requiresPrivate {
|
||||
return nil
|
||||
}
|
||||
|
||||
// User is in a restricted team — verify the config is private
|
||||
var isPrivate bool
|
||||
err = database.DB.QueryRow(`
|
||||
err = database.DB.QueryRow(database.Q(`
|
||||
SELECT COALESCE(is_private, false) FROM provider_configs WHERE id = $1
|
||||
`, configID).Scan(&isPrivate)
|
||||
`), configID).Scan(&isPrivate)
|
||||
if err != nil {
|
||||
return nil // config lookup failed, allow (fail open)
|
||||
}
|
||||
@@ -639,33 +655,33 @@ func enforcePrivateProviderPolicy(userID, configID string) error {
|
||||
|
||||
// ── Team Audit Log (scoped to team members) ─
|
||||
|
||||
// ListTeamAuditLog returns paginated audit entries where the actor is a member
|
||||
// of the specified team. Team admins see only their team's activity; system
|
||||
// admins see everything (but the scoping still applies via the same query).
|
||||
// GET /api/v1/teams/:teamId/audit?page=1&per_page=50&action=...&actor_id=...&resource_type=...
|
||||
func (h *TeamHandler) ListTeamAuditLog(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
page, perPage, offset := parsePagination(c)
|
||||
|
||||
// Build filter clauses — always scoped to team members
|
||||
where := "WHERE al.actor_id IN (SELECT user_id FROM team_members WHERE team_id = $1)"
|
||||
// Build filter clauses — always scoped to team members.
|
||||
// Use ? placeholders and convert for Postgres if needed.
|
||||
clauses := []string{"al.actor_id IN (SELECT user_id FROM team_members WHERE team_id = ?)"}
|
||||
args := []interface{}{teamID}
|
||||
argN := 2
|
||||
|
||||
if action := c.Query("action"); action != "" {
|
||||
where += " AND al.action = $" + strconv.Itoa(argN)
|
||||
clauses = append(clauses, "al.action = ?")
|
||||
args = append(args, action)
|
||||
argN++
|
||||
}
|
||||
if actorID := c.Query("actor_id"); actorID != "" {
|
||||
where += " AND al.actor_id = $" + strconv.Itoa(argN)
|
||||
clauses = append(clauses, "al.actor_id = ?")
|
||||
args = append(args, actorID)
|
||||
argN++
|
||||
}
|
||||
if rt := c.Query("resource_type"); rt != "" {
|
||||
where += " AND al.resource_type = $" + strconv.Itoa(argN)
|
||||
clauses = append(clauses, "al.resource_type = ?")
|
||||
args = append(args, rt)
|
||||
argN++
|
||||
}
|
||||
|
||||
where := "WHERE " + strings.Join(clauses, " AND ")
|
||||
|
||||
// For Postgres, convert ? to $N
|
||||
if database.IsPostgres() {
|
||||
where = convertPlaceholders(where)
|
||||
}
|
||||
|
||||
// Count
|
||||
@@ -679,16 +695,21 @@ func (h *TeamHandler) ListTeamAuditLog(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Query with actor name join
|
||||
limitOffset := fmt.Sprintf("LIMIT %d OFFSET %d", perPage, offset)
|
||||
query := `
|
||||
SELECT al.id, al.actor_id, COALESCE(u.username, '') as actor_name,
|
||||
al.action, al.resource_type, al.resource_id,
|
||||
COALESCE(al.metadata::text, '{}'), al.ip_address, al.created_at
|
||||
COALESCE(al.metadata, '{}'), al.ip_address, al.created_at
|
||||
FROM audit_log al
|
||||
LEFT JOIN users u ON al.actor_id = u.id
|
||||
` + where + `
|
||||
ORDER BY al.created_at DESC
|
||||
LIMIT $` + strconv.Itoa(argN) + ` OFFSET $` + strconv.Itoa(argN+1)
|
||||
args = append(args, perPage, offset)
|
||||
` + limitOffset
|
||||
|
||||
if database.IsPostgres() {
|
||||
// Re-convert placeholders for the full query
|
||||
query = convertPlaceholders(query)
|
||||
}
|
||||
|
||||
rows, err := database.DB.Query(query, args...)
|
||||
if err != nil {
|
||||
@@ -731,18 +752,15 @@ func (h *TeamHandler) ListTeamAuditLog(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ListTeamAuditActions returns distinct action names for audit entries within
|
||||
// the team scope, for filter dropdowns.
|
||||
// GET /api/v1/teams/:teamId/audit/actions
|
||||
func (h *TeamHandler) ListTeamAuditActions(c *gin.Context) {
|
||||
teamID := c.Param("teamId")
|
||||
|
||||
rows, err := database.DB.Query(`
|
||||
rows, err := database.DB.Query(database.Q(`
|
||||
SELECT DISTINCT al.action
|
||||
FROM audit_log al
|
||||
WHERE al.actor_id IN (SELECT user_id FROM team_members WHERE team_id = $1)
|
||||
ORDER BY al.action ASC
|
||||
`, teamID)
|
||||
`), teamID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"actions": []string{}})
|
||||
return
|
||||
@@ -758,3 +776,18 @@ func (h *TeamHandler) ListTeamAuditActions(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"actions": actions})
|
||||
}
|
||||
|
||||
// convertPlaceholders converts ? placeholders to $1, $2, etc. for Postgres.
|
||||
func convertPlaceholders(q string) string {
|
||||
n := 1
|
||||
var result strings.Builder
|
||||
for _, ch := range q {
|
||||
if ch == '?' {
|
||||
result.WriteString(fmt.Sprintf("$%d", n))
|
||||
n++
|
||||
} else {
|
||||
result.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
return result.String()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user