2026-10-23 12:24:54 +00:00
package api
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
"net/mail"
"net/url"
"path"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/rs/zerolog"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
2026-10-23 19:36:02 +00:00
"kereru.forms/internal/auth"
2026-10-23 12:24:54 +00:00
"kereru.forms/internal/database"
"kereru.forms/internal/pdf"
)
// Server implements the generated StrictServerInterface.
type Server struct {
store database . Store
logger zerolog . Logger
version string
startedAt time . Time
tracer trace . Tracer
2026-10-23 19:36:02 +00:00
auth * auth . Service
2026-10-23 12:24:54 +00:00
}
2026-10-23 19:36:02 +00:00
// NewServer constructs the API implementation. Authentication is optional so
// tests can exercise handlers directly; the application always supplies it.
func NewServer ( store database . Store , logger zerolog . Logger , version string , authService ... * auth . Service ) * Server {
server := & Server {
2026-10-23 12:24:54 +00:00
store : store ,
logger : logger ,
version : version ,
startedAt : time . Now ( ) ,
tracer : otel . Tracer ( "kereru.forms/api" ) ,
}
2026-10-23 19:36:02 +00:00
if len ( authService ) > 0 {
server . auth = authService [ 0 ]
}
return server
2026-10-23 12:24:54 +00:00
}
func errJSON ( message , code string ) ErrorJSONResponse {
e := Error { Error : message }
if code != "" {
c := code
e . Code = & c
}
return ErrorJSONResponse ( e )
}
func ( s * Server ) GetHealth ( ctx context . Context , _ GetHealthRequestObject ) ( GetHealthResponseObject , error ) {
_ , span := s . tracer . Start ( ctx , "GetHealth" )
defer span . End ( )
uptime := int64 ( time . Since ( s . startedAt ) . Seconds ( ) )
return GetHealth200JSONResponse ( Health {
Status : "ok" ,
Version : s . version ,
UptimeSeconds : & uptime ,
} ) , nil
}
func ( s * Server ) ListForms ( ctx context . Context , req ListFormsRequestObject ) ( ListFormsResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "ListForms" )
defer span . End ( )
includeInactive := req . Params . IncludeInactive != nil && * req . Params . IncludeInactive
if req . Params . ActiveOnly != nil && * req . Params . ActiveOnly {
includeInactive = false
}
forms , err := s . store . ListForms ( ctx , includeInactive )
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "list forms" )
return ListForms500JSONResponse { ErrorJSONResponse : errJSON ( "could not list forms" , "internal_error" ) } , nil
}
out := make ( [ ] FormSummary , 0 , len ( forms ) )
for _ , f := range forms {
out = append ( out , toAPIFormSummary ( f ) )
}
return ListForms200JSONResponse ( out ) , nil
}
func ( s * Server ) CreateForm ( ctx context . Context , req CreateFormRequestObject ) ( CreateFormResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "CreateForm" )
defer span . End ( )
if req . Body == nil {
return CreateForm400JSONResponse { ErrorJSONResponse : errJSON ( "request body is required" , "validation_error" ) } , nil
}
if err := validateForm ( req . Body . Title , req . Body . Questions ) ; err != nil {
return CreateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
if err := validateLogoURL ( req . Body . LogoUrl ) ; err != nil {
return CreateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
if err := validatePDFURL ( req . Body . PdfUrl ) ; err != nil {
return CreateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
externalReferenceRequired := req . Body . ExternalReferenceRequired != nil && * req . Body . ExternalReferenceRequired
externalReferenceLabel , err := normalizeExternalReferenceLabel ( req . Body . ExternalReferenceLabel , externalReferenceRequired )
if err != nil {
return CreateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
now := time . Now ( ) . UTC ( )
form := & database . Form {
ID : uuid . NewString ( ) ,
FormGroupID : uuid . NewString ( ) ,
Version : 1 ,
Title : strings . TrimSpace ( req . Body . Title ) ,
Description : req . Body . Description ,
Introduction : req . Body . Introduction ,
IsActive : true ,
RequiresAccessCode : req . Body . RequiresAccessCode != nil && * req . Body . RequiresAccessCode ,
SignatureRequired : req . Body . SignatureRequired != nil && * req . Body . SignatureRequired ,
ExternalReferenceRequired : externalReferenceRequired ,
ExternalReferenceLabel : externalReferenceLabel ,
LogoURL : req . Body . LogoUrl ,
PDFURL : req . Body . PdfUrl ,
CompanyName : req . Body . CompanyName ,
ContactEmail : req . Body . ContactEmail ,
ContactPhone : req . Body . ContactPhone ,
ContactURL : req . Body . ContactUrl ,
CreatedAt : now ,
UpdatedAt : now ,
Questions : questionInputsToRecords ( req . Body . Questions ) ,
}
for i := range form . Questions {
form . Questions [ i ] . ID = uuid . NewString ( )
form . Questions [ i ] . FormID = form . ID
}
if err := s . store . CreateForm ( ctx , form ) ; err != nil {
if errors . Is ( err , database . ErrConflict ) {
return CreateForm400JSONResponse { ErrorJSONResponse : errJSON ( "a form with this key already exists" , "conflict" ) } , nil
}
s . logger . Error ( ) . Err ( err ) . Msg ( "create form" )
return CreateForm500JSONResponse ( errJSON ( "could not create form" , "internal_error" ) ) , nil
}
span . SetAttributes ( attribute . String ( "form.id" , form . ID ) , attribute . String ( "form.group" , form . FormGroupID ) )
return CreateForm201JSONResponse ( toAPIForm ( form ) ) , nil
}
func ( s * Server ) GetForm ( ctx context . Context , req GetFormRequestObject ) ( GetFormResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "GetForm" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
form , err := s . store . GetForm ( ctx , req . FormId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return GetForm404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "get form" )
return GetForm500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
return GetForm200JSONResponse ( toAPIForm ( form ) ) , nil
}
func ( s * Server ) UpdateForm ( ctx context . Context , req UpdateFormRequestObject ) ( UpdateFormResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "UpdateForm" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
if req . Body == nil {
return UpdateForm400JSONResponse { ErrorJSONResponse : errJSON ( "request body is required" , "validation_error" ) } , nil
}
if err := validateForm ( req . Body . Title , req . Body . Questions ) ; err != nil {
return UpdateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
if err := validateLogoURL ( req . Body . LogoUrl ) ; err != nil {
return UpdateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
if err := validatePDFURL ( req . Body . PdfUrl ) ; err != nil {
return UpdateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
existing , err := s . store . GetForm ( ctx , req . FormId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return UpdateForm404JSONResponse ( errJSON ( "form not found" , "not_found" ) ) , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "load form for update" )
return UpdateForm500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
// Editing a definition creates a new version so historical submissions keep
// pointing at the exact definition they answered.
externalReferenceRequired := existing . ExternalReferenceRequired
if req . Body . ExternalReferenceRequired != nil {
externalReferenceRequired = * req . Body . ExternalReferenceRequired
}
externalReferenceLabel := existing . ExternalReferenceLabel
if req . Body . ExternalReferenceLabel != nil {
label , err := normalizeExternalReferenceLabel ( req . Body . ExternalReferenceLabel , externalReferenceRequired )
if err != nil {
return UpdateForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
externalReferenceLabel = label
} else if externalReferenceRequired && ( externalReferenceLabel == nil || * externalReferenceLabel == "" ) {
label := defaultExternalReferenceLabel
externalReferenceLabel = & label
}
now := time . Now ( ) . UTC ( )
form := & database . Form {
ID : uuid . NewString ( ) ,
FormGroupID : existing . FormGroupID ,
Version : existing . Version + 1 ,
Title : strings . TrimSpace ( req . Body . Title ) ,
Description : req . Body . Description ,
Introduction : req . Body . Introduction ,
IsActive : true ,
RequiresAccessCode : existing . RequiresAccessCode ,
SignatureRequired : existing . SignatureRequired ,
ExternalReferenceRequired : externalReferenceRequired ,
ExternalReferenceLabel : externalReferenceLabel ,
LogoURL : req . Body . LogoUrl ,
PDFURL : req . Body . PdfUrl ,
CompanyName : req . Body . CompanyName ,
ContactEmail : req . Body . ContactEmail ,
ContactPhone : req . Body . ContactPhone ,
ContactURL : req . Body . ContactUrl ,
CreatedAt : now ,
UpdatedAt : now ,
Questions : questionInputsToRecords ( req . Body . Questions ) ,
}
if req . Body . RequiresAccessCode != nil {
form . RequiresAccessCode = * req . Body . RequiresAccessCode
}
if req . Body . SignatureRequired != nil {
form . SignatureRequired = * req . Body . SignatureRequired
}
for i := range form . Questions {
form . Questions [ i ] . ID = uuid . NewString ( )
form . Questions [ i ] . FormID = form . ID
}
if err := s . store . CreateForm ( ctx , form ) ; err != nil {
if errors . Is ( err , database . ErrConflict ) {
return UpdateForm400JSONResponse { ErrorJSONResponse : errJSON ( "a newer version of this form already exists" , "conflict" ) } , nil
}
s . logger . Error ( ) . Err ( err ) . Msg ( "update form" )
return UpdateForm500JSONResponse ( errJSON ( "could not update form" , "internal_error" ) ) , nil
}
if err := s . store . SetFormActive ( ctx , existing . ID , false ) ; err != nil {
s . logger . Warn ( ) . Err ( err ) . Str ( "form.id" , existing . ID ) . Msg ( "could not inactivate previous form version" )
}
span . SetAttributes ( attribute . Int ( "form.version" , form . Version ) )
return UpdateForm200JSONResponse ( toAPIForm ( form ) ) , nil
}
func ( s * Server ) DeleteForm ( ctx context . Context , req DeleteFormRequestObject ) ( DeleteFormResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "DeleteForm" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
err := s . store . DeleteForm ( ctx , req . FormId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return DeleteForm404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "delete form" )
return DeleteForm500JSONResponse ( errJSON ( "could not delete form" , "internal_error" ) ) , nil
}
return DeleteForm204Response { } , nil
}
func ( s * Server ) InactivateForm ( ctx context . Context , req InactivateFormRequestObject ) ( InactivateFormResponseObject , error ) {
form , err := s . setFormActive ( ctx , req . FormId . String ( ) , false )
if errors . Is ( err , database . ErrNotFound ) {
return InactivateForm404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
if err != nil {
return InactivateForm500JSONResponse ( errJSON ( err . Error ( ) , "internal_error" ) ) , nil
}
return InactivateForm200JSONResponse ( toAPIForm ( form ) ) , nil
}
func ( s * Server ) ActivateForm ( ctx context . Context , req ActivateFormRequestObject ) ( ActivateFormResponseObject , error ) {
form , err := s . setFormActive ( ctx , req . FormId . String ( ) , true )
if errors . Is ( err , database . ErrNotFound ) {
return ActivateForm404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
if err != nil {
return ActivateForm500JSONResponse ( errJSON ( err . Error ( ) , "internal_error" ) ) , nil
}
return ActivateForm200JSONResponse ( toAPIForm ( form ) ) , nil
}
func ( s * Server ) setFormActive ( ctx context . Context , id string , active bool ) ( * database . Form , error ) {
ctx , span := s . tracer . Start ( ctx , "SetFormActive" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , id ) , attribute . Bool ( "form.active" , active ) )
if err := s . store . SetFormActive ( ctx , id , active ) ; err != nil {
if ! errors . Is ( err , database . ErrNotFound ) {
s . logger . Error ( ) . Err ( err ) . Bool ( "active" , active ) . Msg ( "set form active" )
}
return nil , err
}
form , err := s . store . GetForm ( ctx , id )
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "load form after activation change" )
return nil , err
}
return form , nil
}
func ( s * Server ) CreateFormVersion ( ctx context . Context , req CreateFormVersionRequestObject ) ( CreateFormVersionResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "CreateFormVersion" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
existing , err := s . store . GetForm ( ctx , req . FormId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return CreateFormVersion404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "load form for new version" )
return CreateFormVersion500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
now := time . Now ( ) . UTC ( )
version := & database . Form {
ID : uuid . NewString ( ) ,
FormGroupID : existing . FormGroupID ,
Version : existing . Version + 1 ,
Title : existing . Title ,
Description : existing . Description ,
Introduction : existing . Introduction ,
IsActive : true ,
RequiresAccessCode : existing . RequiresAccessCode ,
SignatureRequired : existing . SignatureRequired ,
ExternalReferenceRequired : existing . ExternalReferenceRequired ,
ExternalReferenceLabel : existing . ExternalReferenceLabel ,
LogoURL : existing . LogoURL ,
CompanyName : existing . CompanyName ,
ContactEmail : existing . ContactEmail ,
ContactPhone : existing . ContactPhone ,
ContactURL : existing . ContactURL ,
CreatedAt : now ,
UpdatedAt : now ,
Questions : make ( [ ] database . Question , 0 , len ( existing . Questions ) ) ,
}
for _ , q := range existing . Questions {
clone := q
clone . ID = uuid . NewString ( )
clone . FormID = version . ID
version . Questions = append ( version . Questions , clone )
}
if err := s . store . CreateForm ( ctx , version ) ; err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "create form version" )
return CreateFormVersion500JSONResponse ( errJSON ( "could not create form version" , "internal_error" ) ) , nil
}
if err := s . store . SetFormActive ( ctx , existing . ID , false ) ; err != nil {
s . logger . Warn ( ) . Err ( err ) . Str ( "form.id" , existing . ID ) . Msg ( "could not inactivate previous form version" )
}
return CreateFormVersion201JSONResponse ( toAPIForm ( version ) ) , nil
}
func ( s * Server ) GetFormPdf ( ctx context . Context , req GetFormPdfRequestObject ) ( GetFormPdfResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "GetFormPdf" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
form , err := s . store . GetForm ( ctx , req . FormId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return GetFormPdf404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "load form for pdf" )
return GetFormPdf500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
data , err := pdf . Generate ( form )
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "generate pdf" )
return GetFormPdf500JSONResponse ( errJSON ( "could not generate pdf" , "internal_error" ) ) , nil
}
return GetFormPdf200ApplicationpdfResponse {
Body : bytes . NewReader ( data ) ,
ContentLength : int64 ( len ( data ) ) ,
} , nil
}
func ( s * Server ) ListFormSubmissions ( ctx context . Context , req ListFormSubmissionsRequestObject ) ( ListFormSubmissionsResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "ListFormSubmissions" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
if _ , err := s . store . GetForm ( ctx , req . FormId . String ( ) ) ; err != nil {
if errors . Is ( err , database . ErrNotFound ) {
return ListFormSubmissions404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
s . logger . Error ( ) . Err ( err ) . Msg ( "load form for submissions" )
return ListFormSubmissions500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
subs , err := s . store . ListSubmissions ( ctx , req . FormId . String ( ) )
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "list submissions" )
return ListFormSubmissions500JSONResponse ( errJSON ( "could not list submissions" , "internal_error" ) ) , nil
}
out := make ( [ ] SubmissionSummary , 0 , len ( subs ) )
for _ , sub := range subs {
out = append ( out , toAPISubmissionSummary ( sub ) )
}
return ListFormSubmissions200JSONResponse ( out ) , nil
}
func ( s * Server ) SubmitForm ( ctx context . Context , req SubmitFormRequestObject ) ( SubmitFormResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "SubmitForm" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
form , err := s . store . GetForm ( ctx , req . FormId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return SubmitForm404JSONResponse ( errJSON ( "form not found" , "not_found" ) ) , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "load form for submission" )
return SubmitForm500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
if ! form . IsActive {
return SubmitForm410JSONResponse ( errJSON ( "this form is no longer accepting responses" , "form_inactive" ) ) , nil
}
if req . Body == nil {
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( "request body is required" , "validation_error" ) } , nil
}
userIdType := UserIdType ( strings . ToLower ( strings . TrimSpace ( string ( req . Body . UserIdType ) ) ) )
if ! validUserIdType ( userIdType ) {
2026-10-23 23:57:57 +00:00
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( "userIdType must be one of email, username, phone, foreign key or guid" , "validation_error" ) } , nil
2026-10-23 12:24:54 +00:00
}
userId := strings . TrimSpace ( req . Body . UserId )
if userId == "" {
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( "userId is required" , "validation_error" ) } , nil
}
if userIdType == Email {
if _ , err := mail . ParseAddress ( userId ) ; err != nil {
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( "userId is not a valid email address" , "validation_error" ) } , nil
}
}
externalReference := strings . TrimSpace ( derefString ( req . Body . ExternalReference ) )
if form . ExternalReferenceRequired && externalReference == "" {
label := defaultExternalReferenceLabel
if form . ExternalReferenceLabel != nil && * form . ExternalReferenceLabel != "" {
label = * form . ExternalReferenceLabel
}
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( label + " is required" , "validation_error" ) } , nil
}
if len ( externalReference ) > maxExternalReferenceLength {
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( fmt . Sprintf ( "externalReference must be %d characters or fewer" , maxExternalReferenceLength ) , "validation_error" ) } , nil
}
var externalReferencePtr * string
if externalReference != "" {
externalReferencePtr = & externalReference
}
answers , err := validateAnswers ( form , req . Body . Answers , req . Body . OfflinePdf == nil )
if err != nil {
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
signature , offlinePDF , offlinePDFName , err := validateSignatureAndOfflinePDF ( form , req . Body )
if err != nil {
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( err . Error ( ) , "validation_error" ) } , nil
}
var accessCode string
if form . RequiresAccessCode {
accessCode = strings . ToUpper ( strings . TrimSpace ( derefString ( req . Body . AccessCode ) ) )
if accessCode == "" {
return SubmitForm400JSONResponse { ErrorJSONResponse : errJSON ( "an access code is required for this form" , "access_code_required" ) } , nil
}
}
meta := requestMeta ( ctx )
now := time . Now ( ) . UTC ( )
submission := & database . Submission {
ID : uuid . NewString ( ) ,
FormID : form . ID ,
FormVersion : form . Version ,
UserID : userId ,
UserIDType : string ( userIdType ) ,
SubmittedAt : now ,
IPAddress : meta . ip ,
UserAgent : meta . userAgent ,
ExternalReference : externalReferencePtr ,
Signature : signature ,
OfflinePDFName : offlinePDFName ,
OfflinePDF : offlinePDF ,
Answers : answers ,
}
if req . Body . Tracking != nil {
submission . Locale = req . Body . Tracking . Locale
submission . Timezone = req . Body . Tracking . Timezone
submission . Place = req . Body . Tracking . Place
submission . Referer = req . Body . Tracking . Referer
}
if err := s . store . CreateSubmission ( ctx , submission , accessCode ) ; err != nil {
switch {
case errors . Is ( err , database . ErrAccessCode ) :
return SubmitForm409JSONResponse ( errJSON ( "the access code is not valid for this form" , "invalid_access_code" ) ) , nil
case errors . Is ( err , database . ErrCodeAlreadyIn ) :
return SubmitForm409JSONResponse ( errJSON ( "the access code has already been used" , "access_code_used" ) ) , nil
default :
s . logger . Error ( ) . Err ( err ) . Msg ( "create submission" )
return SubmitForm500JSONResponse ( errJSON ( "could not save submission" , "internal_error" ) ) , nil
}
}
span . SetAttributes ( attribute . String ( "submission.id" , submission . ID ) )
return SubmitForm201JSONResponse ( toAPISubmission ( submission ) ) , nil
}
func ( s * Server ) GetSubmission ( ctx context . Context , req GetSubmissionRequestObject ) ( GetSubmissionResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "GetSubmission" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "submission.id" , req . SubmissionId . String ( ) ) )
sub , err := s . store . GetSubmission ( ctx , req . SubmissionId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return GetSubmission404JSONResponse { ErrorJSONResponse : errJSON ( "submission not found" , "not_found" ) } , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "get submission" )
return GetSubmission500JSONResponse ( errJSON ( "could not load submission" , "internal_error" ) ) , nil
}
return GetSubmission200JSONResponse ( toAPISubmission ( sub ) ) , nil
}
func ( s * Server ) GetSubmissionOfflinePdf ( ctx context . Context , req GetSubmissionOfflinePdfRequestObject ) ( GetSubmissionOfflinePdfResponseObject , error ) {
headerCtx := ctx
ctx , span := s . tracer . Start ( ctx , "GetSubmissionOfflinePdf" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "submission.id" , req . SubmissionId . String ( ) ) )
data , name , err := s . store . GetSubmissionOfflinePDF ( ctx , req . SubmissionId . String ( ) )
if errors . Is ( err , database . ErrNotFound ) {
return GetSubmissionOfflinePdf404JSONResponse { ErrorJSONResponse : errJSON ( "offline PDF not found" , "not_found" ) } , nil
}
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "get submission offline pdf" )
return GetSubmissionOfflinePdf500JSONResponse ( errJSON ( "could not load offline PDF" , "internal_error" ) ) , nil
}
if c , ok := headerCtx . ( interface { Header ( string , string ) } ) ; ok {
c . Header ( "Content-Disposition" , attachmentDisposition ( name ) )
}
return GetSubmissionOfflinePdf200ApplicationpdfResponse {
Body : bytes . NewReader ( data ) ,
ContentLength : int64 ( len ( data ) ) ,
} , nil
}
func attachmentDisposition ( name string ) string {
clean := strings . Map ( func ( r rune ) rune {
if r < ' ' || r == 127 || r == '"' || r == '\\' {
return '-'
}
return r
} , name )
if strings . TrimSpace ( clean ) == "" {
clean = "submission.pdf"
}
return fmt . Sprintf ( "attachment; filename=%q" , clean )
}
func ( s * Server ) ListAccessCodes ( ctx context . Context , req ListAccessCodesRequestObject ) ( ListAccessCodesResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "ListAccessCodes" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
if _ , err := s . store . GetForm ( ctx , req . FormId . String ( ) ) ; err != nil {
if errors . Is ( err , database . ErrNotFound ) {
return ListAccessCodes404JSONResponse { ErrorJSONResponse : errJSON ( "form not found" , "not_found" ) } , nil
}
s . logger . Error ( ) . Err ( err ) . Msg ( "load form for access codes" )
return ListAccessCodes500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
codes , err := s . store . ListAccessCodes ( ctx , req . FormId . String ( ) )
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "list access codes" )
return ListAccessCodes500JSONResponse ( errJSON ( "could not list access codes" , "internal_error" ) ) , nil
}
out := make ( [ ] AccessCode , 0 , len ( codes ) )
for _ , c := range codes {
out = append ( out , toAPIAccessCode ( c ) )
}
return ListAccessCodes200JSONResponse ( out ) , nil
}
func ( s * Server ) CreateAccessCodes ( ctx context . Context , req CreateAccessCodesRequestObject ) ( CreateAccessCodesResponseObject , error ) {
ctx , span := s . tracer . Start ( ctx , "CreateAccessCodes" )
defer span . End ( )
span . SetAttributes ( attribute . String ( "form.id" , req . FormId . String ( ) ) )
if req . Body == nil {
return CreateAccessCodes400JSONResponse { ErrorJSONResponse : errJSON ( "request body is required" , "validation_error" ) } , nil
}
if req . Body . Count < 1 || req . Body . Count > 1000 {
return CreateAccessCodes400JSONResponse { ErrorJSONResponse : errJSON ( "count must be between 1 and 1000" , "validation_error" ) } , nil
}
if _ , err := s . store . GetForm ( ctx , req . FormId . String ( ) ) ; err != nil {
if errors . Is ( err , database . ErrNotFound ) {
return CreateAccessCodes404JSONResponse ( errJSON ( "form not found" , "not_found" ) ) , nil
}
s . logger . Error ( ) . Err ( err ) . Msg ( "load form for access code creation" )
return CreateAccessCodes500JSONResponse ( errJSON ( "could not load form" , "internal_error" ) ) , nil
}
prefix := ""
if req . Body . Prefix != nil {
prefix = strings . ToUpper ( strings . TrimSpace ( * req . Body . Prefix ) )
if prefix != "" && ! codePrefixPattern . MatchString ( prefix ) {
return CreateAccessCodes400JSONResponse { ErrorJSONResponse : errJSON ( "prefix may only contain letters, digits and hyphens" , "validation_error" ) } , nil
}
}
now := time . Now ( ) . UTC ( )
codes := make ( [ ] database . AccessCode , 0 , req . Body . Count )
for i := 0 ; i < req . Body . Count ; i ++ {
code , err := generateCode ( prefix )
if err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "generate access code" )
return CreateAccessCodes500JSONResponse ( errJSON ( "could not generate access codes" , "internal_error" ) ) , nil
}
codes = append ( codes , database . AccessCode {
ID : uuid . NewString ( ) ,
FormID : req . FormId . String ( ) ,
Code : code ,
CreatedAt : now ,
} )
}
if err := s . store . CreateAccessCodes ( ctx , codes ) ; err != nil {
s . logger . Error ( ) . Err ( err ) . Msg ( "create access codes" )
return CreateAccessCodes500JSONResponse ( errJSON ( "could not create access codes" , "internal_error" ) ) , nil
}
out := make ( [ ] AccessCode , 0 , len ( codes ) )
for _ , c := range codes {
out = append ( out , toAPIAccessCode ( c ) )
}
return CreateAccessCodes201JSONResponse ( out ) , nil
}
var (
questionKeyPattern = regexp . MustCompile ( ` ^[A-Za-z][A-Za-z0-9_-] { 0,63}$ ` )
codePrefixPattern = regexp . MustCompile ( ` ^[A-Z0-9-] { 1,16}$ ` )
codeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
)
func generateCode ( prefix string ) ( string , error ) {
var b strings . Builder
for i := 0 ; i < 8 ; i ++ {
if i == 4 {
b . WriteByte ( '-' )
}
n , err := rand . Int ( rand . Reader , big . NewInt ( int64 ( len ( codeAlphabet ) ) ) )
if err != nil {
return "" , err
}
b . WriteByte ( codeAlphabet [ n . Int64 ( ) ] )
}
code := b . String ( )
if prefix != "" {
code = prefix + "-" + code
}
return code , nil
}
func validateLogoURL ( raw * string ) error {
if raw == nil || strings . TrimSpace ( * raw ) == "" {
return nil
}
u , err := url . Parse ( strings . TrimSpace ( * raw ) )
if err != nil || ( u . Scheme != "http" && u . Scheme != "https" ) || u . Host == "" {
return errors . New ( "logoUrl must be an absolute http or https URL" )
}
return nil
}
func validatePDFURL ( raw * string ) error {
if raw == nil || strings . TrimSpace ( * raw ) == "" {
return nil
}
u , err := url . Parse ( strings . TrimSpace ( * raw ) )
if err != nil || ( u . Scheme != "http" && u . Scheme != "https" ) || u . Host == "" {
return errors . New ( "pdfUrl must be an absolute http or https URL" )
}
return nil
}
const (
maxSignatureBytes = 2 << 20
maxOfflinePDFSize = 10 << 20
)
const (
defaultExternalReferenceLabel = "External reference"
maxExternalReferenceLabel = 100
maxExternalReferenceLength = 200
)
func normalizeExternalReferenceLabel ( raw * string , required bool ) ( * string , error ) {
label := strings . TrimSpace ( derefString ( raw ) )
if len ( label ) > maxExternalReferenceLabel {
return nil , fmt . Errorf ( "externalReferenceLabel must be %d characters or fewer" , maxExternalReferenceLabel )
}
if label == "" && required {
label = defaultExternalReferenceLabel
}
if label == "" {
return nil , nil
}
return & label , nil
}
func validateSignatureAndOfflinePDF ( form * database . Form , req * SubmitFormRequest ) ( * database . Signature , [ ] byte , * string , error ) {
var (
offline [ ] byte
offlineName * string
)
if req . OfflinePdf != nil {
data , err := decodeDataURL ( * req . OfflinePdf , "application/pdf" , maxOfflinePDFSize )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "offlinePdf: %w" , err )
}
if ! bytes . HasPrefix ( data , [ ] byte ( "%PDF" ) ) {
return nil , nil , nil , errors . New ( "offlinePdf: file does not look like a PDF" )
}
name := "offline-submission.pdf"
if req . OfflinePdfName != nil {
if trimmed := strings . TrimSpace ( * req . OfflinePdfName ) ; trimmed != "" {
name = path . Base ( strings . ReplaceAll ( trimmed , "\\" , "/" ) )
}
}
if len ( name ) > 255 {
return nil , nil , nil , errors . New ( "offlinePdfName must be 255 characters or fewer" )
}
offline = data
offlineName = & name
}
var signature * database . Signature
if req . Signature != nil {
kind := strings . ToLower ( strings . TrimSpace ( string ( req . Signature . Type ) ) )
if kind != "drawn" && kind != "image" {
return nil , nil , nil , errors . New ( "signature.type must be drawn or image" )
}
_ , mediaType , err := decodeSignatureImage ( req . Signature . DataUrl )
if err != nil {
return nil , nil , nil , fmt . Errorf ( "signature.dataUrl: %w" , err )
}
if kind == "drawn" && mediaType != "image/png" {
return nil , nil , nil , errors . New ( "signature.dataUrl must be a PNG for drawn signatures" )
}
name := strings . TrimSpace ( req . Signature . Name )
if name == "" {
return nil , nil , nil , errors . New ( "signature.name is required" )
}
if len ( name ) > 200 {
return nil , nil , nil , errors . New ( "signature.name must be 200 characters or fewer" )
}
var title * string
if req . Signature . Title != nil {
trimmed := strings . TrimSpace ( * req . Signature . Title )
if len ( trimmed ) > 200 {
return nil , nil , nil , errors . New ( "signature.title must be 200 characters or fewer" )
}
if trimmed != "" {
title = & trimmed
}
}
signature = & database . Signature {
Type : kind ,
DataURL : req . Signature . DataUrl ,
Name : name ,
Title : title ,
SignedAt : time . Now ( ) . UTC ( ) ,
}
} else if form . SignatureRequired && len ( offline ) == 0 {
return nil , nil , nil , errors . New ( "a signature is required for this form" )
}
return signature , offline , offlineName , nil
}
func decodeSignatureImage ( raw string ) ( [ ] byte , string , error ) {
data , mediaType , err := decodeDataURLWithMedia ( raw , maxSignatureBytes )
if err != nil {
return nil , "" , err
}
switch mediaType {
case "image/png" :
if ! bytes . HasPrefix ( data , [ ] byte { 0x89 , 'P' , 'N' , 'G' , 0x0d , 0x0a , 0x1a , 0x0a } ) {
return nil , "" , errors . New ( "file does not look like a PNG image" )
}
case "image/jpeg" :
if ! bytes . HasPrefix ( data , [ ] byte { 0xff , 0xd8 , 0xff } ) {
return nil , "" , errors . New ( "file does not look like a JPEG image" )
}
default :
return nil , "" , errors . New ( "must be a PNG or JPEG data URL" )
}
return data , mediaType , nil
}
func decodeDataURL ( raw , expectedMedia string , maxBytes int ) ( [ ] byte , error ) {
data , mediaType , err := decodeDataURLWithMedia ( raw , maxBytes )
if err != nil {
return nil , err
}
if mediaType != expectedMedia {
return nil , fmt . Errorf ( "must be a %s data URL" , expectedMedia )
}
return data , nil
}
func decodeDataURLWithMedia ( raw string , maxBytes int ) ( [ ] byte , string , error ) {
if ! strings . HasPrefix ( raw , "data:" ) {
return nil , "" , errors . New ( "must be a base64 data URL" )
}
header , payload , ok := strings . Cut ( raw [ len ( "data:" ) : ] , "," )
if ! ok {
return nil , "" , errors . New ( "must be a base64 data URL" )
}
mediaType , encoding , ok := strings . Cut ( header , ";" )
if ! ok || ! strings . EqualFold ( strings . TrimSpace ( encoding ) , "base64" ) {
return nil , "" , errors . New ( "must be a base64 data URL" )
}
mediaType = strings . ToLower ( strings . TrimSpace ( mediaType ) )
data , err := base64 . StdEncoding . DecodeString ( payload )
if err != nil {
return nil , "" , errors . New ( "contains invalid base64 data" )
}
if len ( data ) == 0 {
return nil , "" , errors . New ( "file is empty" )
}
if len ( data ) > maxBytes {
return nil , "" , fmt . Errorf ( "file exceeds the %d MB limit" , maxBytes / ( 1 << 20 ) )
}
return data , mediaType , nil
}
func validateForm ( title string , questions [ ] QuestionInput ) error {
if strings . TrimSpace ( title ) == "" {
return errors . New ( "title is required" )
}
seenKeys := make ( map [ string ] struct { } , len ( questions ) )
for i , q := range questions {
label := fmt . Sprintf ( "question %d" , i + 1 )
key := strings . TrimSpace ( q . Key )
if key == "" {
return fmt . Errorf ( "%s: key is required" , label )
}
if ! questionKeyPattern . MatchString ( key ) {
return fmt . Errorf ( "%s: key must start with a letter and contain only letters, digits, underscores or hyphens" , label )
}
if _ , exists := seenKeys [ key ] ; exists {
return fmt . Errorf ( "%s: duplicate key %q" , label , key )
}
seenKeys [ key ] = struct { } { }
if strings . TrimSpace ( q . Label ) == "" {
return fmt . Errorf ( "%s: label is required" , label )
}
switch q . Type {
case Boolean , Number , Select , Text , Date :
case "" :
return fmt . Errorf ( "%s: type is required" , label )
default :
return fmt . Errorf ( "%s: unsupported type %q" , label , q . Type )
}
if q . Type == Select {
if q . Options == nil || len ( * q . Options ) == 0 {
return fmt . Errorf ( "%s: select questions require at least one option" , label )
}
seenValues := make ( map [ string ] struct { } , len ( * q . Options ) )
for _ , opt := range * q . Options {
value := strings . TrimSpace ( opt . Value )
if value == "" {
return fmt . Errorf ( "%s: option value is required" , label )
}
if strings . TrimSpace ( opt . Label ) == "" {
return fmt . Errorf ( "%s: option label is required" , label )
}
if _ , exists := seenValues [ value ] ; exists {
return fmt . Errorf ( "%s: duplicate option value %q" , label , value )
}
seenValues [ value ] = struct { } { }
}
}
if q . Type == Number && q . MinValue != nil && q . MaxValue != nil && * q . MinValue > * q . MaxValue {
return fmt . Errorf ( "%s: minValue must be less than or equal to maxValue" , label )
}
}
return nil
}
func validateAnswers ( form * database . Form , answers [ ] Answer , requireRequired bool ) ( [ ] database . Answer , error ) {
byID := make ( map [ string ] database . Question , len ( form . Questions ) )
for _ , q := range form . Questions {
byID [ q . ID ] = q
}
seen := make ( map [ string ] struct { } , len ( answers ) )
out := make ( [ ] database . Answer , 0 , len ( answers ) )
for _ , ans := range answers {
id := ans . QuestionId . String ( )
q , ok := byID [ id ]
if ! ok {
return nil , fmt . Errorf ( "question %s does not belong to this form" , id )
}
if _ , dup := seen [ id ] ; dup {
return nil , fmt . Errorf ( "duplicate answer for question %q" , q . Key )
}
seen [ id ] = struct { } { }
if err := validateAnswerValue ( q , ans . Value ) ; err != nil {
return nil , err
}
raw , err := json . Marshal ( ans . Value )
if err != nil {
return nil , fmt . Errorf ( "question %q: could not encode answer" , q . Key )
}
out = append ( out , database . Answer {
QuestionID : id ,
QuestionKey : q . Key ,
Value : raw ,
} )
}
for _ , q := range form . Questions {
if ! requireRequired || ! q . Required {
continue
}
if _ , ok := seen [ q . ID ] ; ! ok {
return nil , fmt . Errorf ( "question %q requires an answer" , q . Key )
}
}
return out , nil
}
func validateAnswerValue ( q database . Question , value interface { } ) error {
if value == nil {
return fmt . Errorf ( "question %q: answer value is required" , q . Key )
}
switch q . Type {
case "boolean" :
if _ , ok := value . ( bool ) ; ! ok {
return fmt . Errorf ( "question %q: expected a true/false value" , q . Key )
}
case "number" :
num , ok := toFloat ( value )
if ! ok {
return fmt . Errorf ( "question %q: expected a numeric value" , q . Key )
}
if q . MinValue != nil && num < * q . MinValue {
return fmt . Errorf ( "question %q: value must be at least %g" , q . Key , * q . MinValue )
}
if q . MaxValue != nil && num > * q . MaxValue {
return fmt . Errorf ( "question %q: value must be at most %g" , q . Key , * q . MaxValue )
}
case "select" :
str , ok := value . ( string )
if ! ok {
return fmt . Errorf ( "question %q: expected one of the configured options" , q . Key )
}
found := false
for _ , opt := range q . Options {
if opt . Value == str {
found = true
break
}
}
if ! found {
return fmt . Errorf ( "question %q: %q is not a configured option" , q . Key , str )
}
case "date" :
str , ok := value . ( string )
if ! ok {
return fmt . Errorf ( "question %q: expected a date in YYYY-MM-DD form" , q . Key )
}
date , err := time . Parse ( "2006-01-02" , strings . TrimSpace ( str ) )
if err != nil || date . Format ( "2006-01-02" ) != strings . TrimSpace ( str ) {
return fmt . Errorf ( "question %q: expected a date in YYYY-MM-DD form" , q . Key )
}
case "text" :
str , ok := value . ( string )
if ! ok {
return fmt . Errorf ( "question %q: expected a text value" , q . Key )
}
if len ( str ) > 10000 {
return fmt . Errorf ( "question %q: answer is too long" , q . Key )
}
if ! q . Multiline && strings . ContainsAny ( str , "\r\n" ) {
return fmt . Errorf ( "question %q: this answer must be a single line" , q . Key )
}
default :
return fmt . Errorf ( "question %q: unsupported type %q" , q . Key , q . Type )
}
return nil
}
func toFloat ( v interface { } ) ( float64 , bool ) {
switch n := v . ( type ) {
case float64 :
return n , true
case float32 :
return float64 ( n ) , true
case int :
return float64 ( n ) , true
case int64 :
return float64 ( n ) , true
case json . Number :
f , err := n . Float64 ( )
return f , err == nil
default :
return 0 , false
}
}
func validUserIdType ( t UserIdType ) bool {
switch t {
2026-10-23 23:57:57 +00:00
case Email , Username , Phone , ForeignKey , Guid :
2026-10-23 12:24:54 +00:00
return true
default :
return false
}
}
func derefString ( v * string ) string {
if v == nil {
return ""
}
return * v
}
const metaKey = "kereru.request_meta"
type requestMetadata struct {
ip * string
userAgent * string
}
// requestMeta returns the server-side captured request metadata.
func requestMeta ( ctx context . Context ) requestMetadata {
if v , ok := ctx . Value ( metaKey ) . ( requestMetadata ) ; ok {
return v
}
return requestMetadata { }
}