Compare commits
2 Commits
8f51f42b6c
...
d4e0ea811f
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e0ea811f | |||
| c9fba01ef1 |
@@ -9,7 +9,12 @@ import "codit/internal/models"
|
||||
import "codit/internal/util"
|
||||
|
||||
func scanSSHServerGroup(row interface{ Scan(dest ...any) error }, item *models.SSHServerGroup) error {
|
||||
return row.Scan(&item.ID, &item.Name, &item.Description, &item.Enabled, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||
var tagsJSON string
|
||||
var err error
|
||||
err = row.Scan(&item.ID, &item.Name, &item.Description, &item.Enabled, &tagsJSON, &item.CreatedByKind, &item.CreatedBySubjectID, &item.CreatedBySubjectName, &item.CreatedAt, &item.UpdatedAt)
|
||||
if err != nil { return err }
|
||||
item.Tags, err = decodeStringList(tagsJSON)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Store) listSSHServerGroupMemberIDs(groupID string) ([]string, error) {
|
||||
@@ -42,7 +47,7 @@ func (s *Store) ListSSHServerGroups() ([]models.SSHServerGroup, error) {
|
||||
var item models.SSHServerGroup
|
||||
var err error
|
||||
|
||||
rows, err = s.Query(`SELECT public_id, name, description, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups ORDER BY name`)
|
||||
rows, err = s.Query(`SELECT public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups ORDER BY name`)
|
||||
if err != nil { return nil, err }
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
@@ -62,7 +67,7 @@ func (s *Store) GetSSHServerGroup(id string) (models.SSHServerGroup, error) {
|
||||
var item models.SSHServerGroup
|
||||
var err error
|
||||
|
||||
row = s.QueryRow(`SELECT public_id, name, description, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
row = s.QueryRow(`SELECT public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at FROM ssh_server_groups WHERE public_id = ?`, strings.TrimSpace(id))
|
||||
err = scanSSHServerGroup(row, &item)
|
||||
if err != nil { return item, err }
|
||||
item.ServerIDs, err = s.listSSHServerGroupMemberIDs(item.ID)
|
||||
@@ -104,17 +109,21 @@ func (s *Store) CreateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
|
||||
var err error
|
||||
var now int64
|
||||
|
||||
var tagsJSON string
|
||||
if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") }
|
||||
if strings.TrimSpace(item.ID) == "" {
|
||||
item.ID, err = util.NewID()
|
||||
if err != nil { return item, err }
|
||||
}
|
||||
item.Tags = normalizeStringList(item.Tags)
|
||||
tagsJSON, err = encodeStringList(item.Tags)
|
||||
if err != nil { return item, err }
|
||||
now = time.Now().UTC().Unix()
|
||||
item.CreatedAt = now
|
||||
item.UpdatedAt = now
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil { return item, err }
|
||||
_, err = tx.Exec(`INSERT INTO ssh_server_groups (public_id, name, description, enabled, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt)
|
||||
_, err = tx.Exec(`INSERT INTO ssh_server_groups (public_id, name, description, enabled, tags_json, created_by_kind, created_by_subject_id, created_by_subject_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, item.ID, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, tagsJSON, strings.TrimSpace(item.CreatedByKind), strings.TrimSpace(item.CreatedBySubjectID), strings.TrimSpace(item.CreatedBySubjectName), item.CreatedAt, item.UpdatedAt)
|
||||
if err != nil { rollbackIfOwned(tx, owned); return item, err }
|
||||
err = commitIfOwned(tx, owned)
|
||||
if err != nil { return item, err }
|
||||
@@ -127,11 +136,15 @@ func (s *Store) UpdateSSHServerGroup(item models.SSHServerGroup) (models.SSHServ
|
||||
var err error
|
||||
var now int64
|
||||
|
||||
var tagsJSON string
|
||||
if strings.TrimSpace(item.Name) == "" { return item, errors.New("name is required") }
|
||||
item.Tags = normalizeStringList(item.Tags)
|
||||
tagsJSON, err = encodeStringList(item.Tags)
|
||||
if err != nil { return item, err }
|
||||
now = time.Now().UTC().Unix()
|
||||
tx, owned, err = s.begin()
|
||||
if err != nil { return item, err }
|
||||
_, err = tx.Exec(`UPDATE ssh_server_groups SET name = ?, description = ?, enabled = ?, updated_at = ? WHERE public_id = ?`, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, now, strings.TrimSpace(item.ID))
|
||||
_, err = tx.Exec(`UPDATE ssh_server_groups SET name = ?, description = ?, enabled = ?, tags_json = ?, updated_at = ? WHERE public_id = ?`, strings.TrimSpace(item.Name), strings.TrimSpace(item.Description), item.Enabled, tagsJSON, now, strings.TrimSpace(item.ID))
|
||||
if err != nil { rollbackIfOwned(tx, owned); return item, err }
|
||||
err = commitIfOwned(tx, owned)
|
||||
if err != nil { return item, err }
|
||||
|
||||
@@ -73,6 +73,7 @@ type sshServerGroupRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type sshServerGroupMemberRequest struct {
|
||||
@@ -471,7 +472,7 @@ func (api *API) CreateSSHServerGroupAdmin(w http.ResponseWriter, r *http.Request
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
item = models.SSHServerGroup{Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), Enabled: req.Enabled, CreatedByKind: "user", CreatedBySubjectID: user.ID, CreatedBySubjectName: user.Username}
|
||||
item = models.SSHServerGroup{Name: strings.TrimSpace(req.Name), Description: strings.TrimSpace(req.Description), Enabled: req.Enabled, Tags: req.Tags, CreatedByKind: "user", CreatedBySubjectID: user.ID, CreatedBySubjectName: user.Username}
|
||||
item, err = api.store(r).CreateSSHServerGroup(item)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
@@ -498,6 +499,7 @@ func (api *API) UpdateSSHServerGroupAdmin(w http.ResponseWriter, r *http.Request
|
||||
item.Name = strings.TrimSpace(req.Name)
|
||||
item.Description = strings.TrimSpace(req.Description)
|
||||
item.Enabled = req.Enabled
|
||||
item.Tags = req.Tags
|
||||
item, err = api.store(r).UpdateSSHServerGroup(item)
|
||||
if err != nil {
|
||||
WriteJSONWithErrorReason(w, r, http.StatusBadRequest, err.Error())
|
||||
|
||||
@@ -551,6 +551,7 @@ type SSHServerGroup struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Tags []string `json:"tags"`
|
||||
ServerIDs []string `json:"server_ids"`
|
||||
Servers []SSHServer `json:"servers"`
|
||||
CreatedByKind string `json:"created_by_kind"`
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE ssh_server_groups ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]';
|
||||
@@ -936,6 +936,7 @@ export interface SSHServerGroup {
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
tags: string[]
|
||||
server_ids: string[]
|
||||
servers?: SSHServer[]
|
||||
created_by_kind: string
|
||||
@@ -1071,6 +1072,7 @@ export type SSHServerGroupUpsertPayload = {
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export type PKIClientProfileTargetPayload = {
|
||||
|
||||
@@ -79,6 +79,7 @@ export default function SSHServerGroupDetailsDialog(props: SSHServerGroupDetails
|
||||
<TextField label="Group ID" value={item?.id || ''} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Description" value={item?.description || ''} InputProps={{ readOnly: true }} multiline minRows={2} />
|
||||
<TextField label="Status" value={item?.enabled ? 'Enabled' : 'Disabled'} InputProps={{ readOnly: true }} />
|
||||
<TextField label="Tags" value={(item?.tags || []).join(', ') || '-'} InputProps={{ readOnly: true }} />
|
||||
{showAdminFields ? <TextField label="Created By Kind" value={item?.created_by_kind || '-'} InputProps={{ readOnly: true }} /> : null}
|
||||
{showAdminFields ? <TextField label="Created By Subject ID" value={item?.created_by_subject_id || '-'} InputProps={{ readOnly: true }} /> : null}
|
||||
{showAdminFields ? <TextField label="Created By Subject Name" value={item?.created_by_subject_name || '-'} InputProps={{ readOnly: true }} /> : null}
|
||||
|
||||
@@ -14,6 +14,7 @@ export type SSHServerGroupFormState = {
|
||||
name: string
|
||||
description: string
|
||||
enabled: boolean
|
||||
tagsText: string
|
||||
}
|
||||
|
||||
type SSHServerGroupFormDialogProps = {
|
||||
@@ -49,6 +50,12 @@ export default function SSHServerGroupFormDialog(props: SSHServerGroupFormDialog
|
||||
multiline
|
||||
minRows={2}
|
||||
/>
|
||||
<TextField
|
||||
label="Tags"
|
||||
value={props.form.tagsText}
|
||||
onChange={(event) => props.setForm((prev) => ({ ...prev, tagsText: event.target.value }))}
|
||||
helperText="Comma-separated list of tags"
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={<Checkbox checked={props.form.enabled} onChange={(event) => props.setForm((prev) => ({ ...prev, enabled: event.target.checked }))} />}
|
||||
label="Enabled"
|
||||
|
||||
@@ -35,7 +35,8 @@ const emptyForm = (): SSHServerFormState => ({
|
||||
const emptyGroupForm = (): SSHServerGroupFormState => ({
|
||||
name: '',
|
||||
description: '',
|
||||
enabled: true
|
||||
enabled: true,
|
||||
tagsText: ''
|
||||
})
|
||||
|
||||
function fmt(value: number): string {
|
||||
@@ -161,6 +162,16 @@ export default function AdminSSHServersPage() {
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const parseTags = (value: string) =>
|
||||
Array.from(
|
||||
new Set(
|
||||
value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item !== '')
|
||||
)
|
||||
)
|
||||
|
||||
const openGroupCreate = () => {
|
||||
setEditingGroupID(null)
|
||||
setGroupForm(emptyGroupForm())
|
||||
@@ -173,7 +184,8 @@ export default function AdminSSHServersPage() {
|
||||
setGroupForm({
|
||||
name: item.name,
|
||||
description: item.description || '',
|
||||
enabled: item.enabled
|
||||
enabled: item.enabled,
|
||||
tagsText: (item.tags || []).join(', ')
|
||||
})
|
||||
setGroupDialogError(null)
|
||||
setGroupDialogOpen(true)
|
||||
@@ -183,7 +195,8 @@ export default function AdminSSHServersPage() {
|
||||
const payload: SSHServerGroupUpsertPayload = {
|
||||
name: groupForm.name.trim(),
|
||||
description: groupForm.description.trim(),
|
||||
enabled: groupForm.enabled
|
||||
enabled: groupForm.enabled,
|
||||
tags: parseTags(groupForm.tagsText)
|
||||
}
|
||||
if (!payload.name) {
|
||||
setGroupDialogError('Name is required')
|
||||
@@ -298,16 +311,6 @@ export default function AdminSSHServersPage() {
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const parseTags = (value: string) =>
|
||||
Array.from(
|
||||
new Set(
|
||||
value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item !== '')
|
||||
)
|
||||
)
|
||||
|
||||
const handleSave = async () => {
|
||||
const payload: SSHServerUpsertPayload = {
|
||||
name: form.name.trim(),
|
||||
@@ -493,26 +496,36 @@ export default function AdminSSHServersPage() {
|
||||
<Typography component="div">
|
||||
{item.name} ({item.id})
|
||||
{item.enabled? null: (<> <Chip size="small" color='error' label='Disabled'/></>)}
|
||||
{(item.tags || []).map((tag: string) => (
|
||||
<Chip key={tag} label={tag} size="small" sx={{ ml: 0.5 }} />
|
||||
))}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.host}:{item.port}· Tags: {(item.tags || []).join(', ') || '-'}· Updated: {fmt(item.updated_at)}
|
||||
<br />
|
||||
Groups: {serverGroups.length ? serverGroups.map((group: SSHServerGroup, index: number) => (
|
||||
<Box key={group.id} component="span">
|
||||
{index > 0 ? ',' : ''}
|
||||
<Link
|
||||
underline="hover"
|
||||
onClick={() => void openGroupView(group)}
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
{group.name}
|
||||
</Link>
|
||||
</Box>
|
||||
)) : '-'}
|
||||
{item.description? (<><br />{item.description}</>): null}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'grid', gap: 0.25 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.host}:{item.port} · Updated: {fmt(item.updated_at)}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Groups: {serverGroups.length ? serverGroups.map((group: SSHServerGroup, index: number) => (
|
||||
<Box key={group.id} component="span">
|
||||
{index > 0 ? ',' : ''}
|
||||
<Link
|
||||
underline="hover"
|
||||
onClick={() => void openGroupView(group)}
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
{group.name}
|
||||
</Link>
|
||||
</Box>
|
||||
)) : '-'}
|
||||
</Typography>
|
||||
{item.description ? (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{item.description}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<ListRowActions>
|
||||
@@ -563,11 +576,14 @@ export default function AdminSSHServersPage() {
|
||||
<Typography component="div">
|
||||
{item.name} ({item.id})
|
||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||
{(item.tags || []).map((tag: string) => (
|
||||
<Chip key={tag} label={tag} size="small" sx={{ ml: 0.5 }} />
|
||||
))}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{(item.server_ids || []).length} servers · {item.description || '-'}
|
||||
{(item.server_ids || []).length} servers · {item.description || '-'}
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -758,6 +758,9 @@ export default function SSHServersPage() {
|
||||
<Typography component="div">
|
||||
{item.name} ({item.id})
|
||||
{item.enabled ? null : (<> <Chip size="small" color="error" label="Disabled" /></>)}
|
||||
{(item.tags || []).map((tag: string) => (
|
||||
<Chip key={tag} label={tag} size="small" variant="outlined" sx={{ ml: 0.5 }} />
|
||||
))}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
@@ -765,9 +768,6 @@ export default function SSHServersPage() {
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
{item.host}:{item.port}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
Tags: {(item.tags || []).join(', ') || '-'}
|
||||
</Typography>
|
||||
{item.description? (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: 'break-word' }}>
|
||||
{item.description}
|
||||
|
||||
Reference in New Issue
Block a user