Backend (Go): - DB Migration 023: ai_system_registrations Tabelle - RegistrationStore: CRUD + Status-Management + Export-JSON - RegistrationHandlers: 7 Endpoints (Create, List, Get, Update, Status, Prefill, Export) - Routes in main.go: /sdk/v1/ai-registration/* Frontend (Next.js): - 6-Step Wizard: Anbieter → System → Klassifikation → Konformitaet → Trainingsdaten → Pruefung - System-Karten mit Status-Badges (Entwurf/Bereit/Eingereicht/Registriert) - JSON-Export fuer EU-Datenbank-Submission - Status-Workflow: draft → ready → submitted → registered - API Proxy Routes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
275 lines
11 KiB
Go
275 lines
11 KiB
Go
package ucca
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// AIRegistration represents an EU AI Database registration entry
|
|
type AIRegistration struct {
|
|
ID uuid.UUID `json:"id"`
|
|
TenantID uuid.UUID `json:"tenant_id"`
|
|
|
|
// System
|
|
SystemName string `json:"system_name"`
|
|
SystemVersion string `json:"system_version,omitempty"`
|
|
SystemDescription string `json:"system_description,omitempty"`
|
|
IntendedPurpose string `json:"intended_purpose,omitempty"`
|
|
|
|
// Provider
|
|
ProviderName string `json:"provider_name,omitempty"`
|
|
ProviderLegalForm string `json:"provider_legal_form,omitempty"`
|
|
ProviderAddress string `json:"provider_address,omitempty"`
|
|
ProviderCountry string `json:"provider_country,omitempty"`
|
|
EURepresentativeName string `json:"eu_representative_name,omitempty"`
|
|
EURepresentativeContact string `json:"eu_representative_contact,omitempty"`
|
|
|
|
// Classification
|
|
RiskClassification string `json:"risk_classification"`
|
|
AnnexIIICategory string `json:"annex_iii_category,omitempty"`
|
|
GPAIClassification string `json:"gpai_classification"`
|
|
|
|
// Conformity
|
|
ConformityAssessmentType string `json:"conformity_assessment_type,omitempty"`
|
|
NotifiedBodyName string `json:"notified_body_name,omitempty"`
|
|
NotifiedBodyID string `json:"notified_body_id,omitempty"`
|
|
CEMarking bool `json:"ce_marking"`
|
|
|
|
// Training data
|
|
TrainingDataCategories json.RawMessage `json:"training_data_categories,omitempty"`
|
|
TrainingDataSummary string `json:"training_data_summary,omitempty"`
|
|
|
|
// Status
|
|
RegistrationStatus string `json:"registration_status"`
|
|
EUDatabaseID string `json:"eu_database_id,omitempty"`
|
|
RegistrationDate *time.Time `json:"registration_date,omitempty"`
|
|
LastUpdateDate *time.Time `json:"last_update_date,omitempty"`
|
|
|
|
// Links
|
|
UCCAAssessmentID *uuid.UUID `json:"ucca_assessment_id,omitempty"`
|
|
DecisionTreeResultID *uuid.UUID `json:"decision_tree_result_id,omitempty"`
|
|
|
|
// Export
|
|
ExportData json.RawMessage `json:"export_data,omitempty"`
|
|
|
|
// Audit
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
CreatedBy string `json:"created_by,omitempty"`
|
|
SubmittedBy string `json:"submitted_by,omitempty"`
|
|
}
|
|
|
|
// RegistrationStore handles AI registration persistence
|
|
type RegistrationStore struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
// NewRegistrationStore creates a new registration store
|
|
func NewRegistrationStore(pool *pgxpool.Pool) *RegistrationStore {
|
|
return &RegistrationStore{pool: pool}
|
|
}
|
|
|
|
// Create creates a new registration
|
|
func (s *RegistrationStore) Create(ctx context.Context, r *AIRegistration) error {
|
|
r.ID = uuid.New()
|
|
r.CreatedAt = time.Now()
|
|
r.UpdatedAt = time.Now()
|
|
if r.RegistrationStatus == "" {
|
|
r.RegistrationStatus = "draft"
|
|
}
|
|
if r.RiskClassification == "" {
|
|
r.RiskClassification = "not_classified"
|
|
}
|
|
if r.GPAIClassification == "" {
|
|
r.GPAIClassification = "none"
|
|
}
|
|
|
|
_, err := s.pool.Exec(ctx, `
|
|
INSERT INTO ai_system_registrations (
|
|
id, tenant_id, system_name, system_version, system_description, intended_purpose,
|
|
provider_name, provider_legal_form, provider_address, provider_country,
|
|
eu_representative_name, eu_representative_contact,
|
|
risk_classification, annex_iii_category, gpai_classification,
|
|
conformity_assessment_type, notified_body_name, notified_body_id, ce_marking,
|
|
training_data_categories, training_data_summary,
|
|
registration_status, ucca_assessment_id, decision_tree_result_id,
|
|
created_by
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12,
|
|
$13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25
|
|
)`,
|
|
r.ID, r.TenantID, r.SystemName, r.SystemVersion, r.SystemDescription, r.IntendedPurpose,
|
|
r.ProviderName, r.ProviderLegalForm, r.ProviderAddress, r.ProviderCountry,
|
|
r.EURepresentativeName, r.EURepresentativeContact,
|
|
r.RiskClassification, r.AnnexIIICategory, r.GPAIClassification,
|
|
r.ConformityAssessmentType, r.NotifiedBodyName, r.NotifiedBodyID, r.CEMarking,
|
|
r.TrainingDataCategories, r.TrainingDataSummary,
|
|
r.RegistrationStatus, r.UCCAAssessmentID, r.DecisionTreeResultID,
|
|
r.CreatedBy,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// List returns all registrations for a tenant
|
|
func (s *RegistrationStore) List(ctx context.Context, tenantID uuid.UUID) ([]AIRegistration, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT id, tenant_id, system_name, system_version, system_description, intended_purpose,
|
|
provider_name, provider_legal_form, provider_address, provider_country,
|
|
eu_representative_name, eu_representative_contact,
|
|
risk_classification, annex_iii_category, gpai_classification,
|
|
conformity_assessment_type, notified_body_name, notified_body_id, ce_marking,
|
|
training_data_categories, training_data_summary,
|
|
registration_status, eu_database_id, registration_date, last_update_date,
|
|
ucca_assessment_id, decision_tree_result_id, export_data,
|
|
created_at, updated_at, created_by, submitted_by
|
|
FROM ai_system_registrations
|
|
WHERE tenant_id = $1
|
|
ORDER BY created_at DESC`,
|
|
tenantID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var registrations []AIRegistration
|
|
for rows.Next() {
|
|
var r AIRegistration
|
|
err := rows.Scan(
|
|
&r.ID, &r.TenantID, &r.SystemName, &r.SystemVersion, &r.SystemDescription, &r.IntendedPurpose,
|
|
&r.ProviderName, &r.ProviderLegalForm, &r.ProviderAddress, &r.ProviderCountry,
|
|
&r.EURepresentativeName, &r.EURepresentativeContact,
|
|
&r.RiskClassification, &r.AnnexIIICategory, &r.GPAIClassification,
|
|
&r.ConformityAssessmentType, &r.NotifiedBodyName, &r.NotifiedBodyID, &r.CEMarking,
|
|
&r.TrainingDataCategories, &r.TrainingDataSummary,
|
|
&r.RegistrationStatus, &r.EUDatabaseID, &r.RegistrationDate, &r.LastUpdateDate,
|
|
&r.UCCAAssessmentID, &r.DecisionTreeResultID, &r.ExportData,
|
|
&r.CreatedAt, &r.UpdatedAt, &r.CreatedBy, &r.SubmittedBy,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
registrations = append(registrations, r)
|
|
}
|
|
return registrations, nil
|
|
}
|
|
|
|
// GetByID returns a registration by ID
|
|
func (s *RegistrationStore) GetByID(ctx context.Context, id uuid.UUID) (*AIRegistration, error) {
|
|
var r AIRegistration
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT id, tenant_id, system_name, system_version, system_description, intended_purpose,
|
|
provider_name, provider_legal_form, provider_address, provider_country,
|
|
eu_representative_name, eu_representative_contact,
|
|
risk_classification, annex_iii_category, gpai_classification,
|
|
conformity_assessment_type, notified_body_name, notified_body_id, ce_marking,
|
|
training_data_categories, training_data_summary,
|
|
registration_status, eu_database_id, registration_date, last_update_date,
|
|
ucca_assessment_id, decision_tree_result_id, export_data,
|
|
created_at, updated_at, created_by, submitted_by
|
|
FROM ai_system_registrations
|
|
WHERE id = $1`,
|
|
id,
|
|
).Scan(
|
|
&r.ID, &r.TenantID, &r.SystemName, &r.SystemVersion, &r.SystemDescription, &r.IntendedPurpose,
|
|
&r.ProviderName, &r.ProviderLegalForm, &r.ProviderAddress, &r.ProviderCountry,
|
|
&r.EURepresentativeName, &r.EURepresentativeContact,
|
|
&r.RiskClassification, &r.AnnexIIICategory, &r.GPAIClassification,
|
|
&r.ConformityAssessmentType, &r.NotifiedBodyName, &r.NotifiedBodyID, &r.CEMarking,
|
|
&r.TrainingDataCategories, &r.TrainingDataSummary,
|
|
&r.RegistrationStatus, &r.EUDatabaseID, &r.RegistrationDate, &r.LastUpdateDate,
|
|
&r.UCCAAssessmentID, &r.DecisionTreeResultID, &r.ExportData,
|
|
&r.CreatedAt, &r.UpdatedAt, &r.CreatedBy, &r.SubmittedBy,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &r, nil
|
|
}
|
|
|
|
// Update updates a registration
|
|
func (s *RegistrationStore) Update(ctx context.Context, r *AIRegistration) error {
|
|
r.UpdatedAt = time.Now()
|
|
_, err := s.pool.Exec(ctx, `
|
|
UPDATE ai_system_registrations SET
|
|
system_name = $2, system_version = $3, system_description = $4, intended_purpose = $5,
|
|
provider_name = $6, provider_legal_form = $7, provider_address = $8, provider_country = $9,
|
|
eu_representative_name = $10, eu_representative_contact = $11,
|
|
risk_classification = $12, annex_iii_category = $13, gpai_classification = $14,
|
|
conformity_assessment_type = $15, notified_body_name = $16, notified_body_id = $17, ce_marking = $18,
|
|
training_data_categories = $19, training_data_summary = $20,
|
|
registration_status = $21, eu_database_id = $22,
|
|
export_data = $23, updated_at = $24, submitted_by = $25
|
|
WHERE id = $1`,
|
|
r.ID, r.SystemName, r.SystemVersion, r.SystemDescription, r.IntendedPurpose,
|
|
r.ProviderName, r.ProviderLegalForm, r.ProviderAddress, r.ProviderCountry,
|
|
r.EURepresentativeName, r.EURepresentativeContact,
|
|
r.RiskClassification, r.AnnexIIICategory, r.GPAIClassification,
|
|
r.ConformityAssessmentType, r.NotifiedBodyName, r.NotifiedBodyID, r.CEMarking,
|
|
r.TrainingDataCategories, r.TrainingDataSummary,
|
|
r.RegistrationStatus, r.EUDatabaseID,
|
|
r.ExportData, r.UpdatedAt, r.SubmittedBy,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// UpdateStatus changes only the registration status
|
|
func (s *RegistrationStore) UpdateStatus(ctx context.Context, id uuid.UUID, status string, submittedBy string) error {
|
|
now := time.Now()
|
|
_, err := s.pool.Exec(ctx, `
|
|
UPDATE ai_system_registrations
|
|
SET registration_status = $2, submitted_by = $3, updated_at = $4,
|
|
registration_date = CASE WHEN $2 = 'submitted' THEN $4 ELSE registration_date END,
|
|
last_update_date = $4
|
|
WHERE id = $1`,
|
|
id, status, submittedBy, now,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// BuildExportJSON creates the EU AI Database submission JSON
|
|
func (s *RegistrationStore) BuildExportJSON(r *AIRegistration) json.RawMessage {
|
|
export := map[string]interface{}{
|
|
"schema_version": "1.0",
|
|
"submission_type": "ai_system_registration",
|
|
"regulation": "EU AI Act (EU) 2024/1689",
|
|
"article": "Art. 49",
|
|
"provider": map[string]interface{}{
|
|
"name": r.ProviderName,
|
|
"legal_form": r.ProviderLegalForm,
|
|
"address": r.ProviderAddress,
|
|
"country": r.ProviderCountry,
|
|
"eu_representative": r.EURepresentativeName,
|
|
"eu_rep_contact": r.EURepresentativeContact,
|
|
},
|
|
"system": map[string]interface{}{
|
|
"name": r.SystemName,
|
|
"version": r.SystemVersion,
|
|
"description": r.SystemDescription,
|
|
"purpose": r.IntendedPurpose,
|
|
},
|
|
"classification": map[string]interface{}{
|
|
"risk_level": r.RiskClassification,
|
|
"annex_iii_category": r.AnnexIIICategory,
|
|
"gpai": r.GPAIClassification,
|
|
},
|
|
"conformity": map[string]interface{}{
|
|
"assessment_type": r.ConformityAssessmentType,
|
|
"notified_body": r.NotifiedBodyName,
|
|
"notified_body_id": r.NotifiedBodyID,
|
|
"ce_marking": r.CEMarking,
|
|
},
|
|
"training_data": map[string]interface{}{
|
|
"categories": r.TrainingDataCategories,
|
|
"summary": r.TrainingDataSummary,
|
|
},
|
|
"status": r.RegistrationStatus,
|
|
}
|
|
data, _ := json.Marshal(export)
|
|
return data
|
|
}
|