// Copy into your Go LMS backend. Standard library only. package artanclass import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) type Client struct { BaseURL, APIKey string } type Person struct { ExternalUserID string `json:"external_user_id"` Name string `json:"name"` NationalID string `json:"national_id"` // Preserve leading zeroes. } type ClassRequest struct { ExternalClassID string `json:"external_class_id"` Name string `json:"name"` Teacher Person `json:"teacher"` Students []Person `json:"students"` } type ClassResponse struct { ClassURL string `json:"class_url"` } type EntryResponse struct { EntryURL string `json:"entry_url"` } type APIError struct { Status int Code string `json:"code"` Errors []string `json:"errors"` RetryAfter string } func (e *APIError) Error() string { return fmt.Sprintf("ArtanClass %d (%s): %s", e.Status, e.Code, strings.Join(e.Errors, "; ")) } func (c Client) post(ctx context.Context, path string, body, result any, timeout time.Duration) error { base, err := url.Parse(c.BaseURL) if err != nil || base.Scheme != "https" || base.Host == "" { return fmt.Errorf("invalid ArtanClass HTTPS base URL") } data, err := json.Marshal(body) if err != nil { return err } req, err := http.NewRequestWithContext(ctx, http.MethodPost, base.ResolveReference(&url.URL{Path: path}).String(), bytes.NewReader(data)) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+c.APIKey) req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") client := &http.Client{Timeout: timeout, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }} resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() decoder := json.NewDecoder(io.LimitReader(resp.Body, 2<<20)) if resp.StatusCode < 200 || resp.StatusCode >= 300 { apiErr := &APIError{Status: resp.StatusCode, RetryAfter: resp.Header.Get("Retry-After")} if err := decoder.Decode(apiErr); err != nil { return fmt.Errorf("ArtanClass HTTP %d", resp.StatusCode) } return apiErr } return decoder.Decode(result) } func (c Client) CreateClass(ctx context.Context, body ClassRequest) (ClassResponse, error) { var result ClassResponse if body.Students == nil { body.Students = []Person{} } err := c.post(ctx, "/api/v1/lms/classes", body, &result, 180*time.Second) return result, err } func (c Client) EntryLink(ctx context.Context, classID, externalUserID string) (EntryResponse, error) { var result EntryResponse if classID == "" || len(classID) > 128 { return result, fmt.Errorf("invalid LMS class id") } for _, ch := range classID { if !(ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z' || ch >= '0' && ch <= '9' || ch == '_' || ch == '-') { return result, fmt.Errorf("invalid LMS class id") } } err := c.post(ctx, "/api/v1/lms/classes/"+classID+"/launch", map[string]string{"external_user_id": externalUserID}, &result, 30*time.Second) return result, err } // 1. result, err := client.CreateClass(ctx, classData) // See class.json. // Handle err, then store result.ClassURL with the LMS class. // // 2. In your authenticated LMS entry handler, authorize class membership FIRST. // result, err := client.EntryLink(r.Context(), lmsClass.ExternalID, authenticatedUser.ExternalID) // if err != nil { /* show a retry message, then return */ } // w.Header().Set("Cache-Control", "no-store") // w.Header().Set("Referrer-Policy", "no-referrer") // http.Redirect(w, r, result.EntryURL, http.StatusSeeOther) // Same code for teachers and students; do not log keys, national IDs or EntryURL.