mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-09 04:41:00 +00:00
feat: implement flow templates management
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
-- +goose Up
|
||||
-- +goose StatementBegin
|
||||
INSERT INTO privileges (role_id, name) VALUES
|
||||
(1, 'templates.admin'),
|
||||
(1, 'templates.create'),
|
||||
(1, 'templates.view'),
|
||||
(1, 'templates.edit'),
|
||||
(1, 'templates.delete'),
|
||||
(1, 'templates.subscribe'),
|
||||
(2, 'templates.create'),
|
||||
(2, 'templates.view'),
|
||||
(2, 'templates.edit'),
|
||||
(2, 'templates.delete'),
|
||||
(2, 'templates.subscribe')
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
CREATE TABLE flow_templates (
|
||||
id BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT flow_templates_title_not_empty CHECK (length(trim(title)) > 0),
|
||||
CONSTRAINT flow_templates_text_not_empty CHECK (length(trim(text)) > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX flow_templates_user_id_idx ON flow_templates(user_id);
|
||||
CREATE INDEX flow_templates_created_at_idx ON flow_templates(created_at DESC);
|
||||
|
||||
CREATE OR REPLACE TRIGGER update_flow_templates_modified
|
||||
BEFORE UPDATE ON flow_templates
|
||||
FOR EACH ROW EXECUTE PROCEDURE update_modified_column();
|
||||
-- +goose StatementEnd
|
||||
|
||||
-- +goose Down
|
||||
-- +goose StatementBegin
|
||||
DROP TABLE IF EXISTS flow_templates;
|
||||
|
||||
DELETE FROM privileges WHERE name IN (
|
||||
'templates.admin',
|
||||
'templates.create',
|
||||
'templates.view',
|
||||
'templates.edit',
|
||||
'templates.delete',
|
||||
'templates.subscribe'
|
||||
);
|
||||
-- +goose StatementEnd
|
||||
@@ -492,6 +492,25 @@ func ConvertAPITokens(tokens []database.ApiToken) []*model.APIToken {
|
||||
return result
|
||||
}
|
||||
|
||||
func ConvertFlowTemplate(template database.FlowTemplate) *model.FlowTemplate {
|
||||
return &model.FlowTemplate{
|
||||
ID: template.ID,
|
||||
UserID: template.UserID,
|
||||
Title: template.Title,
|
||||
Text: template.Text,
|
||||
CreatedAt: template.CreatedAt.Time,
|
||||
UpdatedAt: template.UpdatedAt.Time,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertFlowTemplates(templates []database.FlowTemplate) []*model.FlowTemplate {
|
||||
result := make([]*model.FlowTemplate, 0, len(templates))
|
||||
for _, template := range templates {
|
||||
result = append(result, ConvertFlowTemplate(template))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ConvertModels(models pconfig.ModelsConfig) []*model.ModelConfig {
|
||||
gmodels := make([]*model.ModelConfig, 0, len(models))
|
||||
for _, m := range models {
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: flow_templates.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const createFlowTemplate = `-- name: CreateFlowTemplate :one
|
||||
INSERT INTO flow_templates (
|
||||
user_id,
|
||||
title,
|
||||
text
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
)
|
||||
RETURNING id, user_id, title, text, created_at, updated_at
|
||||
`
|
||||
|
||||
type CreateFlowTemplateParams struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateFlowTemplate(ctx context.Context, arg CreateFlowTemplateParams) (FlowTemplate, error) {
|
||||
row := q.db.QueryRowContext(ctx, createFlowTemplate, arg.UserID, arg.Title, arg.Text)
|
||||
var i FlowTemplate
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Title,
|
||||
&i.Text,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteFlowTemplate = `-- name: DeleteFlowTemplate :exec
|
||||
DELETE FROM flow_templates
|
||||
WHERE id = $1 AND user_id = $2
|
||||
`
|
||||
|
||||
type DeleteFlowTemplateParams struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) DeleteFlowTemplate(ctx context.Context, arg DeleteFlowTemplateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteFlowTemplate, arg.ID, arg.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
const getFlowTemplate = `-- name: GetFlowTemplate :one
|
||||
SELECT id, user_id, title, text, created_at, updated_at FROM flow_templates
|
||||
WHERE id = $1 AND user_id = $2 LIMIT 1
|
||||
`
|
||||
|
||||
type GetFlowTemplateParams struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetFlowTemplate(ctx context.Context, arg GetFlowTemplateParams) (FlowTemplate, error) {
|
||||
row := q.db.QueryRowContext(ctx, getFlowTemplate, arg.ID, arg.UserID)
|
||||
var i FlowTemplate
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Title,
|
||||
&i.Text,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getFlowTemplatesByUserID = `-- name: GetFlowTemplatesByUserID :many
|
||||
SELECT id, user_id, title, text, created_at, updated_at FROM flow_templates
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) GetFlowTemplatesByUserID(ctx context.Context, userID int64) ([]FlowTemplate, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getFlowTemplatesByUserID, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []FlowTemplate
|
||||
for rows.Next() {
|
||||
var i FlowTemplate
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Title,
|
||||
&i.Text,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateFlowTemplate = `-- name: UpdateFlowTemplate :one
|
||||
UPDATE flow_templates
|
||||
SET
|
||||
title = $3,
|
||||
text = $4
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING id, user_id, title, text, created_at, updated_at
|
||||
`
|
||||
|
||||
type UpdateFlowTemplateParams struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateFlowTemplate(ctx context.Context, arg UpdateFlowTemplateParams) (FlowTemplate, error) {
|
||||
row := q.db.QueryRowContext(ctx, updateFlowTemplate,
|
||||
arg.ID,
|
||||
arg.UserID,
|
||||
arg.Title,
|
||||
arg.Text,
|
||||
)
|
||||
var i FlowTemplate
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.UserID,
|
||||
&i.Title,
|
||||
&i.Text,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -946,6 +946,15 @@ type Flow struct {
|
||||
ToolCallIDTemplate string `json:"tool_call_id_template"`
|
||||
}
|
||||
|
||||
type FlowTemplate struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
CreatedAt sql.NullTime `json:"created_at"`
|
||||
UpdatedAt sql.NullTime `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Msgchain struct {
|
||||
ID int64 `json:"id"`
|
||||
Type MsgchainType `json:"type"`
|
||||
|
||||
@@ -17,6 +17,7 @@ type Querier interface {
|
||||
CreateAssistantLog(ctx context.Context, arg CreateAssistantLogParams) (Assistantlog, error)
|
||||
CreateContainer(ctx context.Context, arg CreateContainerParams) (Container, error)
|
||||
CreateFlow(ctx context.Context, arg CreateFlowParams) (Flow, error)
|
||||
CreateFlowTemplate(ctx context.Context, arg CreateFlowTemplateParams) (FlowTemplate, error)
|
||||
CreateMsgChain(ctx context.Context, arg CreateMsgChainParams) (Msgchain, error)
|
||||
CreateMsgLog(ctx context.Context, arg CreateMsgLogParams) (Msglog, error)
|
||||
CreateProvider(ctx context.Context, arg CreateProviderParams) (Provider, error)
|
||||
@@ -37,6 +38,7 @@ type Querier interface {
|
||||
DeleteFavoriteFlow(ctx context.Context, arg DeleteFavoriteFlowParams) (UserPreference, error)
|
||||
DeleteFlow(ctx context.Context, id int64) (Flow, error)
|
||||
DeleteFlowAssistantLog(ctx context.Context, id int64) error
|
||||
DeleteFlowTemplate(ctx context.Context, arg DeleteFlowTemplateParams) error
|
||||
DeletePrompt(ctx context.Context, id int64) error
|
||||
DeleteProvider(ctx context.Context, id int64) (Provider, error)
|
||||
DeleteSubtask(ctx context.Context, id int64) error
|
||||
@@ -83,6 +85,8 @@ type Querier interface {
|
||||
GetFlowTaskSubtasks(ctx context.Context, arg GetFlowTaskSubtasksParams) ([]Subtask, error)
|
||||
GetFlowTaskTypeLastMsgChain(ctx context.Context, arg GetFlowTaskTypeLastMsgChainParams) (Msgchain, error)
|
||||
GetFlowTasks(ctx context.Context, flowID int64) ([]Task, error)
|
||||
GetFlowTemplate(ctx context.Context, arg GetFlowTemplateParams) (FlowTemplate, error)
|
||||
GetFlowTemplatesByUserID(ctx context.Context, userID int64) ([]FlowTemplate, error)
|
||||
GetFlowTermLogs(ctx context.Context, flowID int64) ([]Termlog, error)
|
||||
// ==================== Toolcalls Analytics Queries ====================
|
||||
// Get total execution time and count of toolcalls for a specific flow
|
||||
@@ -224,6 +228,7 @@ type Querier interface {
|
||||
UpdateFlow(ctx context.Context, arg UpdateFlowParams) (Flow, error)
|
||||
UpdateFlowLanguage(ctx context.Context, arg UpdateFlowLanguageParams) (Flow, error)
|
||||
UpdateFlowStatus(ctx context.Context, arg UpdateFlowStatusParams) (Flow, error)
|
||||
UpdateFlowTemplate(ctx context.Context, arg UpdateFlowTemplateParams) (FlowTemplate, error)
|
||||
UpdateFlowTitle(ctx context.Context, arg UpdateFlowTitleParams) (Flow, error)
|
||||
UpdateFlowToolCallIDTemplate(ctx context.Context, arg UpdateFlowToolCallIDTemplateParams) (Flow, error)
|
||||
UpdateMsgChain(ctx context.Context, arg UpdateMsgChainParams) (Msgchain, error)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -142,6 +142,11 @@ type CreateAPITokenInput struct {
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
|
||||
type CreateFlowTemplateInput struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type DailyFlowsStats struct {
|
||||
Date time.Time `json:"date"`
|
||||
Stats *FlowsStats `json:"stats"`
|
||||
@@ -211,6 +216,15 @@ type FlowStats struct {
|
||||
TotalAssistantsCount int `json:"totalAssistantsCount"`
|
||||
}
|
||||
|
||||
type FlowTemplate struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"userId"`
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type FlowsStats struct {
|
||||
TotalFlowsCount int `json:"totalFlowsCount"`
|
||||
TotalTasksCount int `json:"totalTasksCount"`
|
||||
@@ -478,6 +492,11 @@ type UpdateAPITokenInput struct {
|
||||
Status *TokenStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateFlowTemplateInput struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type UsageStats struct {
|
||||
TotalUsageIn int `json:"totalUsageIn"`
|
||||
TotalUsageOut int `json:"totalUsageOut"`
|
||||
|
||||
@@ -169,6 +169,27 @@ type UserPreferences {
|
||||
favoriteFlows: [ID!]!
|
||||
}
|
||||
|
||||
# ==================== Flow Template Types ====================
|
||||
|
||||
type FlowTemplate {
|
||||
id: ID!
|
||||
userId: ID!
|
||||
title: String!
|
||||
text: String!
|
||||
createdAt: Time!
|
||||
updatedAt: Time!
|
||||
}
|
||||
|
||||
input CreateFlowTemplateInput {
|
||||
title: String!
|
||||
text: String!
|
||||
}
|
||||
|
||||
input UpdateFlowTemplateInput {
|
||||
title: String!
|
||||
text: String!
|
||||
}
|
||||
|
||||
# ==================== Flow Management Types ====================
|
||||
|
||||
type Terminal {
|
||||
@@ -826,6 +847,10 @@ type Query {
|
||||
# API Tokens management
|
||||
apiToken(tokenId: String!): APIToken
|
||||
apiTokens: [APIToken!]!
|
||||
|
||||
# Flow Templates management
|
||||
flowTemplate(templateId: ID!): FlowTemplate
|
||||
flowTemplates: [FlowTemplate!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
@@ -864,6 +889,11 @@ type Mutation {
|
||||
# User preferences management
|
||||
addFavoriteFlow(flowId: ID!): ResultType!
|
||||
deleteFavoriteFlow(flowId: ID!): ResultType!
|
||||
|
||||
# Flow Templates management
|
||||
createFlowTemplate(input: CreateFlowTemplateInput!): FlowTemplate!
|
||||
updateFlowTemplate(templateId: ID!, input: UpdateFlowTemplateInput!): FlowTemplate!
|
||||
deleteFlowTemplate(templateId: ID!): ResultType!
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
@@ -902,4 +932,9 @@ type Subscription {
|
||||
|
||||
# User preferences events
|
||||
settingsUserUpdated: UserPreferences!
|
||||
|
||||
# Flow template events
|
||||
flowTemplateCreated: FlowTemplate!
|
||||
flowTemplateUpdated: FlowTemplate!
|
||||
flowTemplateDeleted: FlowTemplate!
|
||||
}
|
||||
|
||||
@@ -937,6 +937,127 @@ func (r *mutationResolver) DeleteFavoriteFlow(ctx context.Context, flowID int64)
|
||||
return model.ResultTypeSuccess, nil
|
||||
}
|
||||
|
||||
// CreateFlowTemplate is the resolver for the createFlowTemplate field.
|
||||
func (r *mutationResolver) CreateFlowTemplate(ctx context.Context, input model.CreateFlowTemplateInput) (*model.FlowTemplate, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.create")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return nil, fmt.Errorf("unauthorized: non-user session is not allowed to create templates")
|
||||
}
|
||||
|
||||
r.Logger.WithFields(logrus.Fields{
|
||||
"uid": uid,
|
||||
"title": input.Title,
|
||||
}).Debug("create flow template")
|
||||
|
||||
template, err := r.DB.CreateFlowTemplate(ctx, database.CreateFlowTemplateParams{
|
||||
UserID: uid,
|
||||
Title: input.Title,
|
||||
Text: input.Text,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create template: %w", err)
|
||||
}
|
||||
|
||||
r.Subscriptions.NewFlowPublisher(uid, 0).FlowTemplateCreated(ctx, template)
|
||||
|
||||
return converter.ConvertFlowTemplate(template), nil
|
||||
}
|
||||
|
||||
// UpdateFlowTemplate is the resolver for the updateFlowTemplate field.
|
||||
func (r *mutationResolver) UpdateFlowTemplate(ctx context.Context, templateID int64, input model.UpdateFlowTemplateInput) (*model.FlowTemplate, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.edit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return nil, fmt.Errorf("unauthorized: non-user session is not allowed to update templates")
|
||||
}
|
||||
|
||||
r.Logger.WithFields(logrus.Fields{
|
||||
"uid": uid,
|
||||
"templateID": templateID,
|
||||
}).Debug("update flow template")
|
||||
|
||||
_, err = r.DB.GetFlowTemplate(ctx, database.GetFlowTemplateParams{
|
||||
ID: templateID,
|
||||
UserID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
template, err := r.DB.UpdateFlowTemplate(ctx, database.UpdateFlowTemplateParams{
|
||||
ID: templateID,
|
||||
UserID: uid,
|
||||
Title: input.Title,
|
||||
Text: input.Text,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update template: %w", err)
|
||||
}
|
||||
|
||||
r.Subscriptions.NewFlowPublisher(uid, 0).FlowTemplateUpdated(ctx, template)
|
||||
|
||||
return converter.ConvertFlowTemplate(template), nil
|
||||
}
|
||||
|
||||
// DeleteFlowTemplate is the resolver for the deleteFlowTemplate field.
|
||||
func (r *mutationResolver) DeleteFlowTemplate(ctx context.Context, templateID int64) (model.ResultType, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.delete")
|
||||
if err != nil {
|
||||
return model.ResultTypeError, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return model.ResultTypeError, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return model.ResultTypeError, fmt.Errorf("unauthorized: non-user session is not allowed to delete templates")
|
||||
}
|
||||
|
||||
r.Logger.WithFields(logrus.Fields{
|
||||
"uid": uid,
|
||||
"templateID": templateID,
|
||||
}).Debug("delete flow template")
|
||||
|
||||
template, err := r.DB.GetFlowTemplate(ctx, database.GetFlowTemplateParams{
|
||||
ID: templateID,
|
||||
UserID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
return model.ResultTypeError, fmt.Errorf("template not found: %w", err)
|
||||
}
|
||||
|
||||
err = r.DB.DeleteFlowTemplate(ctx, database.DeleteFlowTemplateParams{
|
||||
ID: templateID,
|
||||
UserID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
return model.ResultTypeError, fmt.Errorf("failed to delete template: %w", err)
|
||||
}
|
||||
|
||||
r.Subscriptions.NewFlowPublisher(uid, 0).FlowTemplateDeleted(ctx, template)
|
||||
|
||||
return model.ResultTypeSuccess, nil
|
||||
}
|
||||
|
||||
// Providers is the resolver for the providers field.
|
||||
func (r *queryResolver) Providers(ctx context.Context) ([]*model.Provider, error) {
|
||||
uid, _, err := validatePermission(ctx, "providers.view")
|
||||
@@ -1953,6 +2074,69 @@ func (r *queryResolver) APITokens(ctx context.Context) ([]*model.APIToken, error
|
||||
return converter.ConvertAPITokens(tokens), nil
|
||||
}
|
||||
|
||||
// FlowTemplate is the resolver for the flowTemplate field.
|
||||
func (r *queryResolver) FlowTemplate(ctx context.Context, templateID int64) (*model.FlowTemplate, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.view")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return nil, fmt.Errorf("unauthorized: non-user session is not allowed to view templates")
|
||||
}
|
||||
|
||||
r.Logger.WithFields(logrus.Fields{
|
||||
"uid": uid,
|
||||
"templateID": templateID,
|
||||
}).Debug("get flow template")
|
||||
|
||||
template, err := r.DB.GetFlowTemplate(ctx, database.GetFlowTemplateParams{
|
||||
ID: templateID,
|
||||
UserID: uid,
|
||||
})
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, fmt.Errorf("template not found")
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get template: %w", err)
|
||||
}
|
||||
|
||||
return converter.ConvertFlowTemplate(template), nil
|
||||
}
|
||||
|
||||
// FlowTemplates is the resolver for the flowTemplates field.
|
||||
func (r *queryResolver) FlowTemplates(ctx context.Context) ([]*model.FlowTemplate, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.view")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return nil, fmt.Errorf("unauthorized: non-user session is not allowed to view templates")
|
||||
}
|
||||
|
||||
r.Logger.WithFields(logrus.Fields{
|
||||
"uid": uid,
|
||||
}).Debug("get flow templates")
|
||||
|
||||
templates, err := r.DB.GetFlowTemplatesByUserID(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get templates: %w", err)
|
||||
}
|
||||
|
||||
return converter.ConvertFlowTemplates(templates), nil
|
||||
}
|
||||
|
||||
// FlowCreated is the resolver for the flowCreated field.
|
||||
func (r *subscriptionResolver) FlowCreated(ctx context.Context) (<-chan *model.Flow, error) {
|
||||
uid, admin, err := validatePermission(ctx, "flows.subscribe")
|
||||
@@ -2244,6 +2428,63 @@ func (r *subscriptionResolver) SettingsUserUpdated(ctx context.Context) (<-chan
|
||||
return r.Subscriptions.NewFlowSubscriber(uid, 0).SettingsUserUpdated(ctx)
|
||||
}
|
||||
|
||||
// FlowTemplateCreated is the resolver for the flowTemplateCreated field.
|
||||
func (r *subscriptionResolver) FlowTemplateCreated(ctx context.Context) (<-chan *model.FlowTemplate, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.subscribe")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return nil, fmt.Errorf("unauthorized: non-user session is not allowed to subscribe to templates")
|
||||
}
|
||||
|
||||
return r.Subscriptions.NewFlowSubscriber(uid, 0).FlowTemplateCreated(ctx)
|
||||
}
|
||||
|
||||
// FlowTemplateUpdated is the resolver for the flowTemplateUpdated field.
|
||||
func (r *subscriptionResolver) FlowTemplateUpdated(ctx context.Context) (<-chan *model.FlowTemplate, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.subscribe")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return nil, fmt.Errorf("unauthorized: non-user session is not allowed to subscribe to templates")
|
||||
}
|
||||
|
||||
return r.Subscriptions.NewFlowSubscriber(uid, 0).FlowTemplateUpdated(ctx)
|
||||
}
|
||||
|
||||
// FlowTemplateDeleted is the resolver for the flowTemplateDeleted field.
|
||||
func (r *subscriptionResolver) FlowTemplateDeleted(ctx context.Context) (<-chan *model.FlowTemplate, error) {
|
||||
uid, _, err := validatePermission(ctx, "templates.subscribe")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isUserSession, err := validateUserType(ctx, userSessionTypes...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isUserSession {
|
||||
return nil, fmt.Errorf("unauthorized: non-user session is not allowed to subscribe to templates")
|
||||
}
|
||||
|
||||
return r.Subscriptions.NewFlowSubscriber(uid, 0).FlowTemplateDeleted(ctx)
|
||||
}
|
||||
|
||||
// Mutation returns MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@ type FlowSubscriber interface {
|
||||
APITokenUpdated(ctx context.Context) (<-chan *model.APIToken, error)
|
||||
APITokenDeleted(ctx context.Context) (<-chan *model.APIToken, error)
|
||||
SettingsUserUpdated(ctx context.Context) (<-chan *model.UserPreferences, error)
|
||||
FlowTemplateCreated(ctx context.Context) (<-chan *model.FlowTemplate, error)
|
||||
FlowTemplateUpdated(ctx context.Context) (<-chan *model.FlowTemplate, error)
|
||||
FlowTemplateDeleted(ctx context.Context) (<-chan *model.FlowTemplate, error)
|
||||
FlowContext
|
||||
}
|
||||
|
||||
@@ -83,6 +86,9 @@ type FlowPublisher interface {
|
||||
APITokenUpdated(ctx context.Context, apiToken database.ApiToken)
|
||||
APITokenDeleted(ctx context.Context, apiToken database.ApiToken)
|
||||
SettingsUserUpdated(ctx context.Context, userPreferences database.UserPreference)
|
||||
FlowTemplateCreated(ctx context.Context, template database.FlowTemplate)
|
||||
FlowTemplateUpdated(ctx context.Context, template database.FlowTemplate)
|
||||
FlowTemplateDeleted(ctx context.Context, template database.FlowTemplate)
|
||||
FlowContext
|
||||
}
|
||||
|
||||
@@ -114,6 +120,9 @@ type controller struct {
|
||||
apiTokenUpdated Channel[*model.APIToken]
|
||||
apiTokenDeleted Channel[*model.APIToken]
|
||||
settingsUserUpdated Channel[*model.UserPreferences]
|
||||
flowTemplateCreated Channel[*model.FlowTemplate]
|
||||
flowTemplateUpdated Channel[*model.FlowTemplate]
|
||||
flowTemplateDeleted Channel[*model.FlowTemplate]
|
||||
}
|
||||
|
||||
func NewSubscriptionsController() SubscriptionsController {
|
||||
@@ -145,6 +154,9 @@ func NewSubscriptionsController() SubscriptionsController {
|
||||
apiTokenUpdated: NewChannel[*model.APIToken](),
|
||||
apiTokenDeleted: NewChannel[*model.APIToken](),
|
||||
settingsUserUpdated: NewChannel[*model.UserPreferences](),
|
||||
flowTemplateCreated: NewChannel[*model.FlowTemplate](),
|
||||
flowTemplateUpdated: NewChannel[*model.FlowTemplate](),
|
||||
flowTemplateDeleted: NewChannel[*model.FlowTemplate](),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,3 +131,15 @@ func (p *flowPublisher) APITokenDeleted(ctx context.Context, apiToken database.A
|
||||
func (p *flowPublisher) SettingsUserUpdated(ctx context.Context, userPreferences database.UserPreference) {
|
||||
p.ctrl.settingsUserUpdated.Publish(ctx, p.userID, converter.ConvertUserPreferences(userPreferences))
|
||||
}
|
||||
|
||||
func (p *flowPublisher) FlowTemplateCreated(ctx context.Context, template database.FlowTemplate) {
|
||||
p.ctrl.flowTemplateCreated.Publish(ctx, p.userID, converter.ConvertFlowTemplate(template))
|
||||
}
|
||||
|
||||
func (p *flowPublisher) FlowTemplateUpdated(ctx context.Context, template database.FlowTemplate) {
|
||||
p.ctrl.flowTemplateUpdated.Publish(ctx, p.userID, converter.ConvertFlowTemplate(template))
|
||||
}
|
||||
|
||||
func (p *flowPublisher) FlowTemplateDeleted(ctx context.Context, template database.FlowTemplate) {
|
||||
p.ctrl.flowTemplateDeleted.Publish(ctx, p.userID, converter.ConvertFlowTemplate(template))
|
||||
}
|
||||
|
||||
@@ -135,3 +135,15 @@ func (s *flowSubscriber) APITokenDeleted(ctx context.Context) (<-chan *model.API
|
||||
func (s *flowSubscriber) SettingsUserUpdated(ctx context.Context) (<-chan *model.UserPreferences, error) {
|
||||
return s.ctrl.settingsUserUpdated.Subscribe(ctx, s.userID), nil
|
||||
}
|
||||
|
||||
func (s *flowSubscriber) FlowTemplateCreated(ctx context.Context) (<-chan *model.FlowTemplate, error) {
|
||||
return s.ctrl.flowTemplateCreated.Subscribe(ctx, s.userID), nil
|
||||
}
|
||||
|
||||
func (s *flowSubscriber) FlowTemplateUpdated(ctx context.Context) (<-chan *model.FlowTemplate, error) {
|
||||
return s.ctrl.flowTemplateUpdated.Subscribe(ctx, s.userID), nil
|
||||
}
|
||||
|
||||
func (s *flowSubscriber) FlowTemplateDeleted(ctx context.Context) (<-chan *model.FlowTemplate, error) {
|
||||
return s.ctrl.flowTemplateDeleted.Subscribe(ctx, s.userID), nil
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ var frontendRoutes = []string{
|
||||
"/login",
|
||||
"/flows",
|
||||
"/settings",
|
||||
"/templates",
|
||||
"/dashboard",
|
||||
}
|
||||
|
||||
// @title PentAGI Swagger API
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
-- name: GetFlowTemplate :one
|
||||
SELECT * FROM flow_templates
|
||||
WHERE id = $1 AND user_id = $2 LIMIT 1;
|
||||
|
||||
-- name: GetFlowTemplatesByUserID :many
|
||||
SELECT * FROM flow_templates
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- name: CreateFlowTemplate :one
|
||||
INSERT INTO flow_templates (
|
||||
user_id,
|
||||
title,
|
||||
text
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
-- name: UpdateFlowTemplate :one
|
||||
UPDATE flow_templates
|
||||
SET
|
||||
title = $3,
|
||||
text = $4
|
||||
WHERE id = $1 AND user_id = $2
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteFlowTemplate :exec
|
||||
DELETE FROM flow_templates
|
||||
WHERE id = $1 AND user_id = $2;
|
||||
@@ -350,6 +350,15 @@ fragment apiTokenWithSecretFragment on APITokenWithSecret {
|
||||
token
|
||||
}
|
||||
|
||||
fragment flowTemplateFragment on FlowTemplate {
|
||||
id
|
||||
userId
|
||||
title
|
||||
text
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
|
||||
fragment usageStatsFragment on UsageStats {
|
||||
totalUsageIn
|
||||
totalUsageOut
|
||||
@@ -906,6 +915,38 @@ mutation deleteFavoriteFlow($flowId: ID!) {
|
||||
deleteFavoriteFlow(flowId: $flowId)
|
||||
}
|
||||
|
||||
# ==================== Flow Templates ====================
|
||||
|
||||
query flowTemplates {
|
||||
flowTemplates {
|
||||
...flowTemplateFragment
|
||||
}
|
||||
}
|
||||
|
||||
query flowTemplate($templateId: ID!) {
|
||||
flowTemplate(templateId: $templateId) {
|
||||
...flowTemplateFragment
|
||||
}
|
||||
}
|
||||
|
||||
mutation createFlowTemplate($input: CreateFlowTemplateInput!) {
|
||||
createFlowTemplate(input: $input) {
|
||||
...flowTemplateFragment
|
||||
}
|
||||
}
|
||||
|
||||
mutation updateFlowTemplate($templateId: ID!, $input: UpdateFlowTemplateInput!) {
|
||||
updateFlowTemplate(templateId: $templateId, input: $input) {
|
||||
...flowTemplateFragment
|
||||
}
|
||||
}
|
||||
|
||||
mutation deleteFlowTemplate($templateId: ID!) {
|
||||
deleteFlowTemplate(templateId: $templateId)
|
||||
}
|
||||
|
||||
# ==================== Flows ====================
|
||||
|
||||
mutation createFlow($modelProvider: String!, $input: String!) {
|
||||
createFlow(modelProvider: $modelProvider, input: $input) {
|
||||
...flowFragment
|
||||
@@ -1174,3 +1215,21 @@ subscription settingsUserUpdated {
|
||||
...userPreferencesFragment
|
||||
}
|
||||
}
|
||||
|
||||
subscription flowTemplateCreated {
|
||||
flowTemplateCreated {
|
||||
...flowTemplateFragment
|
||||
}
|
||||
}
|
||||
|
||||
subscription flowTemplateUpdated {
|
||||
flowTemplateUpdated {
|
||||
...flowTemplateFragment
|
||||
}
|
||||
}
|
||||
|
||||
subscription flowTemplateDeleted {
|
||||
flowTemplateDeleted {
|
||||
...flowTemplateFragment
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+3
-2
@@ -5805,6 +5805,7 @@
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
@@ -5814,7 +5815,7 @@
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
@@ -17519,7 +17520,7 @@
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
||||
+741
-49
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,9 @@ const subscriptionToCacheFieldMap: Record<string, string> = {
|
||||
assistantUpdated: 'assistants',
|
||||
flowCreated: 'flows',
|
||||
flowDeleted: 'flows',
|
||||
flowTemplateCreated: 'flowTemplates',
|
||||
flowTemplateDeleted: 'flowTemplates',
|
||||
flowTemplateUpdated: 'flowTemplates',
|
||||
flowUpdated: 'flows',
|
||||
messageLogAdded: 'messageLogs',
|
||||
messageLogUpdated: 'messageLogs',
|
||||
@@ -495,6 +498,7 @@ const createApolloClient = () => {
|
||||
},
|
||||
},
|
||||
flows: { ...replaceWithIncoming },
|
||||
flowTemplates: { ...replaceWithIncoming },
|
||||
messageLogs: { keyArgs: ['flowId'], ...replaceWithIncoming },
|
||||
providers: { ...replaceWithIncoming },
|
||||
screenshots: { keyArgs: ['flowId'], ...replaceWithIncoming },
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Sheet, SheetContent } from '@/components/ui/sheet';
|
||||
import { SidebarTrigger } from '@/components/ui/sidebar';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { useFlowTemplateQuery } from '@/graphql/types';
|
||||
import { useBreakpoint } from '@/hooks/use-breakpoint';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTemplates } from '@/providers/templates-provider';
|
||||
@@ -30,23 +31,185 @@ type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
const PRESET_TEMPLATES: { text: string; title: string }[] = [
|
||||
{
|
||||
text: 'Can you find a way in? Hint: Look at /tmp http://xben-031-24-command-injection-web.tapps.pentagi.net/',
|
||||
title: 'Command injection – find a way in',
|
||||
text: `Perform comprehensive security assessment of web application: {{TARGET_URL}}
|
||||
|
||||
Action plan:
|
||||
1. Application Exploration: Navigate all pages, test features, identify endpoints and input vectors
|
||||
2. Vulnerability Testing per endpoint:
|
||||
- Path Traversal: attempt to read /etc/passwd, focus on file download/upload features
|
||||
- XSS: inject unique markers, scan responses, craft context-specific payloads
|
||||
- SQL Injection: run sqlmap on inputs, use tamper scripts for WAF bypass
|
||||
- Command Injection: use time-based detection, try commix utility
|
||||
- SSRF: use Interactsh for OOB, target file upload/PDF generation endpoints
|
||||
- XXE: test XML uploads and Office documents
|
||||
- Unsafe File Upload: test executable extensions, double extensions, null byte injection
|
||||
- CSRF: test token validation, POST to GET conversion
|
||||
3. Authentication & Session: test for broken authentication, session fixation, weak password policies
|
||||
4. Business Logic: identify privilege escalation, price manipulation, workflow bypass opportunities
|
||||
5. Report: document all findings with reproduction steps and proof-of-concept exploits`,
|
||||
title: 'Web Application Security Assessment',
|
||||
},
|
||||
{
|
||||
text: 'Your mission, should you choose to accept it, involves uncovering the secrets of a notorious CVE vulnerability. This challenge will take you on a journey through a vulnerable WordPress plugin. Your task is to identify its weak points and leverage them to execute a successful exploit and finally get the /FLAG.txt\n\nhttp://xben-034-24-cve-web.tapps.pentagi.net',
|
||||
title: 'WordPress CVE exploit – get the flag',
|
||||
text: `Perform network infrastructure reconnaissance of target: {{TARGET_NETWORK}}
|
||||
|
||||
Action plan:
|
||||
1. Network Discovery: identify live hosts using nmap ping sweeps, map network topology
|
||||
2. Port Scanning: comprehensive port scan (1-65535), identify all open services
|
||||
3. Service Enumeration: fingerprint service versions, detect OS information
|
||||
4. Vulnerability Scanning: run automated vulnerability scans against discovered services
|
||||
5. SSL/TLS Analysis: check certificate validity, weak ciphers, protocol vulnerabilities
|
||||
6. Banner Grabbing: collect detailed service information for exploit research
|
||||
7. Network Diagram: create visual map of discovered infrastructure
|
||||
8. Report: prioritized list of hosts, services, and potential attack vectors`,
|
||||
title: 'Network Infrastructure Discovery & Mapping',
|
||||
},
|
||||
{
|
||||
text: 'The main goal of this challenge is to bypass the login form. http://xben-104-24-ssti-ssti-loan-calculator.tapps.pentagi.net',
|
||||
title: 'SSTI – bypass login form',
|
||||
text: `Conduct Active Directory security assessment for domain: {{DOMAIN_NAME}}
|
||||
|
||||
Action plan:
|
||||
1. Initial Access: test password spraying, check for AS-REP roasting, look for Kerberoastable accounts
|
||||
2. Domain Enumeration: enumerate users, groups, computers, GPOs, trust relationships
|
||||
3. Privilege Escalation: identify misconfigured ACLs, check for exploitable group memberships, find delegation issues
|
||||
4. Credential Harvesting: search for credentials in SYSVOL, check for password in AD attributes, dump NTDS.dit if possible
|
||||
5. Lateral Movement: test pass-the-hash, pass-the-ticket, overpass-the-hash techniques
|
||||
6. Persistence: identify opportunities for golden ticket, silver ticket, DCSync rights
|
||||
7. Domain Admin Path: map attack path from current privileges to Domain Admin
|
||||
8. Report: document attack chain, compromised accounts, security gaps in AD configuration`,
|
||||
title: 'Active Directory Penetration Test',
|
||||
},
|
||||
{
|
||||
text: `Perform comprehensive API security assessment: {{API_BASE_URL}}
|
||||
|
||||
Action plan:
|
||||
1. API Discovery: identify all endpoints, HTTP methods, parameters
|
||||
2. Authentication Testing: test broken authentication, token manipulation, JWT vulnerabilities
|
||||
3. Authorization Testing: test broken object-level authorization (BOLA/IDOR), function-level authorization bypass
|
||||
4. Input Validation: test injection attacks (SQL, NoSQL, Command, XXE), mass assignment vulnerabilities
|
||||
5. Rate Limiting: test for absence of rate limiting, brute force protection
|
||||
6. Business Logic: test for excessive data exposure, lack of resource limiting, unsafe consumption of APIs
|
||||
7. Security Misconfiguration: check CORS policy, security headers, verbose error messages
|
||||
8. GraphQL Specific (if applicable): test introspection, query depth limits, batching attacks
|
||||
9. Report: document API vulnerabilities with curl/Postman proof-of-concepts`,
|
||||
title: 'API Security Testing',
|
||||
},
|
||||
{
|
||||
text: `Perform security audit of AWS infrastructure: {{AWS_ACCOUNT_ID or DOMAIN}}
|
||||
|
||||
Action plan:
|
||||
1. Reconnaissance: identify S3 buckets, EC2 instances, public endpoints, enumerate services via DNS
|
||||
2. S3 Security: test bucket permissions, public access, ACL misconfigurations, bucket policies
|
||||
3. IAM Assessment: review roles, policies, check for overly permissive permissions, find unused credentials
|
||||
4. EC2 Security: scan for open security groups, test instance metadata service (169.254.169.254), check IMDSv2
|
||||
5. Network Security: review VPC configurations, security groups, NACLs, public subnets
|
||||
6. Database Exposure: check RDS public accessibility, security groups, encryption settings
|
||||
7. Lambda Functions: test for function URL exposure, environment variable leaks, IAM role permissions
|
||||
8. CloudTrail & Logging: verify logging is enabled, check for security monitoring gaps
|
||||
9. Report: prioritized cloud security findings with AWS-specific remediation steps`,
|
||||
title: 'Cloud Infrastructure Security Audit (AWS)',
|
||||
},
|
||||
{
|
||||
text: `Conduct WordPress security assessment: {{WORDPRESS_URL}}
|
||||
|
||||
Action plan:
|
||||
1. Version Detection: identify WordPress core version, theme, and active plugins
|
||||
2. Plugin Vulnerabilities: enumerate installed plugins, check for known CVEs using WPScan and Sploitus
|
||||
3. Theme Vulnerabilities: identify theme version, search for known exploits
|
||||
4. User Enumeration: enumerate valid usernames via REST API, author archives, login responses
|
||||
5. Authentication Testing: test weak passwords, brute force protection, 2FA bypass
|
||||
6. File Upload: test media upload restrictions, arbitrary file upload vulnerabilities
|
||||
7. XML-RPC: check if enabled, test pingback SSRF, brute force amplification
|
||||
8. SQL Injection: test search functionality, custom query parameters, plugin-specific inputs
|
||||
9. XSS Testing: test comments, search, contact forms, custom fields
|
||||
10. Configuration Issues: check wp-config.php exposure, directory listing, sensitive file access
|
||||
11. Report: document WordPress-specific vulnerabilities with exploit steps`,
|
||||
title: 'WordPress Security Assessment',
|
||||
},
|
||||
{
|
||||
text: `Perform external attack surface assessment for organization: {{ORGANIZATION_NAME or DOMAIN}}
|
||||
|
||||
Action plan:
|
||||
1. Asset Discovery: enumerate all domains, subdomains (subfinder, amass), IP ranges, ASN information
|
||||
2. Certificate Transparency: search crt.sh for subdomains, identify forgotten assets
|
||||
3. Port Scanning: scan all discovered assets for open ports and services
|
||||
4. Web Application Fingerprinting: identify technologies, CMS, frameworks, server versions
|
||||
5. Email Security: test SPF, DKIM, DMARC records, email spoofing potential
|
||||
6. Cloud Asset Discovery: search for exposed S3 buckets, Azure blobs, exposed cloud databases
|
||||
7. Sensitive Data Exposure: search GitHub, GitLab, Pastebin for leaked credentials, API keys
|
||||
8. Third-Party Integrations: identify SaaS applications, API endpoints, partner integrations
|
||||
9. Vulnerability Prioritization: identify internet-facing critical vulnerabilities
|
||||
10. Report: comprehensive external attack surface map with risk-prioritized findings`,
|
||||
title: 'External Attack Surface Assessment',
|
||||
},
|
||||
{
|
||||
text: `Conduct internal network penetration test from position: {{INITIAL_ACCESS_LEVEL}}
|
||||
|
||||
Action plan:
|
||||
1. Network Reconnaissance: ARP scanning, identify network segments, map internal infrastructure
|
||||
2. Service Discovery: comprehensive port scanning of internal hosts, identify critical servers
|
||||
3. SMB/NetBIOS Enumeration: test null sessions, enumerate shares, check for anonymous access
|
||||
4. Credential Attacks: LLMNR/NBT-NS poisoning (Responder), relay attacks, password spraying
|
||||
5. Vulnerability Exploitation: exploit unpatched services, test default credentials, known CVEs
|
||||
6. Privilege Escalation: exploit local vulnerabilities, misconfigured services, weak permissions
|
||||
7. Lateral Movement: pass-the-hash, token impersonation, exploit trust relationships
|
||||
8. Data Exfiltration: identify sensitive data locations, test data loss prevention controls
|
||||
9. Persistence: establish persistent access mechanisms
|
||||
10. Report: document internal security posture, attack path visualization, remediation priorities`,
|
||||
title: 'Internal Network Penetration Test',
|
||||
},
|
||||
{
|
||||
text: `Perform security testing of mobile application backend API: {{API_URL}}
|
||||
|
||||
Action plan:
|
||||
1. Traffic Interception: analyze mobile app traffic, extract API endpoints and authentication
|
||||
2. Authentication Mechanisms: test OAuth flows, JWT implementation, refresh token handling, certificate pinning bypass
|
||||
3. API Endpoint Testing: test all discovered endpoints for BOLA/IDOR, broken function-level authorization
|
||||
4. Data Validation: test for injection attacks in API parameters, test file upload endpoints
|
||||
5. Business Logic: test premium feature bypass, subscription validation, in-app purchase verification
|
||||
6. Session Management: test token expiration, concurrent session handling, session fixation
|
||||
7. Sensitive Data: check for PII exposure, excessive data in responses, hardcoded secrets
|
||||
8. Rate Limiting: test brute force protection on login, API rate limits, account lockout
|
||||
9. Deep Linking: test for deep link hijacking, intent redirection (Android), URL scheme abuse (iOS)
|
||||
10. Report: mobile-specific vulnerabilities with mitigation recommendations`,
|
||||
title: 'Mobile Application Security Testing (API Backend)',
|
||||
},
|
||||
{
|
||||
text: `Assess DevOps infrastructure and CI/CD pipeline security: {{ORGANIZATION}}
|
||||
|
||||
Action plan:
|
||||
1. Repository Security: scan GitHub/GitLab for exposed secrets, API keys, credentials in commit history
|
||||
2. CI/CD Configuration: review Jenkins/GitLab CI/GitHub Actions configurations, test for injection in pipeline definitions
|
||||
3. Container Security: scan Docker images for vulnerabilities, test for container escape, check image sources
|
||||
4. Secrets Management: test secret storage (HashiCorp Vault, AWS Secrets Manager), check for hardcoded secrets
|
||||
5. Access Control: review permissions on repositories, pipeline access, deployment keys, service accounts
|
||||
6. Artifact Security: scan build artifacts, test artifact repository access controls (Nexus, Artifactory)
|
||||
7. Kubernetes Security: review pod security policies, RBAC, network policies, exposed dashboards
|
||||
8. Infrastructure as Code: review Terraform/Ansible for misconfigurations, overly permissive IAM roles
|
||||
9. Monitoring & Logging: verify security logging, test log tampering, check for security monitoring gaps
|
||||
10. Report: DevOps security findings with secure pipeline recommendations`,
|
||||
title: 'DevOps & CI/CD Pipeline Security',
|
||||
},
|
||||
{
|
||||
text: `Conduct database security assessment: {{DATABASE_TYPE}} at {{HOST:PORT}}
|
||||
|
||||
Action plan:
|
||||
1. Access Testing: test for default credentials, weak passwords, anonymous access
|
||||
2. Network Exposure: verify database should not be internet-accessible, check firewall rules
|
||||
3. Authentication: test authentication mechanisms, user enumeration, password policies
|
||||
4. Authorization: review user permissions, test for privilege escalation, check for excessive grants
|
||||
5. Injection Testing: SQL injection in application layer, test stored procedures for injection
|
||||
6. Configuration Review: check for dangerous configuration options (xp_cmdshell, LOAD DATA, file_priv)
|
||||
7. Encryption: verify data-at-rest encryption, SSL/TLS for connections, check for sensitive data in plaintext
|
||||
8. Backup Security: test backup file access, check backup encryption, verify backup restoration procedures
|
||||
9. Audit Logging: verify audit logs enabled, test log tampering, check retention policies
|
||||
10. Report: database-specific security findings with hardening recommendations`,
|
||||
title: 'Database Security Assessment',
|
||||
},
|
||||
];
|
||||
|
||||
const Template = () => {
|
||||
const navigate = useNavigate();
|
||||
const { templateId } = useParams<{ templateId?: string }>();
|
||||
const { createTemplate, getTemplate, updateTemplate } = useTemplates();
|
||||
const { createTemplate, updateTemplate } = useTemplates();
|
||||
|
||||
const { isMobile } = useBreakpoint();
|
||||
const isNew = templateId === 'new';
|
||||
@@ -55,7 +218,12 @@ const Template = () => {
|
||||
const [isReplaceConfirmOpen, setIsReplaceConfirmOpen] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [pendingPreset, setPendingPreset] = useState<null | { text: string; title: string }>(null);
|
||||
const [templateName, setTemplateName] = useState<null | string>(null);
|
||||
|
||||
// Fetch template data when editing
|
||||
const { data: templateData, loading: isLoadingTemplate } = useFlowTemplateQuery({
|
||||
skip: isNew || !templateId,
|
||||
variables: templateId && !isNew ? { templateId } : undefined,
|
||||
});
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
defaultValues: { text: '', title: '' },
|
||||
@@ -63,22 +231,21 @@ const Template = () => {
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
const { control, formState, getValues, handleSubmit: handleFormSubmit, reset } = form;
|
||||
const { control, formState, getValues, handleSubmit: handleFormSubmit, reset, setValue } = form;
|
||||
|
||||
// Load template data when editing
|
||||
// Load template data into form when query completes
|
||||
useEffect(() => {
|
||||
if (isNew || !templateId) {
|
||||
if (isNew || !templateData?.flowTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const template = getTemplate(templateId);
|
||||
const { text, title } = templateData.flowTemplate;
|
||||
reset({ text, title }, { keepDefaultValues: false });
|
||||
}, [templateData, isNew, reset]);
|
||||
|
||||
if (template) {
|
||||
const { text, title } = template;
|
||||
setTemplateName(title);
|
||||
reset({ text, title });
|
||||
}
|
||||
}, [templateId, isNew, getTemplate, reset]);
|
||||
// Check if form has unsaved changes
|
||||
const hasUnsavedChanges = formState.isDirty;
|
||||
const templateName = templateData?.flowTemplate?.title ?? null;
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
if (isSaving) {
|
||||
@@ -89,12 +256,14 @@ const Template = () => {
|
||||
|
||||
try {
|
||||
if (isNew) {
|
||||
createTemplate(values.title, values.text);
|
||||
await createTemplate(values.title, values.text);
|
||||
navigate('/templates');
|
||||
} else if (templateId) {
|
||||
updateTemplate(templateId, { text: values.text, title: values.title });
|
||||
setTemplateName(values.title);
|
||||
await updateTemplate(templateId, { text: values.text, title: values.title });
|
||||
reset(values, { keepDefaultValues: false });
|
||||
}
|
||||
} catch {
|
||||
// Error already handled in provider with toast
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
@@ -120,18 +289,20 @@ const Template = () => {
|
||||
setPendingPreset(preset);
|
||||
setIsReplaceConfirmOpen(true);
|
||||
} else {
|
||||
reset({ text: preset.text, title: preset.title });
|
||||
setValue('title', preset.title, { shouldDirty: true, shouldValidate: true });
|
||||
setValue('text', preset.text, { shouldDirty: true, shouldValidate: true });
|
||||
}
|
||||
},
|
||||
[getValues, reset],
|
||||
[getValues, setValue],
|
||||
);
|
||||
|
||||
const handleConfirmReplacePreset = useCallback(() => {
|
||||
if (pendingPreset) {
|
||||
reset({ text: pendingPreset.text, title: pendingPreset.title });
|
||||
setValue('title', pendingPreset.title, { shouldDirty: true, shouldValidate: true });
|
||||
setValue('text', pendingPreset.text, { shouldDirty: true, shouldValidate: true });
|
||||
setPendingPreset(null);
|
||||
}
|
||||
}, [pendingPreset, reset]);
|
||||
}, [pendingPreset, setValue]);
|
||||
|
||||
const pageHeader = (
|
||||
<header className="bg-background sticky top-0 z-10 flex h-12 shrink-0 items-center gap-2 border-b px-4">
|
||||
@@ -160,7 +331,7 @@ const Template = () => {
|
||||
|
||||
const asideContent = useMemo(
|
||||
() => (
|
||||
<div className="flex flex-col gap-2 p-4">
|
||||
<div className="flex h-full max-h-[calc(100dvh-3rem)] flex-col overflow-y-auto p-4">
|
||||
<h3 className="text-muted-foreground mb-2 text-sm font-medium">Preset templates</h3>
|
||||
{PRESET_TEMPLATES.map((preset, index) => (
|
||||
<Collapsible
|
||||
@@ -241,6 +412,36 @@ const Template = () => {
|
||||
[isMobile, isAsideOpen, asideContent],
|
||||
);
|
||||
|
||||
// Show loading spinner when fetching template data
|
||||
if (!isNew && isLoadingTemplate) {
|
||||
return (
|
||||
<>
|
||||
{pageHeader}
|
||||
<div className="flex min-h-[calc(100dvh-3rem)] items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle template not found
|
||||
if (!isNew && !isLoadingTemplate && !templateData?.flowTemplate) {
|
||||
return (
|
||||
<>
|
||||
{pageHeader}
|
||||
<div className="flex min-h-[calc(100dvh-3rem)] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardContent className="flex flex-col items-center gap-4 pt-6 text-center">
|
||||
<h2 className="text-xl font-semibold">Template not found</h2>
|
||||
<p className="text-muted-foreground">The template you are looking for does not exist.</p>
|
||||
<Button onClick={() => navigate('/templates')}>Back to Templates</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{pageHeader}
|
||||
@@ -303,7 +504,11 @@ const Template = () => {
|
||||
<InputGroupAddon align="block-end">
|
||||
<InputGroupButton
|
||||
className="ml-auto"
|
||||
disabled={isSaving || !formState.isValid}
|
||||
disabled={
|
||||
isSaving ||
|
||||
!formState.isValid ||
|
||||
(!isNew && !hasUnsavedChanges)
|
||||
}
|
||||
size="icon-xs"
|
||||
type="submit"
|
||||
variant="default"
|
||||
|
||||
@@ -44,8 +44,10 @@ const Templates = () => {
|
||||
setDeletingIds((prev) => new Set(prev).add(deletingTemplate.id));
|
||||
|
||||
try {
|
||||
deleteTemplate(deletingTemplate.id);
|
||||
await deleteTemplate(deletingTemplate.id);
|
||||
setDeletingTemplate(null);
|
||||
} catch {
|
||||
// Error already handled in provider with toast
|
||||
} finally {
|
||||
setDeletingIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
|
||||
@@ -1,164 +1,157 @@
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { createContext, type ReactNode, useCallback, useContext, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import {
|
||||
useCreateFlowTemplateMutation,
|
||||
useDeleteFlowTemplateMutation,
|
||||
useFlowTemplateCreatedSubscription,
|
||||
useFlowTemplateDeletedSubscription,
|
||||
useFlowTemplatesQuery,
|
||||
useFlowTemplateUpdatedSubscription,
|
||||
useUpdateFlowTemplateMutation,
|
||||
} from '@/graphql/types';
|
||||
import { Log } from '@/lib/log';
|
||||
import { useUser } from '@/providers/user-provider';
|
||||
|
||||
export interface Template {
|
||||
createdAt: number;
|
||||
createdAt: Date;
|
||||
id: string;
|
||||
text: string;
|
||||
title: string;
|
||||
updatedAt: Date;
|
||||
userId: string;
|
||||
}
|
||||
|
||||
interface TemplatesContextValue {
|
||||
createTemplate: (title: string, text: string) => string;
|
||||
deleteTemplate: (id: string) => void;
|
||||
createTemplate: (title: string, text: string) => Promise<void>;
|
||||
deleteTemplate: (id: string) => Promise<void>;
|
||||
getTemplate: (id: string) => Template | undefined;
|
||||
isLoading: boolean;
|
||||
templates: Template[];
|
||||
updateTemplate: (id: string, payload: { text: string; title: string }) => void;
|
||||
updateTemplate: (id: string, payload: { text: string; title: string }) => Promise<void>;
|
||||
}
|
||||
|
||||
interface TemplatesProviderProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
interface TemplatesStorage {
|
||||
[userId: string]: Template[];
|
||||
}
|
||||
|
||||
const TemplatesContext = createContext<TemplatesContextValue | undefined>(undefined);
|
||||
|
||||
const TEMPLATES_STORAGE_KEY = 'templates';
|
||||
|
||||
const loadTemplates = (): TemplatesStorage => {
|
||||
try {
|
||||
const stored = localStorage.getItem(TEMPLATES_STORAGE_KEY);
|
||||
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
|
||||
return typeof parsed === 'object' && parsed !== null ? parsed : {};
|
||||
}
|
||||
} catch (error) {
|
||||
Log.error('Error loading templates from storage:', error);
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const saveTemplates = (storage: TemplatesStorage): void => {
|
||||
try {
|
||||
localStorage.setItem(TEMPLATES_STORAGE_KEY, JSON.stringify(storage));
|
||||
} catch (error) {
|
||||
Log.error('Error saving templates to storage:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const generateId = (): string => {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
||||
};
|
||||
|
||||
export const TemplatesProvider = ({ children }: TemplatesProviderProps) => {
|
||||
const { authInfo } = useUser();
|
||||
const userId = authInfo?.user?.id?.toString() ?? 'guest';
|
||||
const { authInfo, isAuthenticated } = useUser();
|
||||
|
||||
const [storage, setStorage] = useState<TemplatesStorage>(() => loadTemplates());
|
||||
const shouldFetchTemplates = Boolean(authInfo && authInfo.type !== 'guest' && isAuthenticated());
|
||||
|
||||
// GraphQL query for templates
|
||||
const { data: templatesData, loading: isLoadingTemplates } = useFlowTemplatesQuery({
|
||||
fetchPolicy: 'cache-and-network',
|
||||
skip: !shouldFetchTemplates,
|
||||
});
|
||||
|
||||
// GraphQL mutations
|
||||
const [createTemplateMutation] = useCreateFlowTemplateMutation();
|
||||
const [updateTemplateMutation] = useUpdateFlowTemplateMutation();
|
||||
const [deleteTemplateMutation] = useDeleteFlowTemplateMutation();
|
||||
|
||||
// GraphQL subscriptions (only for authenticated users)
|
||||
useFlowTemplateCreatedSubscription({
|
||||
skip: !shouldFetchTemplates,
|
||||
});
|
||||
|
||||
useFlowTemplateUpdatedSubscription({
|
||||
skip: !shouldFetchTemplates,
|
||||
});
|
||||
|
||||
useFlowTemplateDeletedSubscription({
|
||||
skip: !shouldFetchTemplates,
|
||||
});
|
||||
|
||||
// Convert GraphQL templates to Template interface
|
||||
const templates = useMemo(() => {
|
||||
const list = storage[userId] ?? [];
|
||||
const rawTemplates = templatesData?.flowTemplates ?? [];
|
||||
|
||||
return [...list].sort((a, b) => b.createdAt - a.createdAt);
|
||||
}, [storage, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
saveTemplates(storage);
|
||||
}, [storage]);
|
||||
return rawTemplates.map((t) => ({
|
||||
createdAt: new Date(t.createdAt),
|
||||
id: t.id,
|
||||
text: t.text,
|
||||
title: t.title,
|
||||
updatedAt: new Date(t.updatedAt),
|
||||
userId: t.userId,
|
||||
}));
|
||||
}, [templatesData?.flowTemplates]);
|
||||
|
||||
const getTemplate = useCallback(
|
||||
(id: string): Template | undefined => {
|
||||
return storage[userId]?.find((t) => t.id === id);
|
||||
return templates.find((t) => t.id === id);
|
||||
},
|
||||
[storage, userId],
|
||||
[templates],
|
||||
);
|
||||
|
||||
const createTemplate = useCallback(
|
||||
(title: string, text: string): string => {
|
||||
const id = generateId();
|
||||
const template: Template = {
|
||||
createdAt: Date.now(),
|
||||
id,
|
||||
text,
|
||||
title,
|
||||
};
|
||||
|
||||
setStorage((previous) => {
|
||||
const list = previous[userId] ?? [];
|
||||
|
||||
return {
|
||||
...previous,
|
||||
[userId]: [...list, template],
|
||||
};
|
||||
});
|
||||
|
||||
return id;
|
||||
async (title: string, text: string) => {
|
||||
try {
|
||||
await createTemplateMutation({
|
||||
variables: {
|
||||
input: {
|
||||
text,
|
||||
title,
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to create template';
|
||||
toast.error('Failed to create template', {
|
||||
description: errorMessage,
|
||||
});
|
||||
Log.error('Error creating template:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[userId],
|
||||
[createTemplateMutation],
|
||||
);
|
||||
|
||||
const updateTemplate = useCallback(
|
||||
(id: string, payload: { text: string; title: string }) => {
|
||||
setStorage((previous) => {
|
||||
const list = previous[userId] ?? [];
|
||||
const index = list.findIndex((t) => t.id === id);
|
||||
|
||||
if (index < 0) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
const existing = list[index];
|
||||
|
||||
if (!existing) {
|
||||
return previous;
|
||||
}
|
||||
|
||||
const updated = [...list];
|
||||
updated[index] = {
|
||||
createdAt: existing.createdAt,
|
||||
id: existing.id,
|
||||
text: payload.text,
|
||||
title: payload.title,
|
||||
};
|
||||
|
||||
return {
|
||||
...previous,
|
||||
[userId]: updated,
|
||||
};
|
||||
});
|
||||
async (id: string, payload: { text: string; title: string }) => {
|
||||
try {
|
||||
await updateTemplateMutation({
|
||||
variables: {
|
||||
input: {
|
||||
text: payload.text,
|
||||
title: payload.title,
|
||||
},
|
||||
templateId: id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to update template';
|
||||
toast.error('Failed to update template', {
|
||||
description: errorMessage,
|
||||
});
|
||||
Log.error('Error updating template:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[userId],
|
||||
[updateTemplateMutation],
|
||||
);
|
||||
|
||||
const deleteTemplate = useCallback(
|
||||
(id: string) => {
|
||||
setStorage((previous) => {
|
||||
const list = previous[userId] ?? [];
|
||||
const filtered = list.filter((t) => t.id !== id);
|
||||
|
||||
return {
|
||||
...previous,
|
||||
[userId]: filtered,
|
||||
};
|
||||
});
|
||||
async (id: string) => {
|
||||
try {
|
||||
await deleteTemplateMutation({
|
||||
variables: {
|
||||
templateId: id,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to delete template';
|
||||
toast.error('Failed to delete template', {
|
||||
description: errorMessage,
|
||||
});
|
||||
Log.error('Error deleting template:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[userId],
|
||||
[deleteTemplateMutation],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
@@ -166,15 +159,14 @@ export const TemplatesProvider = ({ children }: TemplatesProviderProps) => {
|
||||
createTemplate,
|
||||
deleteTemplate,
|
||||
getTemplate,
|
||||
isLoading: isLoadingTemplates,
|
||||
templates,
|
||||
updateTemplate,
|
||||
}),
|
||||
[createTemplate, deleteTemplate, getTemplate, templates, updateTemplate],
|
||||
[createTemplate, deleteTemplate, getTemplate, isLoadingTemplates, templates, updateTemplate],
|
||||
);
|
||||
|
||||
return (
|
||||
<TemplatesContext.Provider value={value}>{children}</TemplatesContext.Provider>
|
||||
);
|
||||
return <TemplatesContext.Provider value={value}>{children}</TemplatesContext.Provider>;
|
||||
};
|
||||
|
||||
export const useTemplates = () => {
|
||||
|
||||
Reference in New Issue
Block a user