193 lines
5.9 KiB
Go
193 lines
5.9 KiB
Go
package platform
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type Question struct {
|
|
Key string `json:"key"`
|
|
Required bool `json:"required"`
|
|
}
|
|
type OnboardingConfig struct {
|
|
RequireWorkspace bool `json:"require_workspace"`
|
|
Questions []Question `json:"questions"`
|
|
}
|
|
|
|
var surveyTypes = map[string]string{
|
|
"team_activity": "string", "job_role": "string", "referral": "string",
|
|
"previous_use": "boolean", "purpose": "string", "company_size": "string",
|
|
}
|
|
|
|
func ParseOnboarding(raw string, callbacks map[string]string) (map[string]OnboardingConfig, error) {
|
|
configs := map[string]OnboardingConfig{}
|
|
if strings.TrimSpace(raw) == "" {
|
|
return configs, nil
|
|
}
|
|
decoder := json.NewDecoder(strings.NewReader(raw))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&configs); err != nil {
|
|
return nil, errors.New("invalid FOUNDRY_ONBOARDING JSON")
|
|
}
|
|
if decoder.Decode(new(any)) != io.EOF {
|
|
return nil, errors.New("invalid FOUNDRY_ONBOARDING JSON")
|
|
}
|
|
for project, config := range configs {
|
|
if _, ok := callbacks[project]; !ok {
|
|
return nil, errors.New("onboarding project is not configured")
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, question := range config.Questions {
|
|
if surveyTypes[question.Key] == "" || seen[question.Key] {
|
|
return nil, errors.New("invalid or duplicate onboarding question")
|
|
}
|
|
seen[question.Key] = true
|
|
}
|
|
}
|
|
return configs, nil
|
|
}
|
|
|
|
func validAnswers(config OnboardingConfig, answers map[string]json.RawMessage) bool {
|
|
questions := map[string]Question{}
|
|
for _, question := range config.Questions {
|
|
questions[question.Key] = question
|
|
}
|
|
for key, value := range answers {
|
|
if _, ok := questions[key]; !ok {
|
|
return false
|
|
}
|
|
if surveyTypes[key] == "boolean" {
|
|
var answer *bool
|
|
if json.Unmarshal(value, &answer) != nil || answer == nil {
|
|
return false
|
|
}
|
|
} else {
|
|
var answer *string
|
|
if json.Unmarshal(value, &answer) != nil || answer == nil || len(*answer) > 2000 {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
for _, question := range config.Questions {
|
|
value, ok := answers[question.Key]
|
|
if question.Required {
|
|
if !ok {
|
|
return false
|
|
}
|
|
if surveyTypes[question.Key] == "string" {
|
|
var answer string
|
|
if json.Unmarshal(value, &answer) != nil || strings.TrimSpace(answer) == "" {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *service) onboarding(w http.ResponseWriter, r *http.Request, project, account string) error {
|
|
config := s.onboardingConfigs[project]
|
|
if config.Questions == nil {
|
|
config.Questions = []Question{}
|
|
}
|
|
if r.Method == http.MethodGet {
|
|
var answers, result json.RawMessage
|
|
err := s.db.QueryRowContext(r.Context(), "SELECT answers,result FROM onboarding_responses WHERE project=$1 AND account_id=$2", project, account).Scan(&answers, &result)
|
|
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
reply(w, 200, response{"config": config, "completed": err == nil, "answers": answers, "result": result})
|
|
return nil
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
reject(w, 405, "method_not_allowed")
|
|
return nil
|
|
}
|
|
var input struct {
|
|
Username *string `json:"username"`
|
|
TeamName string `json:"team_name"`
|
|
GenerateInvite bool `json:"generate_invite"`
|
|
Answers map[string]json.RawMessage `json:"answers"`
|
|
}
|
|
if !decode(w, r, &input) {
|
|
return nil
|
|
}
|
|
input.TeamName = strings.TrimSpace(input.TeamName)
|
|
if input.Answers == nil {
|
|
input.Answers = map[string]json.RawMessage{}
|
|
}
|
|
if input.Username != nil {
|
|
value := strings.ToLower(strings.TrimSpace(*input.Username))
|
|
input.Username = &value
|
|
if !usernamePattern.MatchString(value) {
|
|
reject(w, 400, "invalid_username")
|
|
return nil
|
|
}
|
|
}
|
|
if !validAnswers(config, input.Answers) || len(input.TeamName) > 160 || (config.RequireWorkspace && input.TeamName == "") || (input.GenerateInvite && input.TeamName == "") {
|
|
reject(w, 400, "invalid_onboarding")
|
|
return nil
|
|
}
|
|
tx, err := s.db.BeginTx(r.Context(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
var locked string
|
|
if err = tx.QueryRowContext(r.Context(), "SELECT id::text FROM project_accounts WHERE project=$1 AND id=$2 FOR UPDATE", project, account).Scan(&locked); err != nil {
|
|
return err
|
|
}
|
|
var prior json.RawMessage
|
|
err = tx.QueryRowContext(r.Context(), "SELECT result FROM onboarding_responses WHERE project=$1 AND account_id=$2", project, account).Scan(&prior)
|
|
if err == nil {
|
|
reply(w, 200, response{"completed": true, "result": prior, "already_completed": true})
|
|
return nil
|
|
}
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return err
|
|
}
|
|
if input.Username != nil {
|
|
_, err = tx.ExecContext(r.Context(), "UPDATE project_accounts SET username=$3,updated_at=now() WHERE project=$1 AND id=$2", project, account, *input.Username)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
result := response{}
|
|
var invitation string
|
|
if input.TeamName != "" {
|
|
workspace, err := createWorkspace(r.Context(), tx, project, account, input.TeamName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result["workspace_id"] = workspace
|
|
if input.GenerateInvite {
|
|
token, hash := newLinkToken()
|
|
var id string
|
|
err = tx.QueryRowContext(r.Context(), `
|
|
INSERT INTO share_links(project,workspace_id,token_hash,role,created_by,expires_at)
|
|
VALUES($1,$2,$3,'viewer',$4,now()+interval '7 days') RETURNING id::text
|
|
`, project, workspace, hash, account).Scan(&id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
result["invite_id"] = id
|
|
invitation = s.origin + "/s/" + token
|
|
}
|
|
}
|
|
answers, _ := json.Marshal(input.Answers)
|
|
resultJSON, _ := json.Marshal(result)
|
|
_, err = tx.ExecContext(r.Context(), "INSERT INTO onboarding_responses(project,account_id,answers,result) VALUES($1,$2,$3,$4)", project, account, answers, resultJSON)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
reply(w, 201, response{"completed": true, "result": result, "invite_url": invitation, "already_completed": false})
|
|
return nil
|
|
}
|