mirror of
https://github.com/Alexander-D-Karpov/webring.git
synced 2026-03-16 22:07:41 +03:00
636 lines
17 KiB
Go
636 lines
17 KiB
Go
package dashboard
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"math"
|
|
"net/http"
|
|
"os"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"webring/internal/favicon"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/lib/pq"
|
|
|
|
"webring/internal/auth"
|
|
"webring/internal/models"
|
|
)
|
|
|
|
const (
|
|
millisecondsMultiplier = 1000
|
|
uniqueViolation = "unique_violation"
|
|
)
|
|
|
|
var slugRegex = regexp.MustCompile(`^(?:[a-z0-9-]{3,50}|\d+)$`)
|
|
var (
|
|
templates *template.Template
|
|
templatesMu sync.RWMutex
|
|
)
|
|
|
|
func InitTemplates(t *template.Template) {
|
|
templatesMu.Lock()
|
|
defer templatesMu.Unlock()
|
|
templates = t
|
|
}
|
|
|
|
func adminSessionMiddleware(db *sql.DB) mux.MiddlewareFunc {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
sid := auth.GetSessionFromRequest(r)
|
|
if sid == "" {
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
user, err := auth.GetSessionUser(db, sid)
|
|
if err != nil {
|
|
auth.ClearSessionCookie(w)
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if !user.IsAdmin {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
func RegisterHandlers(r *mux.Router, db *sql.DB) {
|
|
adminRouter := r.PathPrefix("/admin").Subrouter()
|
|
adminRouter.Use(adminSessionMiddleware(db))
|
|
|
|
adminRouter.HandleFunc("", dashboardHandler(db)).Methods("GET")
|
|
adminRouter.HandleFunc("/add", addSiteHandler(db)).Methods("POST")
|
|
adminRouter.HandleFunc("/remove/{id}", removeSiteHandler(db)).Methods("POST")
|
|
adminRouter.HandleFunc("/update/{id}", updateSiteHandler(db)).Methods("POST")
|
|
adminRouter.HandleFunc("/reorder/{id}/{direction}", reorderSiteHandler(db)).Methods("POST")
|
|
adminRouter.HandleFunc("/move/{id}/{position}", moveSiteHandler(db)).Methods("POST")
|
|
adminRouter.HandleFunc("/toggle-enabled/{id}", toggleEnabledHandler(db)).Methods("POST")
|
|
}
|
|
|
|
func renderTemplate(w http.ResponseWriter, name string, data interface{}) error {
|
|
templatesMu.RLock()
|
|
defer templatesMu.RUnlock()
|
|
|
|
if templates == nil {
|
|
return fmt.Errorf("templates not initialized")
|
|
}
|
|
|
|
return templates.ExecuteTemplate(w, name, data)
|
|
}
|
|
|
|
func dashboardHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
templatesMu.RLock()
|
|
t := templates
|
|
templatesMu.RUnlock()
|
|
|
|
if t == nil {
|
|
log.Println("Templates not initialized")
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sites, err := getAllSites(db)
|
|
if err != nil {
|
|
log.Printf("Error fetching sites: %v", err)
|
|
http.Error(w, "Error fetching sites", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
data := struct {
|
|
Sites []models.Site
|
|
Request *http.Request
|
|
}{
|
|
Sites: sites,
|
|
Request: r,
|
|
}
|
|
|
|
if err = renderTemplate(w, "dashboard.html", data); err != nil {
|
|
log.Printf("Error rendering template: %v", err)
|
|
http.Error(w, "Error rendering template", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func addSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := r.FormValue("id")
|
|
slug := r.FormValue("slug")
|
|
name := r.FormValue("name")
|
|
url := r.FormValue("url")
|
|
telegramUsername := r.FormValue("telegram_username")
|
|
|
|
if slug == "" || idStr == "" || name == "" || url == "" {
|
|
http.Error(w, "ID, Slug, Name, and URL are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if !slugRegex.MatchString(slug) {
|
|
http.Error(w, "Invalid Slug format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var maxDisplayOrder int
|
|
err = db.QueryRow("SELECT COALESCE(MAX(display_order), 0) FROM sites").Scan(&maxDisplayOrder)
|
|
if err != nil {
|
|
log.Printf("Error determining display order: %v", err)
|
|
http.Error(w, "Error determining display order", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var userID *int
|
|
if telegramUsername != "" {
|
|
telegramUsernameClean := sanitizeTelegramUsername(telegramUsername)
|
|
if telegramUsernameClean == "" {
|
|
http.Error(w, "Invalid Telegram username format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
userID, err = findOrCreateUserByTelegramUsername(db, telegramUsernameClean)
|
|
if err != nil {
|
|
log.Printf("Error handling telegram username: %v", err)
|
|
http.Error(w, "Error processing telegram username", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err = db.Exec("INSERT INTO sites (id, slug, name, url, display_order, user_id, enabled) "+
|
|
"VALUES ($1, $2, $3, $4, $5, $6, $7)",
|
|
id, slug, name, url, maxDisplayOrder+1, userID, true)
|
|
if err != nil {
|
|
var pqErr *pq.Error
|
|
if errors.As(err, &pqErr) && pqErr.Code.Name() == uniqueViolation {
|
|
switch pqErr.Constraint {
|
|
case "sites_pkey":
|
|
http.Error(w, "Site ID is already in use", http.StatusConflict)
|
|
case "sites_slug_key":
|
|
http.Error(w, "Slug is already in use", http.StatusConflict)
|
|
default:
|
|
http.Error(w, "Unique constraint violation", http.StatusConflict)
|
|
}
|
|
return
|
|
}
|
|
log.Printf("Error adding site: %v", err)
|
|
http.Error(w, "Error adding site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
mediaFolder := os.Getenv("MEDIA_FOLDER")
|
|
if mediaFolder == "" {
|
|
mediaFolder = "media"
|
|
}
|
|
|
|
faviconPath, faviconErr := favicon.GetAndStoreFavicon(url, mediaFolder, id)
|
|
if faviconErr != nil {
|
|
log.Printf("Error retrieving favicon for %s: %v", url, faviconErr)
|
|
return
|
|
}
|
|
|
|
if _, faviconErr = db.Exec("UPDATE sites SET favicon = $1 WHERE id = $2", faviconPath, id); faviconErr != nil {
|
|
log.Printf("Error updating favicon for site %d: %v", id, faviconErr)
|
|
}
|
|
}()
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func removeSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id := mux.Vars(r)["id"]
|
|
_, err := db.Exec("DELETE FROM sites WHERE id = $1", id)
|
|
if err != nil {
|
|
log.Printf("Error removing site: %v", err)
|
|
http.Error(w, "Error removing site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func updateSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id := mux.Vars(r)["id"]
|
|
slug := r.FormValue("slug")
|
|
name := r.FormValue("name")
|
|
url := r.FormValue("url")
|
|
telegramUsername := r.FormValue("telegram_username")
|
|
|
|
if slug == "" || name == "" || url == "" {
|
|
http.Error(w, "Slug, Name and URL are required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if !slugRegex.MatchString(slug) {
|
|
http.Error(w, "Invalid Slug format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var userID *int
|
|
if telegramUsername != "" {
|
|
var findErr error
|
|
telegramUsernameClean := sanitizeTelegramUsername(telegramUsername)
|
|
if telegramUsernameClean == "" {
|
|
http.Error(w, "Invalid Telegram username format", http.StatusBadRequest)
|
|
return
|
|
}
|
|
userID, findErr = findOrCreateUserByTelegramUsername(db, telegramUsernameClean)
|
|
if findErr != nil {
|
|
log.Printf("Error handling telegram username: %v", findErr)
|
|
http.Error(w, "Error processing telegram username", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
_, err := db.Exec("UPDATE sites SET slug = $1, name = $2, url = $3, user_id = $4 WHERE id = $5",
|
|
slug, name, url, userID, id)
|
|
if err != nil {
|
|
var pqErr *pq.Error
|
|
if errors.As(err, &pqErr) && pqErr.Code.Name() == uniqueViolation {
|
|
switch pqErr.Constraint {
|
|
case "sites_slug_key":
|
|
http.Error(w, "Slug is already in use", http.StatusConflict)
|
|
default:
|
|
http.Error(w, "Unique constraint violation", http.StatusConflict)
|
|
}
|
|
return
|
|
}
|
|
log.Printf("Error updating site: %v", err)
|
|
http.Error(w, "Error updating site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
mediaFolder := os.Getenv("MEDIA_FOLDER")
|
|
if mediaFolder == "" {
|
|
mediaFolder = "media"
|
|
}
|
|
|
|
siteID, parseErr := strconv.Atoi(id)
|
|
if parseErr != nil {
|
|
log.Printf("Error converting site ID to int: %v", parseErr)
|
|
return
|
|
}
|
|
|
|
faviconPath, faviconErr := favicon.GetAndStoreFavicon(url, mediaFolder, siteID)
|
|
if faviconErr != nil {
|
|
log.Printf("Error retrieving favicon for %s: %v", url, faviconErr)
|
|
return
|
|
}
|
|
|
|
if _, faviconErr = db.Exec("UPDATE sites SET favicon = $1 WHERE id = $2", faviconPath, siteID); faviconErr != nil {
|
|
log.Printf("Error updating favicon for site %d: %v", siteID, faviconErr)
|
|
}
|
|
}()
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func toggleEnabledHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
id := mux.Vars(r)["id"]
|
|
|
|
_, err := db.Exec("UPDATE sites SET enabled = NOT enabled WHERE id = $1", id)
|
|
if err != nil {
|
|
log.Printf("Error toggling site enabled status: %v", err)
|
|
http.Error(w, "Error toggling site status", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func reorderSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := mux.Vars(r)["id"]
|
|
direction := mux.Vars(r)["direction"]
|
|
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var offset int
|
|
switch direction {
|
|
case "up":
|
|
offset = -1
|
|
case "down":
|
|
offset = 1
|
|
default:
|
|
http.Error(w, "Invalid direction", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
log.Printf("Error starting transaction: %v", err)
|
|
http.Error(w, "Error reordering sites", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer func() {
|
|
if rollbackErr := tx.Rollback(); rollbackErr != nil && rollbackErr != sql.ErrTxDone {
|
|
log.Printf("Error rolling back transaction: %v", rollbackErr)
|
|
}
|
|
}()
|
|
|
|
if err = normalizeDisplayOrder(tx); err != nil {
|
|
log.Printf("Error normalizing display order: %v", err)
|
|
http.Error(w, "Error normalizing order", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
var currentOrder int
|
|
err = tx.QueryRow("SELECT display_order FROM sites WHERE id = $1", id).Scan(¤tOrder)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
http.Error(w, "Site not found", http.StatusNotFound)
|
|
} else {
|
|
log.Printf("Error fetching site order: %v", err)
|
|
http.Error(w, "Error fetching site", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
targetOrder := currentOrder + offset
|
|
|
|
var minOrder, maxOrder int
|
|
err = tx.QueryRow("SELECT MIN(display_order), MAX(display_order) FROM sites").Scan(&minOrder, &maxOrder)
|
|
if err != nil {
|
|
log.Printf("Error getting order bounds: %v", err)
|
|
http.Error(w, "Error getting order bounds", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if targetOrder < minOrder || targetOrder > maxOrder {
|
|
if err = tx.Commit(); err != nil {
|
|
log.Printf("Error committing transaction: %v", err)
|
|
}
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
tempOrder := maxOrder + 1000
|
|
_, err = tx.Exec("UPDATE sites SET display_order = $1 WHERE id = $2", tempOrder, id)
|
|
if err != nil {
|
|
log.Printf("Error setting temporary order: %v", err)
|
|
http.Error(w, "Error reordering sites", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
_, err = tx.Exec("UPDATE sites SET display_order = $1 WHERE display_order = $2", currentOrder, targetOrder)
|
|
if err != nil {
|
|
log.Printf("Error updating target site: %v", err)
|
|
http.Error(w, "Error reordering sites", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
_, err = tx.Exec("UPDATE sites SET display_order = $1 WHERE id = $2", targetOrder, id)
|
|
if err != nil {
|
|
log.Printf("Error updating current site: %v", err)
|
|
http.Error(w, "Error reordering sites", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
log.Printf("Error committing transaction: %v", err)
|
|
http.Error(w, "Error reordering sites", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
http.Redirect(w, r, "/admin", http.StatusSeeOther)
|
|
}
|
|
}
|
|
|
|
func normalizeDisplayOrder(tx *sql.Tx) error {
|
|
rows, err := tx.Query("SELECT id FROM sites ORDER BY display_order, id")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if closeErr := rows.Close(); closeErr != nil {
|
|
log.Printf("Error closing rows: %v", closeErr)
|
|
}
|
|
}()
|
|
|
|
var siteIDs []int
|
|
for rows.Next() {
|
|
var siteID int
|
|
if scanErr := rows.Scan(&siteID); scanErr != nil {
|
|
return scanErr
|
|
}
|
|
siteIDs = append(siteIDs, siteID)
|
|
}
|
|
|
|
if rowsErr := rows.Err(); rowsErr != nil {
|
|
return rowsErr
|
|
}
|
|
|
|
for i, siteID := range siteIDs {
|
|
newOrder := i + 1
|
|
if _, err = tx.Exec("UPDATE sites SET display_order = $1 WHERE id = $2", newOrder, siteID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func moveSiteHandler(db *sql.DB) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
idStr := mux.Vars(r)["id"]
|
|
positionStr := mux.Vars(r)["position"]
|
|
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid ID", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
targetPosition, err := strconv.Atoi(positionStr)
|
|
if err != nil {
|
|
http.Error(w, "Invalid position", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var currentOrder int
|
|
err = db.QueryRow("SELECT display_order FROM sites WHERE id = $1", id).Scan(¤tOrder)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
http.Error(w, "Site not found", http.StatusNotFound)
|
|
} else {
|
|
log.Printf("Error fetching site order: %v", err)
|
|
http.Error(w, "Error fetching site", http.StatusInternalServerError)
|
|
}
|
|
return
|
|
}
|
|
|
|
if currentOrder == targetPosition {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
|
|
tx, err := db.Begin()
|
|
if err != nil {
|
|
log.Printf("Error starting transaction: %v", err)
|
|
http.Error(w, "Error moving site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer func() {
|
|
if rollbackErr := tx.Rollback(); rollbackErr != nil && !errors.Is(rollbackErr, sql.ErrTxDone) {
|
|
log.Printf("Error rolling back transaction: %v", rollbackErr)
|
|
}
|
|
}()
|
|
|
|
if currentOrder < targetPosition {
|
|
_, err = tx.Exec(`
|
|
UPDATE sites
|
|
SET display_order = display_order - 1
|
|
WHERE display_order > $1 AND display_order <= $2
|
|
`, currentOrder, targetPosition)
|
|
} else {
|
|
_, err = tx.Exec(`
|
|
UPDATE sites
|
|
SET display_order = display_order + 1
|
|
WHERE display_order >= $2 AND display_order < $1
|
|
`, currentOrder, targetPosition)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Printf("Error updating display orders: %v", err)
|
|
http.Error(w, "Error moving site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
_, err = tx.Exec("UPDATE sites SET display_order = $1 WHERE id = $2", targetPosition, id)
|
|
if err != nil {
|
|
log.Printf("Error setting new position: %v", err)
|
|
http.Error(w, "Error moving site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
log.Printf("Error committing transaction: %v", err)
|
|
http.Error(w, "Error moving site", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
}
|
|
|
|
func getAllSites(db *sql.DB) ([]models.Site, error) {
|
|
rows, err := db.QueryContext(
|
|
context.Background(), `
|
|
SELECT s.id, s.slug, s.name, s.url, s.is_up, s.enabled, s.last_check, s.favicon, s.user_id, u.telegram_username
|
|
FROM sites s
|
|
LEFT JOIN users u ON s.user_id = u.id
|
|
ORDER BY s.display_order
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if closeErr := rows.Close(); closeErr != nil {
|
|
log.Printf("Error closing rows: %v", closeErr)
|
|
}
|
|
}()
|
|
|
|
var sites []models.Site
|
|
for rows.Next() {
|
|
var site models.Site
|
|
var telegramUsername sql.NullString
|
|
scanErr := rows.Scan(&site.ID, &site.Slug, &site.Name, &site.URL, &site.IsUp,
|
|
&site.Enabled, &site.LastCheck, &site.Favicon, &site.UserID, &telegramUsername)
|
|
if scanErr != nil {
|
|
return nil, scanErr
|
|
}
|
|
site.LastCheck = math.Round(site.LastCheck * millisecondsMultiplier)
|
|
|
|
if telegramUsername.Valid {
|
|
site.TelegramUsername = &telegramUsername.String
|
|
}
|
|
|
|
sites = append(sites, site)
|
|
}
|
|
|
|
if rowsErr := rows.Err(); rowsErr != nil {
|
|
return nil, rowsErr
|
|
}
|
|
|
|
return sites, nil
|
|
}
|
|
|
|
func sanitizeTelegramUsername(username string) string {
|
|
username = strings.ToLower(strings.TrimPrefix(strings.TrimSpace(username), "@"))
|
|
if matched, err := regexp.MatchString("^[a-z0-9_]{4,32}$", username); !matched {
|
|
if err != nil {
|
|
log.Printf("Error validating Telegram username: %v", err)
|
|
} else {
|
|
log.Printf("Invalid Telegram username format: %s", username)
|
|
}
|
|
return ""
|
|
}
|
|
return username
|
|
}
|
|
|
|
func findOrCreateUserByTelegramUsername(db *sql.DB, username string) (*int, error) {
|
|
if username == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
var userID int
|
|
usernameLower := strings.ToLower(username)
|
|
|
|
err := db.QueryRow("SELECT id FROM users WHERE LOWER(telegram_username) = LOWER($1)", usernameLower).Scan(&userID)
|
|
if err == nil {
|
|
return &userID, nil
|
|
}
|
|
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
return nil, fmt.Errorf("error querying user: %w", err)
|
|
}
|
|
|
|
err = db.QueryRow(`
|
|
INSERT INTO users (telegram_username, telegram_id)
|
|
VALUES ($1, NULL)
|
|
RETURNING id
|
|
`, usernameLower).Scan(&userID)
|
|
|
|
if err != nil {
|
|
var pqErr *pq.Error
|
|
if errors.As(err, &pqErr) && pqErr.Code.Name() == uniqueViolation {
|
|
err = db.QueryRow("SELECT id FROM users WHERE LOWER(telegram_username) = LOWER($1)", usernameLower).Scan(&userID)
|
|
if err == nil {
|
|
return &userID, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("error creating user: %w", err)
|
|
}
|
|
|
|
return &userID, nil
|
|
}
|