From 31e0162cc8bd0479ebe455dfabb8b9b0d506e105 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sun, 31 May 2026 18:28:28 -0400 Subject: [PATCH 1/5] Clearing files (#2) Reviewed-on: http://git.kundeng.us/phoenix/textsender-api/pulls/2 --- .gitea/workflows/workflow.yaml | 134 -- .gitignore | 6 - Dockerfile | 54 - Makefile | 21 - README.md | 21 - cmd/api/main.go | 161 --- cmd/api/main_test.go | 129 -- docker-compose.yaml | 112 -- docs/docs.go | 1282 ----------------- docs/swagger.json | 1258 ---------------- docs/swagger.yaml | 810 ----------- go.mod | 48 - go.sum | 122 -- internal/app/app.go | 9 - internal/config/config.go | 168 --- internal/db/connection.go | 136 -- internal/handler/contact.go | 246 ---- internal/handler/contact_test.go | 123 -- internal/handler/draftMessage.go | 15 - internal/handler/endpoint/endpoint.go | 29 - internal/handler/helper.go | 46 - internal/handler/helper_test.go | 111 -- internal/handler/instant_message.go | 189 --- internal/handler/message.go | 180 --- internal/handler/message_event_response.go | 174 --- .../handler/message_event_response_test.go | 162 --- internal/handler/message_test.go | 74 - internal/handler/schedule_message_status.go | 169 --- .../handler/schedule_message_status_test.go | 75 - internal/handler/scheduled_message.go | 242 ---- internal/handler/scheduled_message_event.go | 247 ---- .../handler/scheduled_message_event_test.go | 182 --- internal/handler/scheduled_message_test.go | 141 -- internal/middleware/auth.go | 47 - internal/middleware/middleware.go | 22 - internal/model/messageDraft.go | 6 - internal/services/jwt_service.go | 49 - internal/store/contact_store.go | 182 --- .../store/message_event_response_store.go | 61 - internal/store/message_store.go | 95 -- internal/store/mock/contact_store.go | 146 -- .../mock/message_event_response_store.go | 81 -- .../mock/scheduled_message_event_store.go | 159 -- .../store/mock/scheduled_message_store.go | 141 -- internal/store/mock/store.go | 101 -- .../store/scheduled_message_event_store.go | 113 -- internal/store/scheduled_message_store.go | 127 -- internal/version/version.go | 17 - 48 files changed, 8223 deletions(-) delete mode 100644 .gitea/workflows/workflow.yaml delete mode 100644 .gitignore delete mode 100644 Dockerfile delete mode 100644 Makefile delete mode 100644 README.md delete mode 100644 cmd/api/main.go delete mode 100644 cmd/api/main_test.go delete mode 100644 docker-compose.yaml delete mode 100644 docs/docs.go delete mode 100644 docs/swagger.json delete mode 100644 docs/swagger.yaml delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 internal/app/app.go delete mode 100644 internal/config/config.go delete mode 100644 internal/db/connection.go delete mode 100644 internal/handler/contact.go delete mode 100644 internal/handler/contact_test.go delete mode 100644 internal/handler/draftMessage.go delete mode 100644 internal/handler/endpoint/endpoint.go delete mode 100644 internal/handler/helper.go delete mode 100644 internal/handler/helper_test.go delete mode 100644 internal/handler/instant_message.go delete mode 100644 internal/handler/message.go delete mode 100644 internal/handler/message_event_response.go delete mode 100644 internal/handler/message_event_response_test.go delete mode 100644 internal/handler/message_test.go delete mode 100644 internal/handler/schedule_message_status.go delete mode 100644 internal/handler/schedule_message_status_test.go delete mode 100644 internal/handler/scheduled_message.go delete mode 100644 internal/handler/scheduled_message_event.go delete mode 100644 internal/handler/scheduled_message_event_test.go delete mode 100644 internal/handler/scheduled_message_test.go delete mode 100644 internal/middleware/auth.go delete mode 100644 internal/middleware/middleware.go delete mode 100644 internal/model/messageDraft.go delete mode 100644 internal/services/jwt_service.go delete mode 100644 internal/store/contact_store.go delete mode 100644 internal/store/message_event_response_store.go delete mode 100644 internal/store/message_store.go delete mode 100644 internal/store/mock/contact_store.go delete mode 100644 internal/store/mock/message_event_response_store.go delete mode 100644 internal/store/mock/scheduled_message_event_store.go delete mode 100644 internal/store/mock/scheduled_message_store.go delete mode 100644 internal/store/mock/store.go delete mode 100644 internal/store/scheduled_message_event_store.go delete mode 100644 internal/store/scheduled_message_store.go delete mode 100644 internal/version/version.go diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml deleted file mode 100644 index 2c1eef7..0000000 --- a/.gitea/workflows/workflow.yaml +++ /dev/null @@ -1,134 +0,0 @@ -name: Go - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - runs-on: ubuntu-24.04 # You can change this to macos-latest or windows-latest if needed - - steps: - - uses: actions/checkout@v5 - - - name: Set up Go - uses: actions/setup-go@v6 - with: - go-version: '1.26.2' - - - name: Build - run: | - echo "Initializing config" - mkdir -p ~/.ssh - echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key - chmod 600 ~/.ssh/textsender_models_deploy_key - ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts - - eval $(ssh-agent -s) - ssh-add -v ~/.ssh/textsender_models_deploy_key - - go env -w GOPRIVATE='${{ secrets.GIT_HOST_ROOT }}' - - echo "Creating local .gitconfig" - touch ~/.gitconfig - cat > ~/.gitconfig << "EOF" - [url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"] - insteadOf = https://${{ secrets.GIT_HOST_ROOT }} - [url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"] - insteadOf = http://${{ secrets.GIT_HOST_ROOT }} - EOF - - echo "Building binary" - make build - - echo "Binary built" - file textsender-api - - test: - name: Test - runs-on: ubuntu-24.04 - services: - postgres: - image: postgres:18.4-alpine - env: - POSTGRES_USER: ${{ secrets.DB_TEST_USER }} - POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }} - POSTGRES_DB: ${{ secrets.DB_TEST_NAME }} - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Setup Go - uses: actions/setup-go@v4 - with: - go-version: '1.26.2' - - - name: Install PostgreSQL client - run: sudo apt update && sudo apt-get install -y postgresql-client - - - name: Wait for PostgreSQL to be ready - run: | - for i in {1..30}; do - if pg_isready -h postgres -p 5432; then - echo "PostgreSQL is ready" - exit 0 - fi - echo "Waiting for PostgreSQL... Attempt $i" - sleep 2 - done - echo "PostgreSQL did not start in time" - exit 1 - - - name: Run tests - env: - DB_NAME: ${{ secrets.DB_TEST_NAME }} - DB_USER: ${{ secrets.DB_TEST_USER }} - DB_PASSWORD: ${{ secrets.DB_TEST_PASSWORD }} - DB_HOST: postgres - DB_PORT: 5432 - DB_SSLMODE: disable - SECRET_KEY: ${{ secrets.SECRET_KEY }} - run: | - echo "JWT_SECRET=$SECRET_KEY" > .env - echo "DB_NAME=$DB_NAME" >> .env - echo "DB_USER=$DB_USER" >> .env - echo "DB_PASSWORD=$DB_PASSWORD" >> .env - echo "DB_HOST=$DB_HOST" >> .env - echo "DB_PORT=$DB_PORT" >> .env - echo "DB_SSLMODE=$DB_SSLMODE" >> .env - echo "TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U" >> .env - echo "TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T" >> .env - echo "TWILIO_AUTH_TOKEN=f4a1f2b0b79ea3735078c2d8ee9684e1" >> .env - echo "TWILIO_PHONE_NUMBER=10123456789" >> .env - echo "ALLOWED_ORIGINS=http://localhost:5173" >> .env - - echo "Initializing config" - mkdir -p ~/.ssh - echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender_models_deploy_key - chmod 600 ~/.ssh/textsender_models_deploy_key - ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts - - eval $(ssh-agent -s) - ssh-add -v ~/.ssh/textsender_models_deploy_key - - go env -w GOPRIVATE='${{ secrets.GIT_HOST_ROOT }}' - - echo "Creating local .gitconfig" - touch ~/.gitconfig - cat > ~/.gitconfig << "EOF" - [url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"] - insteadOf = https://${{ secrets.GIT_HOST_ROOT }} - [url "ssh://git@${{ secrets.GIT_HOST_ROOT }}"] - insteadOf = http://${{ secrets.GIT_HOST_ROOT }} - EOF - - go test -v ./... diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 80799e6..0000000 --- a/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/textsender-api -/vendor/ - -.env -.env.local -.env.docker diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ad58f9d..0000000 --- a/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -# Multi-stage Dockerfile for Go application -FROM golang:1.26.2 AS builder - -WORKDIR /app - -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - openssh-client git - -RUN mkdir -p -m 0700 ~/.ssh && \ - ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts - -# Configure Git to use SSH for GitHub -RUN git config --global url."git@git.kundeng.us:".insteadOf "https://git.kundeng.us/" && \ - git config --global url."git@git.kundeng.us:".insteadOf "http://git.kundeng.us/" - -# Set up the Go environment for private modules -ENV GOPRIVATE=git.kundeng.us - - -# Copy go mod and sum files -COPY go.mod go.sum ./ - -RUN --mount=type=ssh \ - go mod download - -# Copy source code -COPY ./cmd ./cmd -COPY ./internal ./internal -COPY ./Makefile . -COPY ./.env . -COPY ./migrations ./migrations -COPY ./docs ./docs - -# Build the application -RUN CGO_ENABLED=0 GOOS=linux make build - -# Runtime stage -FROM alpine:latest AS production - -RUN apk --no-cache add ca-certificates - -WORKDIR /root/ - -# Copy the pre-built binary file from the previous stage -COPY --from=builder /app/textsender-api . -COPY --from=builder /app/.env ./ -COPY --from=builder /app/migrations ./migrations - -# Expose port -EXPOSE 8080 - -# Command to run the executable -CMD ["./textsender-api"] diff --git a/Makefile b/Makefile deleted file mode 100644 index 296dfe1..0000000 --- a/Makefile +++ /dev/null @@ -1,21 +0,0 @@ -VERSION ?= $(shell git describe --tags 2>/dev/null || echo "dev") -COMMIT ?= $(shell git rev-parse --short HEAD) -BUILD_TIME ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) -GO_VERSION ?= $(shell go version | awk '{print $$3}') - -.PHONY: build -build: - go build -ldflags="\ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.Version=$(VERSION)' \ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.BuildTime=$(BUILD_TIME)' \ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.Commit=$(COMMIT)' \ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.GoVersion=$(GO_VERSION)'" \ - -o textsender-api cmd/api/main.go - -.PHONY: install -install: - go install -ldflags="\ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.Version=$(VERSION)' \ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.BuildTime=$(BUILD_TIME)' \ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.Commit=$(COMMIT)' \ - -X 'git.kundeng.us/phoenix/textsender-api/internal/version.GoVersion=$(GO_VERSION)'" diff --git a/README.md b/README.md deleted file mode 100644 index 13a8c36..0000000 --- a/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# TextSender-API -A software system to process Text messaging queueing - - -## Getting started - - -### Building -```SHELL -make build -``` - - -Generate API documentation -``` -go install github.com/swaggo/swag/cmd/swag@latest - -swag init --generalInfo main.go --dir ./cmd/api,./internal/handler --output docs/ --parseDependency --parseInternal -``` - -The API documentation can be viewed from `http://localhost:8080/swagger/index.html`. diff --git a/cmd/api/main.go b/cmd/api/main.go deleted file mode 100644 index f0f1fa6..0000000 --- a/cmd/api/main.go +++ /dev/null @@ -1,161 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - "github.com/go-chi/cors" - "github.com/swaggo/http-swagger/v2" - - _ "git.kundeng.us/phoenix/textsender-api/docs" - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/config" - database "git.kundeng.us/phoenix/textsender-api/internal/db" - "git.kundeng.us/phoenix/textsender-api/internal/handler" - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - mdlware "git.kundeng.us/phoenix/textsender-api/internal/middleware" - "git.kundeng.us/phoenix/textsender-api/internal/services" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -// @title textsender-api -// @version 1.0 -// @description Core API to send text messages - -// @host localhost:8080 -// @BasePath /api/v1 - -// @securityDefinitions.apikey BearerAuth -// @in header -// @name Authorization -// @description JWT Bearer Token -func main() { - cfg, tCfg, err := config.Load() - if err != nil { - log.Fatal("Error:", err) - } else if cfg == nil { - log.Fatal("Error initializing config") - } else if cfg.JWTSecret == "" { - log.Fatal("Error: JWTSecret not initialized") - } - - apiApp := app.App{TwilioConfig: tCfg} - - db, err := database.NewDatabase(cfg.GetDBConnString()) - if err != nil { - log.Fatal("Failed to connect to database:", err) - } - defer db.Close() - - ctx := context.Background() - - if cfg.ResetDB { - log.Println("Resetting database") - if err := db.ResetDatabase(ctx); err != nil { - log.Fatal("Failed to reset database:", err) - } - log.Println("Database reset completed. Exiting.") - return - } else { - if exists, err := database.TableExists(ctx, db.Pool, "contacts"); err == nil && !exists { - log.Println("Resetting database") - if err = db.ResetDatabase(ctx); err != nil { - log.Fatal("Error:", err) - } - } else { - log.Fatal("Error:", err) - } - } - - jwtService := services.NewJWTService(cfg.JWTSecret) - - contactStore := store.NewContactStore(db.Pool) - messageStore := store.NewMessageStore(db.Pool) - schStore := store.NewScheduledMessageStore(db.Pool) - schMsgEventStore := store.NewScheduledMessageEventStore(db.Pool) - merStore := store.NewMessageEventResponseStore(db.Pool) - - contactHandler := handler.NewContactHandler(&apiApp, contactStore) - messageHandler := handler.NewMessageHandler(&apiApp, messageStore) - scheduledMessageHandler := handler.NewScheduledMessageHandler(&apiApp, schStore) - scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(&apiApp, schMsgEventStore, schStore) - scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(&apiApp, schMsgEventStore, schStore) - eventHandler := handler.NewEventResponseHandler(&apiApp, merStore) - instantMessageHandler := handler.NewSendInstantMessageHandler(&apiApp, merStore, contactStore, messageStore) - - router := chi.NewRouter() - - // Configure CORS - router.Use(cors.Handler(cors.Options{ - AllowedOrigins: cfg.AllowedOrigins, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, - AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, - ExposedHeaders: []string{"Link", "X-Total-Count"}, - AllowCredentials: true, - MaxAge: 300, // 5 minutes - })) - router.Use(middleware.Logger) - router.Use(middleware.Recoverer) - router.Use(middleware.Timeout(60 * time.Second)) - router.Use(mdlware.JSONContentType) - - router.Method("POST", endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact))) - router.Method("GET", endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact))) - router.Method("POST", endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage))) - router.Method("GET", endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage))) - router.Method("POST", endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage))) - router.Method("POST", endpoint.AddEventToScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.AddScheduledMessageEvent))) - router.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent))) - router.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent))) - router.Method("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageStatusHandler.UpdateStatus))) - router.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage))) - router.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage))) - router.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse))) - router.Method("GET", endpoint.FetchMessageEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.Fetch))) - router.Method("POST", endpoint.SendInstantMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(instantMessageHandler.Send))) - router.Method("PATCH", endpoint.Update_Names_Endpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.UpdateName))) - - router.Method("GET", "/swagger/*", httpSwagger.Handler( - httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.PORT)), - )) - - // Start server - server := &http.Server{ - Addr: ":" + cfg.ServerPort, - Handler: router, - ReadTimeout: 15 * time.Second, - WriteTimeout: 15 * time.Second, - IdleTimeout: 60 * time.Second, - } - - // Graceful shutdown - go func() { - log.Printf("Server starting on port %s", cfg.ServerPort) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server failed to start: %v", err) - } - }() - - // Wait for interrupt signal - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Shutting down server...") - - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - log.Fatalf("Server forced to shutdown: %v", err) - } - - log.Println("Server exited") -} diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go deleted file mode 100644 index c6b0c3d..0000000 --- a/cmd/api/main_test.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "context" - "flag" - "log" - "net/http" - "os" - "path" - "testing" - - "github.com/go-chi/chi/v5" - "github.com/joho/godotenv" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/config" - "git.kundeng.us/phoenix/textsender-api/internal/db" - "git.kundeng.us/phoenix/textsender-api/internal/handler" - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - mdlware "git.kundeng.us/phoenix/textsender-api/internal/middleware" - "git.kundeng.us/phoenix/textsender-api/internal/services" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -var testRouter *chi.Mux - -func TestMain(m *testing.M) { - cfg, apiApp := load() - - database, err := db.NewDatabase(cfg.GetDBConnString()) - if err != nil { - log.Println(err.Error()) - log.Fatal("Failed to initialize database") - } - defer database.Close() - - ctx := context.Background() - - if cfg.ResetDB { - if err = database.ResetDatabase(ctx); err != nil { - log.Println(err.Error()) - log.Fatalln("Failed to initialize database") - } - } else { - if exists, err := db.TableExists(ctx, database.Pool, "contacts "); err == nil && !exists { - log.Println("Resetting database") - err = database.ResetDatabase(ctx) - if err != nil { - log.Println("Error:", err) - } - } else { - log.Println("Error:", err) - } - } - - jwtService := services.NewJWTService(cfg.JWTSecret) - - contactStore := store.NewContactStore(database.Pool) - messageStore := store.NewMessageStore(db.Pool) - schStore := store.NewScheduledMessageStore(db.Pool) - schMsgEventStore := store.NewScheduledMessageEventStore(db.Pool) - merStore := store.NewMessageEventResponseStore(db.Pool) - - contactHandler := handler.NewContactHandler(apiApp, contactStore) - messageHandler := handler.NewMessageHandler(apiApp, messageStore) - scheduledMessageHandler := handler.NewScheduledMessageHandler(apiApp, schStore) - scheduledMessageEventHandler := handler.NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schStore) - scheduledMessageStatusHandler := handler.NewScheduledMessageStatusHandler(apiApp, schMsgEventStore, schStore) - eventHandler := handler.NewEventResponseHandler(apiApp, merStore) - instantMessageHandler := handler.NewSendInstantMessageHandler(apiApp, merStore, contactStore, messageStore) - - testRouter = chi.NewRouter() - testRouter.Handle(endpoint.ADD_CONTACT_ENDPOINT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.AddContact))) - testRouter.Handle(endpoint.GET_CONTACT, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.GetContact))) - testRouter.Handle(endpoint.ADD_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.AddMessage))) - testRouter.Handle(endpoint.GET_MESSAGE, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(messageHandler.GetMessage))) - testRouter.Handle(endpoint.ScheduleMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.AddScheduledMessage))) - testRouter.Method("GET", endpoint.GetScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.GetScheduledMessageEvent))) - testRouter.Method("DELETE", endpoint.DeleteScheduledMessageEventEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageEventHandler.DeleteScheduledMessageEvent))) - testRouter.Method("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageStatusHandler.UpdateStatus))) - testRouter.Method("GET", endpoint.GetScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.GetScheduledMessage))) - testRouter.Method("GET", endpoint.FetchNextScheduledMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(scheduledMessageHandler.FetchNextMessage))) - testRouter.Method("POST", endpoint.RecordEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.RecordResponse))) - testRouter.Method("GET", endpoint.FetchMessageEventResponse, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(eventHandler.Fetch))) - testRouter.Method("POST", endpoint.SendInstantMessageEndpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(instantMessageHandler.Send))) - testRouter.Method("PATCH", endpoint.Update_Names_Endpoint, mdlware.AuthMiddleware(jwtService)(http.HandlerFunc(contactHandler.UpdateName))) - - code := m.Run() - os.Exit(code) -} - -func load() (*config.Config, *app.App) { - resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit") - port := flag.String("port", config.PORT, "Server port") - flag.Parse() - - cwd, _ := os.Getwd() - envPath := path.Join(cwd, ".env") - - err := godotenv.Load(envPath) - if err != nil { - envPath = path.Join(cwd, "../..", ".env") - if err := godotenv.Load(envPath); err != nil { - panic("Error loading .env file: " + err.Error()) - } - } - - unpackedConnString := config.UnpackDBConnString() - dbConnString := unpackedConnString.Parse() - - if tCfg, err := config.TwilioConfig(); err != nil { - log.Fatal("Error: ", err) - return nil, nil - } else { - return &config.Config{ - DBConnString: dbConnString, - ServerPort: *port, - ResetDB: *resetDb, - }, &app.App{TwilioConfig: tCfg} - } -} - -func resetTestDB(t *testing.T) { - t.Helper() - _, err := db.Pool.Exec(context.Background(), "DELETE FROM contacts") - if err != nil { - t.Fatalf("Failed to reset test database: %v", err) - } -} diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index b1b641c..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,112 +0,0 @@ -version: '3.8' # Use a recent version - -services: - api: - build: # Tells docker-compose to build the Dockerfile in the current directory - context: . - ssh: ["default"] # Uses host's SSH agent - container_name: textsender_api # Optional: Give the container a specific name - ports: - # Map host port 8000 to container port 3000 (adjust as needed) - - "8080:8080" - env_file: - - .env - depends_on: - main_db: - condition: service_healthy # Wait for the DB to be healthy before starting the app - networks: - - textsender-network - restart: unless-stopped # Optional: Restart policy - - auth_api: - build: - context: ../textsender-auth - ssh: ["default"] # Uses host's SSH agent - dockerfile: Dockerfile - container_name: textsender_auth # Optional: Give the container a specific name - ports: - # Map host port 8000 to container port 3000 (adjust as needed) - - "9080:9080" - env_file: - - ../textsender-auth/.env - depends_on: - auth_db: - condition: service_healthy # Wait for the DB to be healthy before starting the app - networks: - - textsender-network - restart: unless-stopped # Optional: Restart policy - - catapult: - build: - context: ../catapult - ssh: ["default"] - dockerfile: Dockerfile - container_name: catapult - env_file: - - ../catapult/.env - depends_on: - - auth_api - - api - networks: - - textsender-network - restart: unless-stopped - - # PostgreSQL Database Service - main_db: - image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller) - container_name: textsender_db # Optional: Give the container a specific name - environment: - # These MUST match the user, password, and database name in the DATABASE_URL above - POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender} - POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password} - POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_db} - volumes: - # Persist database data using a named volume - - postgres_data:/var/lib/postgresql - ports: - - "5432:5432" - healthcheck: - # Checks if Postgres is ready to accept connections - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - networks: - - textsender-network - restart: always # Optional: Restart policy - - auth_db: - image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller) - container_name: textsender_auth_db # Optional: Give the container a specific name - environment: - # These MUST match the user, password, and database name in the DATABASE_URL above - POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender_auth} - POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password} - POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_auth_db} - volumes: - # Persist database data using a named volume - - postgres_data_auth:/var/lib/postgresql - ports: - - "5433:5432" - healthcheck: - # Checks if Postgres is ready to accept connections - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] - interval: 10s - timeout: 5s - retries: 5 - start_period: 10s - networks: - - textsender-network - restart: always # Optional: Restart policy - -# Define the named volume for data persistence -volumes: - postgres_data: - driver: local # Use the default local driver - postgres_data_auth: - driver: local # Use the default local driver - -networks: - textsender-network: - driver: bridge diff --git a/docs/docs.go b/docs/docs.go deleted file mode 100644 index 64e9ff5..0000000 --- a/docs/docs.go +++ /dev/null @@ -1,1282 +0,0 @@ -// Package docs Code generated by swaggo/swag. DO NOT EDIT -package docs - -import "github.com/swaggo/swag" - -const docTemplate = `{ - "schemes": {{ marshal .Schemes }}, - "swagger": "2.0", - "info": { - "description": "{{escape .Description}}", - "title": "{{.Title}}", - "contact": {}, - "version": "{{.Version}}" - }, - "host": "{{.Host}}", - "basePath": "{{.BasePath}}", - "paths": { - "/contact": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get a contact record to have a recipient to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contacts" - ], - "summary": "Get contact", - "parameters": [ - { - "type": "string", - "description": "Contact Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetContactResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetContactResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetContactResponse" - } - } - } - } - }, - "/contact/new": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a contact record to have a recipient to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contacts" - ], - "summary": "Add contact", - "parameters": [ - { - "description": "Data to add contact", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddContact" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddContactResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddContactResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddContactResponse" - } - } - } - } - }, - "/contact/update": { - "patch": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update the first, last, or nickname of a Contact (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contacts" - ], - "summary": "Update names of Contact", - "parameters": [ - { - "description": "Data to update contact's names", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.UpdateNameRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.UpdateNameResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.UpdateNameResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.UpdateNameResponse" - } - } - } - } - }, - "/instant/message": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Send a message without the constraints of scheduled messages (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "messages" - ], - "summary": "Send instant message", - "parameters": [ - { - "description": "Send an instnt message", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageResponse" - } - } - } - } - }, - "/message": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get a message record to have a recipient to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "messages" - ], - "summary": "Get message", - "parameters": [ - { - "type": "string", - "description": "Message Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetMessageResponse" - } - } - } - } - }, - "/message/new": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a message record to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "messages" - ], - "summary": "Add message", - "parameters": [ - { - "description": "Data to add message", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddMessage" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddMessageResponse" - } - } - } - } - }, - "/schedule/message": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get a scheduled message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled messages" - ], - "summary": "Get scheduled message", - "parameters": [ - { - "type": "string", - "description": "Contact Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Adds a scheduled message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled messages" - ], - "summary": "Add scheduled message", - "parameters": [ - { - "description": "Data to add scheduled message", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddScheduledMessage" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageResponse" - } - } - } - } - }, - "/schedule/message/event": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Gets a scheduled message event (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled message events" - ], - "summary": "Get scheduled message event", - "parameters": [ - { - "type": "string", - "description": "Contact Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Scheduled Message Id", - "name": "scheduled_message_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageEventResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a scheduled message event (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled message events" - ], - "summary": "Add scheduled message event", - "parameters": [ - { - "description": "Data to add scheduled message event", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddScheduledMessageEvent" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageEventResponse" - } - } - } - } - }, - "/schedule/message/event/response": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Fetches a MessageEventResponse given a user id (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "message" - ], - "summary": "Fetcht MessageEventResponse", - "parameters": [ - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse" - } - } - } - } - }, - "/schedule/message/event/response/record": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Saves the result of sending a text message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "message" - ], - "summary": "Record MessageEventResponse", - "parameters": [ - { - "description": "Data to add MessageEventResponse", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RecordEventRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.RecordEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.RecordEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.RecordEventResponse" - } - } - } - } - }, - "/schedule/message/event/{id}": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Deletes a scheduled message event (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled message events" - ], - "summary": "Delete scheduled message event", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "scheduled message event Id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse" - } - } - } - } - }, - "/schedule/message/fetch": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Fetches a scheduled message that is available to be processed (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled messages" - ], - "summary": "Fetch scheduled message", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.FetchNextMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.FetchNextMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.FetchNextMessageResponse" - } - } - } - } - }, - "/schedule/message/status/update": { - "patch": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update the status of a scheduled message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "status" - ], - "summary": "Update status", - "parameters": [ - { - "description": "Used to update scheduled message status", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestScheduledMessageStatus" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - } - } - } - } - }, - "definitions": { - "contact.Contact": { - "type": "object", - "properties": { - "first_name": { - "type": "string" - }, - "id": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone_number": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "event.MessageEventResponse": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "response": { - "type": "object", - "additionalProperties": {} - }, - "scheduled_message_event_id": { - "type": "string" - }, - "sent": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.AddContactResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/contact.Contact" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.AddMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/message.Message" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.AddScheduledMessageEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessageEvent" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.AddScheduledMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.DeleteScheduledMessageEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessageEvent" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.FetchNextMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetContactResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/contact.Contact" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetMessageEventResponseFetchedResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/event.MessageEventResponse" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/message.Message" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetScheduledMessageEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessageEvent" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetScheduledMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.RecordEventRequest": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "response": { - "type": "object", - "additionalProperties": {} - }, - "scheduled_message_event_id": { - "type": "string" - }, - "sent": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RecordEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/event.MessageEventResponse" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.RequestAddContact": { - "type": "object", - "properties": { - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone_number": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RequestAddMessage": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RequestAddScheduledMessage": { - "type": "object", - "properties": { - "scheduled": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RequestAddScheduledMessageEvent": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "scheduled_message_id": { - "type": "string" - } - } - }, - "handler.RequestScheduledMessageStatus": { - "type": "object", - "properties": { - "scheduled_message_id": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "handler.ScheduledMessageChange": { - "type": "object", - "properties": { - "old_status": { - "type": "string" - }, - "scheduled_message": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - } - }, - "handler.ScheduledMessageStatusResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/handler.ScheduledMessageChange" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.SendInstantMessageRequest": { - "type": "object", - "properties": { - "contact_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "message_id": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.SendInstantMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/event.MessageEventResponse" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.UpdateNameRequest": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.UpdateNameResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/contact.Contact" - } - }, - "message": { - "type": "string" - } - } - }, - "message.Message": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "id": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "scheduling.ScheduledMessage": { - "type": "object", - "properties": { - "created": { - "type": "string" - }, - "id": { - "type": "string" - }, - "scheduled": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "scheduling.ScheduledMessageEvent": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "created": { - "type": "string" - }, - "id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "scheduled_message_id": { - "type": "string" - } - } - } - }, - "securityDefinitions": { - "BearerAuth": { - "description": "JWT Bearer Token", - "type": "apiKey", - "name": "Authorization", - "in": "header" - } - } -}` - -// SwaggerInfo holds exported Swagger Info so clients can modify it -var SwaggerInfo = &swag.Spec{ - Version: "1.0", - Host: "localhost:8080", - BasePath: "/api/v1", - Schemes: []string{}, - Title: "textsender-api", - Description: "Core API to send text messages", - InfoInstanceName: "swagger", - SwaggerTemplate: docTemplate, - LeftDelim: "{{", - RightDelim: "}}", -} - -func init() { - swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) -} diff --git a/docs/swagger.json b/docs/swagger.json deleted file mode 100644 index c08fe8b..0000000 --- a/docs/swagger.json +++ /dev/null @@ -1,1258 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "description": "Core API to send text messages", - "title": "textsender-api", - "contact": {}, - "version": "1.0" - }, - "host": "localhost:8080", - "basePath": "/api/v1", - "paths": { - "/contact": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get a contact record to have a recipient to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contacts" - ], - "summary": "Get contact", - "parameters": [ - { - "type": "string", - "description": "Contact Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetContactResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetContactResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetContactResponse" - } - } - } - } - }, - "/contact/new": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a contact record to have a recipient to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contacts" - ], - "summary": "Add contact", - "parameters": [ - { - "description": "Data to add contact", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddContact" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddContactResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddContactResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddContactResponse" - } - } - } - } - }, - "/contact/update": { - "patch": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update the first, last, or nickname of a Contact (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "contacts" - ], - "summary": "Update names of Contact", - "parameters": [ - { - "description": "Data to update contact's names", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.UpdateNameRequest" - } - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.UpdateNameResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.UpdateNameResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.UpdateNameResponse" - } - } - } - } - }, - "/instant/message": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Send a message without the constraints of scheduled messages (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "messages" - ], - "summary": "Send instant message", - "parameters": [ - { - "description": "Send an instnt message", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.SendInstantMessageResponse" - } - } - } - } - }, - "/message": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get a message record to have a recipient to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "messages" - ], - "summary": "Get message", - "parameters": [ - { - "type": "string", - "description": "Message Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetMessageResponse" - } - } - } - } - }, - "/message/new": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a message record to send a text to (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "messages" - ], - "summary": "Add message", - "parameters": [ - { - "description": "Data to add message", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddMessage" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddMessageResponse" - } - } - } - } - }, - "/schedule/message": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Get a scheduled message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled messages" - ], - "summary": "Get scheduled message", - "parameters": [ - { - "type": "string", - "description": "Contact Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Adds a scheduled message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled messages" - ], - "summary": "Add scheduled message", - "parameters": [ - { - "description": "Data to add scheduled message", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddScheduledMessage" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageResponse" - } - } - } - } - }, - "/schedule/message/event": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Gets a scheduled message event (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled message events" - ], - "summary": "Get scheduled message event", - "parameters": [ - { - "type": "string", - "description": "Contact Id", - "name": "id", - "in": "path", - "required": true - }, - { - "type": "string", - "description": "Scheduled Message Id", - "name": "scheduled_message_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetScheduledMessageEventResponse" - } - } - } - }, - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Add a scheduled message event (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled message events" - ], - "summary": "Add scheduled message event", - "parameters": [ - { - "description": "Data to add scheduled message event", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestAddScheduledMessageEvent" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.AddScheduledMessageEventResponse" - } - } - } - } - }, - "/schedule/message/event/response": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Fetches a MessageEventResponse given a user id (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "message" - ], - "summary": "Fetcht MessageEventResponse", - "parameters": [ - { - "type": "string", - "description": "User Id", - "name": "user_id", - "in": "path", - "required": true - } - ], - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.GetMessageEventResponseFetchedResponse" - } - } - } - } - }, - "/schedule/message/event/response/record": { - "post": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Saves the result of sending a text message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "message" - ], - "summary": "Record MessageEventResponse", - "parameters": [ - { - "description": "Data to add MessageEventResponse", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RecordEventRequest" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.RecordEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.RecordEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.RecordEventResponse" - } - } - } - } - }, - "/schedule/message/event/{id}": { - "delete": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Deletes a scheduled message event (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled message events" - ], - "summary": "Delete scheduled message event", - "parameters": [ - { - "type": "string", - "format": "uuid", - "description": "scheduled message event Id", - "name": "id", - "in": "path", - "required": true - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.DeleteScheduledMessageEventResponse" - } - } - } - } - }, - "/schedule/message/fetch": { - "get": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Fetches a scheduled message that is available to be processed (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "scheduled messages" - ], - "summary": "Fetch scheduled message", - "responses": { - "200": { - "description": "OK", - "schema": { - "$ref": "#/definitions/handler.FetchNextMessageResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.FetchNextMessageResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.FetchNextMessageResponse" - } - } - } - } - }, - "/schedule/message/status/update": { - "patch": { - "security": [ - { - "BearerAuth": [] - } - ], - "description": "Update the status of a scheduled message (requires JWT)", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "status" - ], - "summary": "Update status", - "parameters": [ - { - "description": "Used to update scheduled message status", - "name": "request", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/handler.RequestScheduledMessageStatus" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - }, - "400": { - "description": "Bad Request", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - }, - "404": { - "description": "Not Found", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/handler.ScheduledMessageStatusResponse" - } - } - } - } - } - }, - "definitions": { - "contact.Contact": { - "type": "object", - "properties": { - "first_name": { - "type": "string" - }, - "id": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone_number": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "event.MessageEventResponse": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "response": { - "type": "object", - "additionalProperties": {} - }, - "scheduled_message_event_id": { - "type": "string" - }, - "sent": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.AddContactResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/contact.Contact" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.AddMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/message.Message" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.AddScheduledMessageEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessageEvent" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.AddScheduledMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.DeleteScheduledMessageEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessageEvent" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.FetchNextMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetContactResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/contact.Contact" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetMessageEventResponseFetchedResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/event.MessageEventResponse" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/message.Message" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetScheduledMessageEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessageEvent" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.GetScheduledMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.RecordEventRequest": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "response": { - "type": "object", - "additionalProperties": {} - }, - "scheduled_message_event_id": { - "type": "string" - }, - "sent": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RecordEventResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/event.MessageEventResponse" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.RequestAddContact": { - "type": "object", - "properties": { - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "phone_number": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RequestAddMessage": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RequestAddScheduledMessage": { - "type": "object", - "properties": { - "scheduled": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.RequestAddScheduledMessageEvent": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "scheduled_message_id": { - "type": "string" - } - } - }, - "handler.RequestScheduledMessageStatus": { - "type": "object", - "properties": { - "scheduled_message_id": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, - "handler.ScheduledMessageChange": { - "type": "object", - "properties": { - "old_status": { - "type": "string" - }, - "scheduled_message": { - "$ref": "#/definitions/scheduling.ScheduledMessage" - } - } - }, - "handler.ScheduledMessageStatusResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/handler.ScheduledMessageChange" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.SendInstantMessageRequest": { - "type": "object", - "properties": { - "contact_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "message_id": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.SendInstantMessageResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/event.MessageEventResponse" - } - }, - "message": { - "type": "string" - } - } - }, - "handler.UpdateNameRequest": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "nickname": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "handler.UpdateNameResponse": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/contact.Contact" - } - }, - "message": { - "type": "string" - } - } - }, - "message.Message": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "id": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "scheduling.ScheduledMessage": { - "type": "object", - "properties": { - "created": { - "type": "string" - }, - "id": { - "type": "string" - }, - "scheduled": { - "type": "string" - }, - "status": { - "type": "string" - }, - "user_id": { - "type": "string" - } - } - }, - "scheduling.ScheduledMessageEvent": { - "type": "object", - "properties": { - "contact_id": { - "type": "string" - }, - "created": { - "type": "string" - }, - "id": { - "type": "string" - }, - "message_id": { - "type": "string" - }, - "scheduled_message_id": { - "type": "string" - } - } - } - }, - "securityDefinitions": { - "BearerAuth": { - "description": "JWT Bearer Token", - "type": "apiKey", - "name": "Authorization", - "in": "header" - } - } -} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml deleted file mode 100644 index 53252df..0000000 --- a/docs/swagger.yaml +++ /dev/null @@ -1,810 +0,0 @@ -basePath: /api/v1 -definitions: - contact.Contact: - properties: - first_name: - type: string - id: - type: string - last_name: - type: string - nickname: - type: string - phone_number: - type: string - user_id: - type: string - type: object - event.MessageEventResponse: - properties: - contact_id: - type: string - id: - type: string - message_id: - type: string - response: - additionalProperties: {} - type: object - scheduled_message_event_id: - type: string - sent: - type: string - status: - type: string - user_id: - type: string - type: object - handler.AddContactResponse: - properties: - data: - items: - $ref: '#/definitions/contact.Contact' - type: array - message: - type: string - type: object - handler.AddMessageResponse: - properties: - data: - items: - $ref: '#/definitions/message.Message' - type: array - message: - type: string - type: object - handler.AddScheduledMessageEventResponse: - properties: - data: - items: - $ref: '#/definitions/scheduling.ScheduledMessageEvent' - type: array - message: - type: string - type: object - handler.AddScheduledMessageResponse: - properties: - data: - items: - $ref: '#/definitions/scheduling.ScheduledMessage' - type: array - message: - type: string - type: object - handler.DeleteScheduledMessageEventResponse: - properties: - data: - items: - $ref: '#/definitions/scheduling.ScheduledMessageEvent' - type: array - message: - type: string - type: object - handler.FetchNextMessageResponse: - properties: - data: - items: - $ref: '#/definitions/scheduling.ScheduledMessage' - type: array - message: - type: string - type: object - handler.GetContactResponse: - properties: - data: - items: - $ref: '#/definitions/contact.Contact' - type: array - message: - type: string - type: object - handler.GetMessageEventResponseFetchedResponse: - properties: - data: - items: - $ref: '#/definitions/event.MessageEventResponse' - type: array - message: - type: string - type: object - handler.GetMessageResponse: - properties: - data: - items: - $ref: '#/definitions/message.Message' - type: array - message: - type: string - type: object - handler.GetScheduledMessageEventResponse: - properties: - data: - items: - $ref: '#/definitions/scheduling.ScheduledMessageEvent' - type: array - message: - type: string - type: object - handler.GetScheduledMessageResponse: - properties: - data: - items: - $ref: '#/definitions/scheduling.ScheduledMessage' - type: array - message: - type: string - type: object - handler.RecordEventRequest: - properties: - contact_id: - type: string - message_id: - type: string - response: - additionalProperties: {} - type: object - scheduled_message_event_id: - type: string - sent: - type: string - status: - type: string - user_id: - type: string - type: object - handler.RecordEventResponse: - properties: - data: - items: - $ref: '#/definitions/event.MessageEventResponse' - type: array - message: - type: string - type: object - handler.RequestAddContact: - properties: - first_name: - type: string - last_name: - type: string - nickname: - type: string - phone_number: - type: string - user_id: - type: string - type: object - handler.RequestAddMessage: - properties: - content: - type: string - user_id: - type: string - type: object - handler.RequestAddScheduledMessage: - properties: - scheduled: - type: string - status: - type: string - user_id: - type: string - type: object - handler.RequestAddScheduledMessageEvent: - properties: - contact_id: - type: string - message_id: - type: string - scheduled_message_id: - type: string - type: object - handler.RequestScheduledMessageStatus: - properties: - scheduled_message_id: - type: string - status: - type: string - type: object - handler.ScheduledMessageChange: - properties: - old_status: - type: string - scheduled_message: - $ref: '#/definitions/scheduling.ScheduledMessage' - type: object - handler.ScheduledMessageStatusResponse: - properties: - data: - items: - $ref: '#/definitions/handler.ScheduledMessageChange' - type: array - message: - type: string - type: object - handler.SendInstantMessageRequest: - properties: - contact_ids: - items: - type: string - type: array - message_id: - type: string - user_id: - type: string - type: object - handler.SendInstantMessageResponse: - properties: - data: - items: - $ref: '#/definitions/event.MessageEventResponse' - type: array - message: - type: string - type: object - handler.UpdateNameRequest: - properties: - contact_id: - type: string - first_name: - type: string - last_name: - type: string - nickname: - type: string - user_id: - type: string - type: object - handler.UpdateNameResponse: - properties: - data: - items: - $ref: '#/definitions/contact.Contact' - type: array - message: - type: string - type: object - message.Message: - properties: - content: - type: string - id: - type: string - user_id: - type: string - type: object - scheduling.ScheduledMessage: - properties: - created: - type: string - id: - type: string - scheduled: - type: string - status: - type: string - user_id: - type: string - type: object - scheduling.ScheduledMessageEvent: - properties: - contact_id: - type: string - created: - type: string - id: - type: string - message_id: - type: string - scheduled_message_id: - type: string - type: object -host: localhost:8080 -info: - contact: {} - description: Core API to send text messages - title: textsender-api - version: "1.0" -paths: - /contact: - get: - consumes: - - application/json - description: Get a contact record to have a recipient to send a text to (requires - JWT) - parameters: - - description: Contact Id - in: path - name: id - required: true - type: string - - description: User Id - in: path - name: user_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.GetContactResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.GetContactResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.GetContactResponse' - security: - - BearerAuth: [] - summary: Get contact - tags: - - contacts - /contact/new: - post: - consumes: - - application/json - description: Add a contact record to have a recipient to send a text to (requires - JWT) - parameters: - - description: Data to add contact - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.RequestAddContact' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.AddContactResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.AddContactResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.AddContactResponse' - security: - - BearerAuth: [] - summary: Add contact - tags: - - contacts - /contact/update: - patch: - consumes: - - application/json - description: Update the first, last, or nickname of a Contact (requires JWT) - parameters: - - description: Data to update contact's names - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.UpdateNameRequest' - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.UpdateNameResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.UpdateNameResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.UpdateNameResponse' - security: - - BearerAuth: [] - summary: Update names of Contact - tags: - - contacts - /instant/message: - post: - consumes: - - application/json - description: Send a message without the constraints of scheduled messages (requires - JWT) - parameters: - - description: Send an instnt message - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.SendInstantMessageRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.SendInstantMessageResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.SendInstantMessageResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.SendInstantMessageResponse' - security: - - BearerAuth: [] - summary: Send instant message - tags: - - messages - /message: - get: - consumes: - - application/json - description: Get a message record to have a recipient to send a text to (requires - JWT) - parameters: - - description: Message Id - in: path - name: id - required: true - type: string - - description: User Id - in: path - name: user_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.GetMessageResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.GetMessageResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.GetMessageResponse' - security: - - BearerAuth: [] - summary: Get message - tags: - - messages - /message/new: - post: - consumes: - - application/json - description: Add a message record to send a text to (requires JWT) - parameters: - - description: Data to add message - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.RequestAddMessage' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.AddMessageResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.AddMessageResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.AddMessageResponse' - security: - - BearerAuth: [] - summary: Add message - tags: - - messages - /schedule/message: - get: - consumes: - - application/json - description: Get a scheduled message (requires JWT) - parameters: - - description: Contact Id - in: path - name: id - required: true - type: string - - description: User Id - in: path - name: user_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.GetScheduledMessageResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.GetScheduledMessageResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.GetScheduledMessageResponse' - security: - - BearerAuth: [] - summary: Get scheduled message - tags: - - scheduled messages - post: - consumes: - - application/json - description: Adds a scheduled message (requires JWT) - parameters: - - description: Data to add scheduled message - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.RequestAddScheduledMessage' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.AddScheduledMessageResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.AddScheduledMessageResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.AddScheduledMessageResponse' - security: - - BearerAuth: [] - summary: Add scheduled message - tags: - - scheduled messages - /schedule/message/event: - get: - consumes: - - application/json - description: Gets a scheduled message event (requires JWT) - parameters: - - description: Contact Id - in: path - name: id - required: true - type: string - - description: Scheduled Message Id - in: path - name: scheduled_message_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.GetScheduledMessageEventResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.GetScheduledMessageEventResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.GetScheduledMessageEventResponse' - security: - - BearerAuth: [] - summary: Get scheduled message event - tags: - - scheduled message events - post: - consumes: - - application/json - description: Add a scheduled message event (requires JWT) - parameters: - - description: Data to add scheduled message event - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.RequestAddScheduledMessageEvent' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.AddScheduledMessageEventResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.AddScheduledMessageEventResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.AddScheduledMessageEventResponse' - security: - - BearerAuth: [] - summary: Add scheduled message event - tags: - - scheduled message events - /schedule/message/event/{id}: - delete: - consumes: - - application/json - description: Deletes a scheduled message event (requires JWT) - parameters: - - description: scheduled message event Id - format: uuid - in: path - name: id - required: true - type: string - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.DeleteScheduledMessageEventResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.DeleteScheduledMessageEventResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.DeleteScheduledMessageEventResponse' - security: - - BearerAuth: [] - summary: Delete scheduled message event - tags: - - scheduled message events - /schedule/message/event/response: - get: - consumes: - - application/json - description: Fetches a MessageEventResponse given a user id (requires JWT) - parameters: - - description: User Id - in: path - name: user_id - required: true - type: string - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.GetMessageEventResponseFetchedResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.GetMessageEventResponseFetchedResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.GetMessageEventResponseFetchedResponse' - security: - - BearerAuth: [] - summary: Fetcht MessageEventResponse - tags: - - message - /schedule/message/event/response/record: - post: - consumes: - - application/json - description: Saves the result of sending a text message (requires JWT) - parameters: - - description: Data to add MessageEventResponse - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.RecordEventRequest' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.RecordEventResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.RecordEventResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.RecordEventResponse' - security: - - BearerAuth: [] - summary: Record MessageEventResponse - tags: - - message - /schedule/message/fetch: - get: - consumes: - - application/json - description: Fetches a scheduled message that is available to be processed (requires - JWT) - produces: - - application/json - responses: - "200": - description: OK - schema: - $ref: '#/definitions/handler.FetchNextMessageResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.FetchNextMessageResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.FetchNextMessageResponse' - security: - - BearerAuth: [] - summary: Fetch scheduled message - tags: - - scheduled messages - /schedule/message/status/update: - patch: - consumes: - - application/json - description: Update the status of a scheduled message (requires JWT) - parameters: - - description: Used to update scheduled message status - in: body - name: request - required: true - schema: - $ref: '#/definitions/handler.RequestScheduledMessageStatus' - produces: - - application/json - responses: - "201": - description: Created - schema: - $ref: '#/definitions/handler.ScheduledMessageStatusResponse' - "400": - description: Bad Request - schema: - $ref: '#/definitions/handler.ScheduledMessageStatusResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/handler.ScheduledMessageStatusResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/handler.ScheduledMessageStatusResponse' - security: - - BearerAuth: [] - summary: Update status - tags: - - status -securityDefinitions: - BearerAuth: - description: JWT Bearer Token - in: header - name: Authorization - type: apiKey -swagger: "2.0" diff --git a/go.mod b/go.mod deleted file mode 100644 index 696ac94..0000000 --- a/go.mod +++ /dev/null @@ -1,48 +0,0 @@ -module git.kundeng.us/phoenix/textsender-api - -go 1.26.2 - -require ( - git.kundeng.us/phoenix/swoosh v0.2.1 - git.kundeng.us/phoenix/textsender-models v0.2.1 - github.com/go-chi/chi/v5 v5.2.5 - github.com/go-chi/cors v1.2.2 - github.com/golang-jwt/jwt/v5 v5.3.1 - github.com/google/uuid v1.6.0 - github.com/jackc/pgx/v5 v5.9.2 - github.com/joho/godotenv v1.5.1 - github.com/stretchr/testify v1.11.1 - github.com/swaggo/http-swagger/v2 v2.0.2 - github.com/swaggo/swag v1.16.6 -) - -require ( - github.com/KyleBanks/depth v1.2.1 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-openapi/jsonpointer v0.23.1 // indirect - github.com/go-openapi/jsonreference v0.21.5 // indirect - github.com/go-openapi/spec v0.22.4 // indirect - github.com/go-openapi/swag/conv v0.26.0 // indirect - github.com/go-openapi/swag/jsonname v0.26.0 // indirect - github.com/go-openapi/swag/jsonutils v0.26.0 // indirect - github.com/go-openapi/swag/loading v0.26.0 // indirect - github.com/go-openapi/swag/stringutils v0.26.0 // indirect - github.com/go-openapi/swag/typeutils v0.26.0 // indirect - github.com/go-openapi/swag/yamlutils v0.26.0 // indirect - github.com/golang/mock v1.6.0 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/swaggo/files/v2 v2.0.2 // indirect - github.com/twilio/twilio-go v1.30.5 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/text v0.36.0 // indirect - golang.org/x/tools v0.44.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index c090c0f..0000000 --- a/go.sum +++ /dev/null @@ -1,122 +0,0 @@ -git.kundeng.us/phoenix/swoosh v0.2.1 h1:mJ/zXBOqzi0YTrSm5kxvahzp2WH7lx4R3iIO0s9VFFI= -git.kundeng.us/phoenix/swoosh v0.2.1/go.mod h1:81XNzmTmDPuDPmXMUmKTLErNSKKBM+hsW5U60Lm78ww= -git.kundeng.us/phoenix/textsender-models v0.2.1 h1:21br4NF58aUFuCx8laKxC5RvZMl4GsSIaMX4bvf5plw= -git.kundeng.us/phoenix/textsender-models v0.2.1/go.mod h1:nu5QWy9o+spx/t9NFipaGmF5qiBJS/0QhxyCjoi3Z3E= -github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= -github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= -github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= -github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= -github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4= -github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY= -github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= -github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= -github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= -github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= -github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= -github.com/go-openapi/swag/conv v0.26.0 h1:5yGGsPYI1ZCva93U0AoKi/iZrNhaJEjr324YVsiD89I= -github.com/go-openapi/swag/conv v0.26.0/go.mod h1:tpAmIL7X58VPnHHiSO4uE3jBeRamGsFsfdDeDtb5ECE= -github.com/go-openapi/swag/jsonname v0.26.0 h1:gV1NFX9M8avo0YSpmWogqfQISigCmpaiNci8cGECU5w= -github.com/go-openapi/swag/jsonname v0.26.0/go.mod h1:urBBR8bZNoDYGr653ynhIx+gTeIz0ARZxHkAPktJK2M= -github.com/go-openapi/swag/jsonutils v0.26.0 h1:FawFML2iAXsPqmERscuMPIHmFsoP1tOqWkxBaKNMsnA= -github.com/go-openapi/swag/jsonutils v0.26.0/go.mod h1:2VmA0CJlyFqgawOaPI9psnjFDqzyivIqLYN34t9p91E= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0 h1:apqeINu/ICHouqiRZbyFvuDge5jCmmLTqGQ9V95EaOM= -github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.0/go.mod h1:AyM6QT8uz5IdKxk5akv0y6u4QvcL9GWERt0Jx/F/R8Y= -github.com/go-openapi/swag/loading v0.26.0 h1:Apg6zaKhCJurpJer0DCxq99qwmhFddBhaMX7kilDcko= -github.com/go-openapi/swag/loading v0.26.0/go.mod h1:dBxQ/6V2uBaAQdevN18VELE6xSpJWZxLX4txe12JwDg= -github.com/go-openapi/swag/stringutils v0.26.0 h1:qZQngLxs5s7SLijc3N2ZO+fUq2o8LjuWAASSrJuh+xg= -github.com/go-openapi/swag/stringutils v0.26.0/go.mod h1:sWn5uY+QIIspwPhvgnqJsH8xqFT2ZbYcvbcFanRyhFE= -github.com/go-openapi/swag/typeutils v0.26.0 h1:2kdEwdiNWy+JJdOvu5MA2IIg2SylWAFuuyQIKYybfq4= -github.com/go-openapi/swag/typeutils v0.26.0/go.mod h1:oovDuIUvTrEHVMqWilQzKzV4YlSKgyZmFh7AlfABNVE= -github.com/go-openapi/swag/yamlutils v0.26.0 h1:H7O8l/8NJJQ/oiReEN+oMpnGMyt8G0hl460nRZxhLMQ= -github.com/go-openapi/swag/yamlutils v0.26.0/go.mod h1:1evKEGAtP37Pkwcc7EWMF0hedX0/x3Rkvei2wtG/TbU= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.2 h1:5zRca5jw7lzVREKCZVNBpysDNBjj74rBh0N2BGQbSR0= -github.com/go-openapi/testify/enable/yaml/v2 v2.4.2/go.mod h1:XVevPw5hUXuV+5AkI1u1PeAm27EQVrhXTTCPAF85LmE= -github.com/go-openapi/testify/v2 v2.4.2 h1:tiByHpvE9uHrrKjOszax7ZvKB7QOgizBWGBLuq0ePx4= -github.com/go-openapi/testify/v2 v2.4.2/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= -github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275 h1:IZycmTpoUtQK3PD60UYBwjaCUHUP7cML494ao9/O8+Q= -github.com/localtunnel/go-localtunnel v0.0.0-20170326223115-8a804488f275/go.mod h1:zt6UU74K6Z6oMOYJbJzYpYucqdcQwSMPBEdSvGiaUMw= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU= -github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0= -github.com/swaggo/http-swagger/v2 v2.0.2 h1:FKCdLsl+sFCx60KFsyM0rDarwiUSZ8DqbfSyIKC9OBg= -github.com/swaggo/http-swagger/v2 v2.0.2/go.mod h1:r7/GBkAWIfK6E/OLnE8fXnviHiDeAHmgIyooa4xm3AQ= -github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= -github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= -github.com/twilio/twilio-go v1.30.5 h1:hi6+2kMte29zrFBsw7VSSNtbF30GPMGz/4LRIUgXng8= -github.com/twilio/twilio-go v1.30.5/go.mod h1:QbitvbvtkV77Jn4BABAKVmxabYSjMyQG4tHey9gfPqg= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= -golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/app.go b/internal/app/app.go deleted file mode 100644 index 9948d51..0000000 --- a/internal/app/app.go +++ /dev/null @@ -1,9 +0,0 @@ -package app - -import ( - auxcfg "git.kundeng.us/phoenix/textsender-models/tx0/config/auxiliary" -) - -type App struct { - TwilioConfig *auxcfg.TwilioConfig -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index d23f4dc..0000000 --- a/internal/config/config.go +++ /dev/null @@ -1,168 +0,0 @@ -package config - -import ( - "flag" - "fmt" - "os" - "strconv" - "strings" - - auxcfg "git.kundeng.us/phoenix/textsender-models/tx0/config/auxiliary" - "github.com/joho/godotenv" - - "git.kundeng.us/phoenix/textsender-api/internal/version" -) - -type Config struct { - DBConnString string - ServerPort string - ResetDB bool - JWTSecret string `env:"JWT_SECRET" required:"true"` - TwilioConfig *auxcfg.TwilioConfig - AllowedOrigins []string -} - -type ConnectionInfo struct { - Username string - Password string - Database string - Host string - Port int - SslMode string -} - -const PORT = "8080" -const APP_NAME = "textsender-api" - -func PortString() string { - portMessage := fmt.Sprintf(":%s", PORT) - - return portMessage -} - -func (ci ConnectionInfo) Parse() string { - return fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=%s", ci.Username, ci.Password, ci.Host, ci.Port, ci.Database, ci.SslMode) -} - -func PrintName() { - fmt.Println(APP_NAME) - fmt.Println(version.String()) -} - -func Load() (*Config, *auxcfg.TwilioConfig, error) { - versionFlag := flag.Bool("version", false, "Print version information") - resetDb := flag.Bool("reset-db", false, "Reset the database schema and exit") - port := flag.String("port", PORT, "Server port") - flag.Parse() - - if *versionFlag { - fmt.Println(version.String()) - os.Exit(-1) - } - - if err := godotenv.Load(); err != nil { - return nil, nil, fmt.Errorf("Error loading .env file") - } - - unpackedConnString := UnpackDBConnString() - dbConnString := unpackedConnString.Parse() - - if cfg, err := TwilioConfig(); err != nil { - return nil, nil, err - } else { - allowedOrigins := unpackAllowedOrigins() - return &Config{ - DBConnString: dbConnString, - ServerPort: *port, - ResetDB: *resetDb, - JWTSecret: os.Getenv("JWT_SECRET"), - AllowedOrigins: allowedOrigins, - }, cfg, nil - } -} - -func UnpackDBConnString() (connInfo ConnectionInfo) { - username := os.Getenv("DB_USER") - password := os.Getenv("DB_PASSWORD") - host := os.Getenv("DB_HOST") - port := os.Getenv("DB_PORT") - database := os.Getenv("DB_NAME") - sslMode := os.Getenv("DB_SSLMODE") - - if username != "" { - connInfo.Username = username - } else { - connInfo.Username = "user" - } - - if password != "" { - connInfo.Password = password - } else { - connInfo.Password = "password" - } - - if host != "" { - connInfo.Host = host - } else { - connInfo.Host = "localhost" - } - - if port != "" { - num, err := strconv.Atoi(port) - if err != nil { - return - } - connInfo.Port = num - } else { - connInfo.Port = 5432 - } - - if database != "" { - connInfo.Database = database - } else { - connInfo.Database = "textsender_db" - } - - if sslMode != "" { - connInfo.SslMode = sslMode - } else { - connInfo.SslMode = "disable" - } - - return -} - -func unpackAllowedOrigins() []string { - allowedOriginsRaw := os.Getenv("ALLOWED_ORIGINS") - allowedOriginsSplit := strings.Split(allowedOriginsRaw, ",") - return allowedOriginsSplit -} - -func TwilioConfig() (*auxcfg.TwilioConfig, error) { - authSid := os.Getenv("TWILIO_AUTH_SID") - serviceSid := os.Getenv("TWILIO_SERVICE_SID") - authToken := os.Getenv("TWILIO_AUTH_TOKEN") - phoneNumber := os.Getenv("TWILIO_PHONE_NUMBER") - - if len(authSid) == 0 { - return nil, fmt.Errorf("Twilio config account sid not found") - } else if len(serviceSid) == 0 { - return nil, fmt.Errorf("Twilio config service sid not found") - } else if len(authToken) == 0 { - return nil, fmt.Errorf("Twilio config auth token not found") - } else if len(phoneNumber) == 0 { - return nil, fmt.Errorf("Twilio config phone number not found") - } else { - var cfg auxcfg.TwilioConfig - cfg.AccountSID = authSid - cfg.ServiceSID = serviceSid - cfg.AuthToken = authToken - cfg.Number = phoneNumber - - return &cfg, nil - } -} - -func (c *Config) GetDBConnString() string { - return c.DBConnString -} diff --git a/internal/db/connection.go b/internal/db/connection.go deleted file mode 100644 index feab2a3..0000000 --- a/internal/db/connection.go +++ /dev/null @@ -1,136 +0,0 @@ -// db/connection.go -package db - -import ( - "context" - "fmt" - "log" - "os" - "path" - "strings" - "time" - - "github.com/jackc/pgx/v5/pgxpool" -) - -var Pool *pgxpool.Pool - -type Database struct { - Pool *pgxpool.Pool -} - -func NewDatabase(connString string) (*Database, error) { - ctx := context.Background() - // Parse the connection string and create pool configuration - config, err := pgxpool.ParseConfig(connString) - if err != nil { - return nil, fmt.Errorf("unable to parse DSN: %v", err) - } - - // Configure connection pool settings - config.MaxConns = 25 - config.MinConns = 5 - config.MaxConnLifetime = time.Hour - config.MaxConnIdleTime = 30 * time.Minute - config.HealthCheckPeriod = time.Minute - - // Configure connection timeouts - config.ConnConfig.ConnectTimeout = 10 * time.Second - config.ConnConfig.RuntimeParams["timezone"] = "UTC" - - // Create the connection pool - pool, err := pgxpool.NewWithConfig(ctx, config) - if err != nil { - return nil, fmt.Errorf("unable to create connection pool: %v", err) - } - - // Test the connection with a short timeout - if err := pool.Ping(ctx); err != nil { - pool.Close() - return nil, fmt.Errorf("unable to ping database: %v", err) - } - - log.Printf("Successfully connected to database") - return &Database{Pool: pool}, nil -} - -func TableExists(ctx context.Context, conn *pgxpool.Pool, tableName string) (bool, error) { - var exists bool - - query := ` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'public' - AND table_name = $1 - ); - ` - - err := conn.QueryRow(ctx, query, tableName).Scan(&exists) - if err != nil { - return false, fmt.Errorf("error checking if table exists: %w", err) - } - - return exists, nil -} - -func (db *Database) Close() { - if db.Pool != nil { - db.Pool.Close() - log.Println("Database connection pool closed") - } -} - -// HealthCheck verifies the database connection is still alive -func (db *Database) HealthCheck(ctx context.Context) error { - return db.Pool.Ping(ctx) -} - -// GetPoolStats returns connection pool statistics -func (db *Database) GetPoolStats() string { - stats := db.Pool.Stat() - return fmt.Sprintf("TotalConns: %d, IdleConns: %d, AcquiredConns: %d", - stats.TotalConns(), stats.IdleConns(), stats.AcquiredConns()) -} - -func (db *Database) ResetDatabase(ctx context.Context) error { - tx, err := db.Pool.Begin(ctx) - if err != nil { - return fmt.Errorf("Transaction unable to begin: %v", err) - } - defer tx.Rollback(ctx) - - schemaContent, err := os.ReadFile("migrations/schema.sql") - if err != nil { - log.Println("Default migrations not found. Checking different directory") - cwd, _ := os.Getwd() - migrationsPath := path.Join(cwd, "../..", "migrations/schema.sql") - schemaContent, err = os.ReadFile(migrationsPath) - if err != nil { - return fmt.Errorf("error reading schema file: %v", err) - } - } - - for _, stmt := range strings.Split(string(schemaContent), ";") { - stmt = strings.TrimSpace(stmt) - if stmt == "" { - continue - } - - if strings.HasPrefix(stmt, "--") { - continue - } - - _, err := tx.Exec(ctx, stmt) - if err != nil { - return fmt.Errorf("error executing SQL statement: %v\nStatement: %s", err, stmt) - } - } - - if err := tx.Commit(ctx); err != nil { - return fmt.Errorf("unable to commit transaction: %v", err) - } - - log.Println("Database schema applied successfully") - - return nil -} diff --git a/internal/handler/contact.go b/internal/handler/contact.go deleted file mode 100644 index 52a0095..0000000 --- a/internal/handler/contact.go +++ /dev/null @@ -1,246 +0,0 @@ -package handler - -import ( - "fmt" - "log" - "net/http" - - "git.kundeng.us/phoenix/textsender-models/tx0/contact" - "github.com/google/uuid" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -type ContactHandler struct { - App *app.App - ContactStore store.ContactStore -} - -func NewContactHandler(apiApp *app.App, str store.ContactStore) *ContactHandler { - return &ContactHandler{App: apiApp, ContactStore: str} -} - -type RequestAddContact struct { - PhoneNumber string `json:"phone_number"` - UserId uuid.UUID `json:"user_id"` - Firstname *string `json:"first_name,omitempty"` - Lastname *string `json:"last_name,omitempty"` - Nickname *string `json:"nickname,omitempty"` -} - -type AddContactResponse struct { - Message string `json:"message"` - Data []contact.Contact `json:"data"` -} - -// AddContact godoc -// @Summary Add contact -// @Description Add a contact record to have a recipient to send a text to (requires JWT) -// @Tags contacts -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body RequestAddContact true "Data to add contact" -// @Success 201 {object} AddContactResponse -// @Failure 400 {object} AddContactResponse -// @Failure 500 {object} AddContactResponse -// @Router /contact/new [post] -func (c *ContactHandler) AddContact(w http.ResponseWriter, r *http.Request) { - var req RequestAddContact - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - } - defer r.Body.Close() - - newContact := contact.Contact{PhoneNumber: req.PhoneNumber, UserId: &req.UserId, Firstname: req.Firstname, Lastname: req.Lastname, Nickname: req.Nickname} - - var statusCode int - var resp AddContactResponse - - ctx := r.Context() - - if exists, err := c.ContactStore.ContactExists(ctx, newContact.PhoneNumber, *newContact.UserId); err != nil { - log.Printf("Error: %v", err) - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - if exists { - statusCode = http.StatusBadRequest - resp.Message = "Cannot create contact" - } else { - if err := c.ContactStore.CreateContact(ctx, &newContact); err != nil { - log.Printf("Error: %v", err) - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - statusCode = http.StatusCreated - resp.Message = "Contact created" - resp.Data = append(resp.Data, newContact) - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -type GetContactResponse struct { - Message string `json:"message"` - Data []contact.Contact `json:"data"` -} - -// GetContact godoc -// @Summary Get contact -// @Description Get a contact record to have a recipient to send a text to (requires JWT) -// @Tags contacts -// @Accept json -// @Produce json -// @Param id path string true "Contact Id" -// @Param user_id path string true "User Id" -// @Security BearerAuth -// @Success 200 {object} GetContactResponse -// @Failure 400 {object} GetContactResponse -// @Failure 500 {object} GetContactResponse -// @Router /contact [get] -func (c *ContactHandler) GetContact(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - fmt.Println("Method:", r.Method) - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - // One or the other - var id, userId uuid.UUID - queryParams := r.URL.Query() - - // Check if parameter exists - if _, exists := queryParams["id"]; !exists { - if _, exists := queryParams["user_id"]; !exists { - fmt.Fprintf(w, "Name parameter not provided") - http.Error(w, "Query params", http.StatusBadRequest) - return - } else { - var err error - userId, err = uuid.Parse(queryParams.Get("user_id")) - if err != nil { - fmt.Fprintf(w, "Name parameter exists: %s", userId) - } - } - } else { - var err error - idTmp := queryParams.Get("id") - id, err = uuid.Parse(idTmp) - if err != nil { - fmt.Println("Error:", err) - http.Error(w, "Error parsing Id", http.StatusBadRequest) - return - } - } - - var statusCode int - var resp GetContactResponse - - ctx := r.Context() - - if id != uuid.Nil { - fmt.Println("Checking with Id") - if con, err := c.ContactStore.GetContactByID(ctx, id); err == nil { - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, *con) - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } else if userId != uuid.Nil { - fmt.Println("Checking with User Id") - if contacts, err := c.ContactStore.GetAllContacts(ctx); err == nil { - for _, con := range contacts { - if *con.UserId == userId { - resp.Data = append(resp.Data, *con) - } - } - - if len(resp.Data) > 0 { - statusCode = http.StatusOK - resp.Message = "Successful" - } else { - statusCode = http.StatusNotFound - resp.Message = "Contact not found" - } - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } else { - statusCode = http.StatusBadRequest - resp.Message = "Invalid query parameter" - } - - RespondWithJSON(w, statusCode, &resp) -} - -type UpdateNameRequest struct { - Nickname *string `json:"nickname,omitempty"` - Firstname *string `json:"first_name,omitempty"` - Lastname *string `json:"last_name,omitempty"` - ContactId uuid.UUID `json:"contact_id"` - UserId uuid.UUID `json:"user_id"` -} - -type UpdateNameResponse struct { - Message string `json:"message"` - Data []contact.Contact `json:"data"` -} - -// UpdateName godoc -// @Summary Update names of Contact -// @Description Update the first, last, or nickname of a Contact (requires JWT) -// @Tags contacts -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body UpdateNameRequest true "Data to update contact's names" -// @Success 200 {object} UpdateNameResponse -// @Failure 400 {object} UpdateNameResponse -// @Failure 500 {object} UpdateNameResponse -// @Router /contact/update [patch] -func (c *ContactHandler) UpdateName(w http.ResponseWriter, r *http.Request) { - var req UpdateNameRequest - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - } - defer r.Body.Close() - - var statusCode int - var resp UpdateNameResponse - - if req.UserId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "User Id not provided" - } else if req.ContactId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Contact Id not provided" - } else if req.Firstname == nil && req.Lastname == nil && req.Nickname == nil { - statusCode = http.StatusBadRequest - resp.Message = "No name provided" - } else { - ctx := r.Context() - if con, err := c.ContactStore.GetContactByID(ctx, req.ContactId); err != nil { - resp.Message = err.Error() - statusCode = http.StatusInternalServerError - } else { - if affectedRows, err := c.ContactStore.UpdateNames(ctx, con, req.Firstname, req.Lastname, req.Nickname); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - log.Println("Updated", affectedRows, "rows") - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, *con) - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} diff --git a/internal/handler/contact_test.go b/internal/handler/contact_test.go deleted file mode 100644 index 218f08d..0000000 --- a/internal/handler/contact_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "git.kundeng.us/phoenix/textsender-models/tx0/contact" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - "git.kundeng.us/phoenix/textsender-api/internal/store/mock" -) - -type Request struct { - PhoneNumber string - UserId uuid.UUID -} - -func TestCreateContactWithMock(t *testing.T) { - mockstore := mock.NewMockContactStore() - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - - handler := NewContactHandler(apiApp, mockstore) - - testUserId := uuid.New() - testBody := Request{PhoneNumber: "+12335403383", UserId: testUserId} - jsonValue, _ := json.Marshal(testBody) - - req, _ := http.NewRequest("POST", endpoint.ADD_CONTACT_ENDPOINT, strings.NewReader(string(jsonValue))) - rr := httptest.NewRecorder() - - handler.AddContact(rr, req) - - assert.Equal(t, http.StatusCreated, rr.Code) - - var response AddContactResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error Creating contact %v", err) - - assert.NotEmpty(t, response.Data, "No Contact created") - - assert.NotNil(t, response.Data[0].Id, "Id should not be nil") -} - -func TestGetContactWithMock(t *testing.T) { - mockstore := mock.NewMockContactStore() - testUserId := uuid.New() - - testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: &testUserId} - ctx := t.Context() - if err := mockstore.CreateContact(ctx, &testCon); err != nil { - assert.NoError(t, err, "Error creating contact") - return - } - - url := fmt.Sprintf("%s?user_id=%s", endpoint.GET_CONTACT, testCon.UserId) - req, _ := http.NewRequest("GET", url, nil) - rr := httptest.NewRecorder() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - contactHandler := NewContactHandler(apiApp, mockstore) - contactHandler.GetContact(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response GetContactResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error getting contact %v", err) - - assert.NotEmpty(t, response.Data, "No Contact retrieved") - - assert.NotNil(t, response.Data[0].Id, "Id should not be nil") -} - -func TestUpdateContactNamesWithMock(t *testing.T) { - mockstore := mock.NewMockContactStore() - testUserId := uuid.New() - - testCon := contact.Contact{PhoneNumber: "+12335403383", UserId: &testUserId} - ctx := t.Context() - var firstname, lastname, nickname string - firstname = "Bob" - lastname = "De-Buildor" - nickname = "With The Tool" - if err := mockstore.CreateContact(ctx, &testCon); err != nil { - assert.NoError(t, err, "Error creating contact") - return - } - - var requestBody UpdateNameRequest - requestBody.Firstname = &firstname - requestBody.Lastname = &lastname - requestBody.Nickname = &nickname - requestBody.ContactId = *testCon.Id - requestBody.UserId = testUserId - jsonValue, _ := json.Marshal(requestBody) - req, _ := http.NewRequest("PATCH", endpoint.Update_Names_Endpoint, strings.NewReader(string(jsonValue))) - rr := httptest.NewRecorder() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - contactHandler := NewContactHandler(apiApp, mockstore) - contactHandler.UpdateName(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response UpdateNameResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error updating names of contact %v", err) - - assert.NotEmpty(t, response.Data, "No Contact updated") - - assert.NotNil(t, response.Data[0].Firstname, "Firstname should not be nil") - assert.NotNil(t, response.Data[0].Lastname, "Lastname should not be nil") - assert.NotNil(t, response.Data[0].Nickname, "Nickname should not be nil") -} diff --git a/internal/handler/draftMessage.go b/internal/handler/draftMessage.go deleted file mode 100644 index 9d76e83..0000000 --- a/internal/handler/draftMessage.go +++ /dev/null @@ -1,15 +0,0 @@ -package handler - -import "net/http" - -import "git.kundeng.us/phoenix/textsender-api/internal/model" - -// TODO: Remove file -func DraftMessageHandler(w http.ResponseWriter, r *http.Request) { - da := model.MessageItem{ - Id: 1, - Message: "The Word", - } - - RespondWithJSON(w, http.StatusOK, da) -} diff --git a/internal/handler/endpoint/endpoint.go b/internal/handler/endpoint/endpoint.go deleted file mode 100644 index cefaa9e..0000000 --- a/internal/handler/endpoint/endpoint.go +++ /dev/null @@ -1,29 +0,0 @@ -package endpoint - -const ( - MESSAGE_DRAFT_ENDPOINT = "/api/v1/message/draft" - - ADD_MESSAGE = "/api/v1/message/new" - GET_MESSAGE = "/api/v1/message" - - GET_CONTACT = "/api/v1/contact" - ADD_CONTACT_ENDPOINT = "/api/v1/contact/new" - Update_Names_Endpoint = "/api/v1/contact/update" - - ScheduleMessageEndpoint = "/api/v1/schedule/message" - GetScheduledMessageEndpoint = "/api/v1/schedule/message" - - AddEventToScheduledMessageEndpoint = "/api/v1/schedule/message/event" - GetScheduledMessageEventEndpoint = "/api/v1/schedule/message/event" - DeleteScheduledMessageEventEndpoint = "/api/v1/schedule/message/event/{id}" - UpdateScheduledMessageStatusEndpoint = "/api/v1/schedule/message/status/update" - - FetchNextScheduledMessageEndpoint = "/api/v1/schedule/message/fetch" - - // TODO: Tweak endpoint verbage for MessageEventResponse endpoint - // TODO: Make RecordEventResponse more consistent with MessageEventResponse - RecordEventResponse = "/api/v1/schedule/message/event/response/record" - FetchMessageEventResponse = "/api/v1/schedule/message/event/response" - - SendInstantMessageEndpoint = "/api/v1/instant/message" -) diff --git a/internal/handler/helper.go b/internal/handler/helper.go deleted file mode 100644 index adb68b2..0000000 --- a/internal/handler/helper.go +++ /dev/null @@ -1,46 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "log" - "net/http" -) - -func ExtractFromRequest(r *http.Request, reqItem interface{}) error { - err := json.NewDecoder(r.Body).Decode(&reqItem) - if err != nil { - return err - } else { - return nil - } -} - -func InvalidJson(err error) string { - return fmt.Sprintf("Invalid JSON: %s", err.Error()) -} - -// Helper function to send JSON responses -// TODO: Change type of data to any -func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - if err := json.NewEncoder(w).Encode(data); err != nil { - log.Printf("Error encoding JSON: %v", err) - } -} - -// Gets the query parameter from the URL -func ParseQueryParams(r *http.Request, query string) (*string, error) { - queryParams := r.URL.Query() - if _, exists := queryParams[query]; exists { - value := queryParams.Get(query) - if len(value) == 0 { - return nil, fmt.Errorf("Value of query parameter is empty") - } else { - return &value, nil - } - } else { - return nil, fmt.Errorf("Could not find query %s", query) - } -} diff --git a/internal/handler/helper_test.go b/internal/handler/helper_test.go deleted file mode 100644 index 81ca2ea..0000000 --- a/internal/handler/helper_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package handler - -import ( - "context" - "encoding/json" - "fmt" - "os" - "path" - "testing" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/contact" - "git.kundeng.us/phoenix/textsender-models/tx0/message" - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - "github.com/joho/godotenv" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/config" - "git.kundeng.us/phoenix/textsender-api/internal/db" -) - -func GetApp() (*app.App, error) { - cwd, _ := os.Getwd() - envPath := path.Join(cwd, ".env") - - err := godotenv.Load(envPath) - if err != nil { - envPath = path.Join(cwd, "../..", ".env") - if err := godotenv.Load(envPath); err != nil { - panic("Error loading .env file: " + err.Error()) - } - } - - if tCfg, err := config.TwilioConfig(); err != nil { - return nil, fmt.Errorf("Error: %v", err) - } else { - return &app.App{TwilioConfig: tCfg}, nil - } -} - -func ResetTestDB(t *testing.T, tableName string) { - t.Helper() - _, err := db.Pool.Exec(context.Background(), fmt.Sprintf("DELETE FROM %s", tableName)) - if err != nil { - t.Fatalf("Failed to reset test database: %v", err) - } -} - -func ContactTest(id uuid.UUID, userId uuid.UUID) contact.Contact { - if id == uuid.Nil { - id = uuid.New() - return contact.Contact{Id: &id, PhoneNumber: "+10123456789", UserId: &userId} - } else { - return contact.Contact{Id: &id, PhoneNumber: "+10123456789", UserId: &userId} - } -} - -func MessageTest(id uuid.UUID, userId uuid.UUID) message.Message { - if id == uuid.Nil { - id = uuid.New() - return message.Message{Id: &id, Content: "Oh how the mighty have fallen", UserId: &userId} - } else { - return message.Message{Id: &id, Content: "Oh how the mighty have fallen", UserId: &userId} - } -} - -func ScheduledMessageTest(id uuid.UUID, userId uuid.UUID, now time.Time) scheduling.ScheduledMessage { - if id == uuid.Nil { - return scheduling.ScheduledMessage{Id: uuid.New(), UserId: userId, Scheduled: now.Add(20 * time.Minute), Status: scheduling.Pending} - } else { - return scheduling.ScheduledMessage{Id: id, UserId: userId, Scheduled: now.Add(20 * time.Minute), Status: scheduling.Pending} - } -} - -func ScheduledMessageEventTest(messageId, contactId, scheduledMessageId uuid.UUID) scheduling.ScheduledMessageEvent { - return scheduling.ScheduledMessageEvent{MessageId: messageId, ContactId: contactId, ScheduledMessageId: scheduledMessageId} -} - -func MERResponseInString() string { - return `{ - "body": "Whoknows?", - "num_segments": "0", - "direction": "outbound-api", - "from": "+12243026041", - "to": "+16303831708", - "date_updated": "Sat,29Nov202519:06:59+0000", - "uri": "/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a.json", - "account_sid": "ACefa1ef516314c9d1a68cbd657de49277", - "num_media": "0", - "status": "scheduled", - "messaging_service_sid": "MG803f3676706b92eb02e18dd820c447f2", - "sid": "SM1193a529e7f7a840667cd1e0f13ea95a", - "date_created": "Sat,29Nov202519:06:59+0000", - "api_version": "2010-04-01", - "subresource_uris": { - "media": "/2010-04-01/Accounts/ACefa1ef516314c9d1a68cbd657de49277/Messages/SM1193a529e7f7a840667cd1e0f13ea95a/Media.json" - } -}` -} - -func ConvertStringToMapOfAny(response string) (map[string]any, error) { - bytes := []byte(response) - var merResponse map[string]any - err := json.Unmarshal(bytes, &merResponse) - if err != nil { - return nil, err - } else { - return merResponse, nil - } -} diff --git a/internal/handler/instant_message.go b/internal/handler/instant_message.go deleted file mode 100644 index 46ad663..0000000 --- a/internal/handler/instant_message.go +++ /dev/null @@ -1,189 +0,0 @@ -package handler - -import ( - "context" - "fmt" - "log" - "net/http" - "strings" - "time" - - "git.kundeng.us/phoenix/swoosh/swoop/send" - "git.kundeng.us/phoenix/textsender-models/tx0/contact" - "git.kundeng.us/phoenix/textsender-models/tx0/message" - "git.kundeng.us/phoenix/textsender-models/tx0/message/event" - "github.com/google/uuid" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -const Contact_Limit = 400 - -type SendInstantMessageHandler struct { - App *app.App - MERStore store.MessageEventResponseStore - ContactStore store.ContactStore - MessageStore store.MessageStore -} - -func NewSendInstantMessageHandler(apiApp *app.App, str store.MessageEventResponseStore, cStore store.ContactStore, mStore store.MessageStore) *SendInstantMessageHandler { - return &SendInstantMessageHandler{App: apiApp, MERStore: str, ContactStore: cStore, MessageStore: mStore} -} - -type SendInstantMessageRequest struct { - ContactIds []uuid.UUID `json:"contact_ids"` - MessageId uuid.UUID `json:"message_id"` - UserId uuid.UUID `json:"user_id"` -} - -type SendInstantMessageResponse struct { - Message string `json:"message"` - Data []*event.MessageEventResponse `json:"data"` -} - -// Sent godoc -// @Summary Send instant message -// @Description Send a message without the constraints of scheduled messages (requires JWT) -// @Tags messages -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body SendInstantMessageRequest true "Send an instnt message" -// @Success 201 {object} SendInstantMessageResponse -// @Failure 400 {object} SendInstantMessageResponse -// @Failure 500 {object} SendInstantMessageResponse -// @Router /instant/message [post] -func (s *SendInstantMessageHandler) Send(w http.ResponseWriter, r *http.Request) { - var req SendInstantMessageRequest - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, InvalidJson(err), http.StatusBadRequest) - } - defer r.Body.Close() - - var statusCode int - var resp SendInstantMessageResponse - - if req.UserId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "User Id is nil" - } else if req.MessageId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Message Id is nil" - } else if len(req.ContactIds) == 0 { - statusCode = http.StatusBadRequest - resp.Message = "No contacts provided" - } else { - if len(req.ContactIds) > Contact_Limit { - statusCode = http.StatusBadRequest - resp.Message = "Too many contacts" - } else { - ctx := r.Context() - if msg, contacts, err := s.verify(ctx, &req); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - if msg == nil { - statusCode = http.StatusBadRequest - resp.Message = "Message is empty" - } else if len(contacts) == 0 { - statusCode = http.StatusBadRequest - resp.Message = "Contacts not found" - } else { - if s.App != nil { - msgSender := send.MessageSender{Config: s.App.TwilioConfig} - for _, c := range contacts { - if twilioResp, twilioRespObject, err := msgSender.Send(*msg, *c, nil); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - dateStr := strings.TrimSpace(*twilioResp.DateUpdated) - if parsedTime, err := time.Parse(time.RFC1123Z, dateStr); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - mer := event.MessageEventResponse{} - mer.Response = twilioRespObject - mer.UserId = req.UserId - mer.Sent = parsedTime - mer.Status = event.Message_Event_Response_Status_Instant - mer.ContactId = c.Id - mer.MessageId = msg.Id - if err := s.MERStore.Create(ctx, &mer); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, &mer) - } - } - } - } - } else { - statusCode = http.StatusInternalServerError - resp.Message = "Config not properly configured" - } - } - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -func (s *SendInstantMessageHandler) verify(ctx context.Context, req *SendInstantMessageRequest) (*message.Message, []*contact.Contact, error) { - if msg, err := s.verifyMessage(ctx, req.MessageId); err != nil { - log.Println("Huh?") - return nil, nil, fmt.Errorf("Error:%v", err) - } else { - log.Println("Okay") - if msg != nil { - if contacts, err := s.verifyContacts(ctx, req.ContactIds); err != nil { - return nil, nil, fmt.Errorf("Error:%v", err) - } else { - if len(contacts) == 0 { - return msg, nil, nil - } else { - return msg, contacts, nil - } - } - } else { - log.Println("How can this be?") - return nil, nil, fmt.Errorf("Could not verify message") - } - } -} - -func (s *SendInstantMessageHandler) verifyMessage(ctx context.Context, messageId uuid.UUID) (*message.Message, error) { - if msg, err := s.MessageStore.GetMessageByID(ctx, messageId); err != nil { - return nil, fmt.Errorf("Error getting message:%v", err) - } else { - if msg == nil { - log.Println("Message is nil") - return nil, nil - } else { - return msg, nil - } - } -} - -func (s *SendInstantMessageHandler) verifyContacts(ctx context.Context, contactIds []uuid.UUID) ([]*contact.Contact, error) { - var contacts []*contact.Contact - - for _, contactId := range contactIds { - if c, err := s.ContactStore.GetContactByID(ctx, contactId); err != nil { - return nil, fmt.Errorf("Error getting contacts:%v", err) - } else { - if c != nil { - contacts = append(contacts, c) - } - } - } - - if len(contacts) == 0 { - return nil, nil - } else { - return contacts, nil - } -} diff --git a/internal/handler/message.go b/internal/handler/message.go deleted file mode 100644 index 0d2cd06..0000000 --- a/internal/handler/message.go +++ /dev/null @@ -1,180 +0,0 @@ -package handler - -import ( - "fmt" - "log" - "net/http" - - "git.kundeng.us/phoenix/textsender-models/tx0/message" - "github.com/google/uuid" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -type MessageHandler struct { - App *app.App - MessageStore store.MessageStore -} - -func NewMessageHandler(apiApp *app.App, str store.MessageStore) *MessageHandler { - return &MessageHandler{App: apiApp, MessageStore: str} -} - -const Message_Limit = 200 - -type RequestAddMessage struct { - Content string `json:"content"` - UserId uuid.UUID `json:"user_id"` -} - -type AddMessageResponse struct { - Message string `json:"message"` - Data []message.Message `json:"data"` -} - -// AddMessage godoc -// @Summary Add message -// @Description Add a message record to send a text to (requires JWT) -// @Tags messages -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body RequestAddMessage true "Data to add message" -// @Success 201 {object} AddMessageResponse -// @Failure 400 {object} AddMessageResponse -// @Failure 500 {object} AddMessageResponse -// @Router /message/new [post] -func (m *MessageHandler) AddMessage(w http.ResponseWriter, r *http.Request) { - var req RequestAddMessage - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - } - defer r.Body.Close() - - var statusCode int - var resp AddMessageResponse - newMessage := message.Message{Content: req.Content, UserId: &req.UserId} - - if isMessageValid(&newMessage) { - ctx := r.Context() - - if err := m.MessageStore.CreateMessage(ctx, &newMessage); err != nil { - fmt.Println("Error:", err) - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - statusCode = http.StatusCreated - resp.Message = "Message created" - resp.Data = append(resp.Data, newMessage) - } - } else { - statusCode = http.StatusBadRequest - resp.Message = fmt.Sprintf("Message length exceeds maximum limit. Current size: %d Limit: %d", len(newMessage.Content), Message_Limit) - } - - RespondWithJSON(w, statusCode, &resp) -} - -func isMessageValid(msg *message.Message) bool { - if len(msg.Content) <= Message_Limit { - return true - } else { - return false - } -} - -type GetMessageResponse struct { - Message string `json:"message"` - Data []message.Message `json:"data"` -} - -// GetMessage godoc -// @Summary Get message -// @Description Get a message record to have a recipient to send a text to (requires JWT) -// @Tags messages -// @Accept json -// @Produce json -// @Param id path string true "Message Id" -// @Param user_id path string true "User Id" -// @Security BearerAuth -// @Success 200 {object} GetMessageResponse -// @Failure 400 {object} GetMessageResponse -// @Failure 500 {object} GetMessageResponse -// @Router /message [get] -func (c *MessageHandler) GetMessage(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var id, userId uuid.UUID - queryParams := r.URL.Query() - - // Check if parameter exists - if _, exists := queryParams["id"]; !exists { - if _, exists := queryParams["user_id"]; !exists { - fmt.Fprintf(w, "Name parameter not provided") - http.Error(w, "Query params", http.StatusBadRequest) - return - } else { - var err error - userId, err = uuid.Parse(queryParams.Get("user_id")) - if err != nil { - fmt.Fprintf(w, "Name parameter exists: %s", userId) - } - } - } else { - var err error - idTmp := queryParams.Get("id") - id, err = uuid.Parse(idTmp) - if err != nil { - fmt.Println("Error:", err) - http.Error(w, "Error parsing Id", http.StatusBadRequest) - return - } - } - - var statusCode int - var resp GetMessageResponse - - ctx := r.Context() - - if id != uuid.Nil { - log.Println("Checking with Id") - if con, err := c.MessageStore.GetMessageByID(ctx, id); err == nil { - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, *con) - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } else if userId != uuid.Nil { - log.Println("Checking with User Id") - if messages, err := c.MessageStore.GetAllMessages(ctx); err == nil { - log.Println("Amount of messages:", len(messages)) - for _, msg := range messages { - if *msg.UserId == userId { - resp.Data = append(resp.Data, *msg) - } - } - - if len(resp.Data) > 0 { - statusCode = http.StatusOK - resp.Message = "Successful" - } else { - statusCode = http.StatusNotFound - resp.Message = "Message not found" - } - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } else { - statusCode = http.StatusBadRequest - resp.Message = "Invalid query parameter" - } - - RespondWithJSON(w, statusCode, &resp) -} diff --git a/internal/handler/message_event_response.go b/internal/handler/message_event_response.go deleted file mode 100644 index 7dc00ac..0000000 --- a/internal/handler/message_event_response.go +++ /dev/null @@ -1,174 +0,0 @@ -package handler - -import ( - "log" - "net/http" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/event" - "github.com/google/uuid" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -type EventResponseHandler struct { - App *app.App - MessageEventResponseStore store.MessageEventResponseStore -} - -func NewEventResponseHandler(apiApp *app.App, str store.MessageEventResponseStore) *EventResponseHandler { - return &EventResponseHandler{App: apiApp, MessageEventResponseStore: str} -} - -type RecordEventRequest struct { - ScheduledMessageEventId uuid.UUID `json:"scheduled_message_event_id"` - Response map[string]any `json:"response"` - UserId uuid.UUID `json:"user_id"` - Sent *time.Time `json:"sent,omitempty"` - Status string `json:"status"` - ContactId *uuid.UUID `json:"contact_id"` - MessageId *uuid.UUID `json:"message_id"` -} - -type RecordEventResponse struct { - Message string `json:"message"` - Data []*event.MessageEventResponse `json:"data"` -} - -// RecordMessageEventResponse godoc -// @Summary Record MessageEventResponse -// @Description Saves the result of sending a text message (requires JWT) -// @Tags message -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body RecordEventRequest true "Data to add MessageEventResponse" -// @Success 201 {object} RecordEventResponse -// @Failure 400 {object} RecordEventResponse -// @Failure 500 {object} RecordEventResponse -// @Router /schedule/message/event/response/record [post] -func (e *EventResponseHandler) RecordResponse(w http.ResponseWriter, r *http.Request) { - var req RecordEventRequest - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - } - defer r.Body.Close() - - var statusCode int - var rp event.MessageEventResponse - var resp RecordEventResponse - - if req.Sent == nil { - statusCode = http.StatusBadRequest - resp.Message = "Sent time is empty" - } else if req.UserId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "User Id is empty" - } else if len(req.Response) == 0 { - statusCode = http.StatusBadRequest - resp.Message = "Response is empty" - } else if req.ScheduledMessageEventId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Scheduled message event Id is empty" - } else { - log.Println("Starting process to record Message Event Response") - rp.ScheduledMessageEventId = req.ScheduledMessageEventId - rp.Response = req.Response - rp.UserId = req.UserId - rp.Sent = *req.Sent - - switch req.Status { - case event.Message_Event_Response_Status_Instant: - { - log.Println("Status:", req.Status) - rp.Status = req.Status - rp.MessageId = req.MessageId - rp.ContactId = req.ContactId - } - case event.Message_Event_Response_Status_Scheduled: - { - log.Println("Status:", req.Status) - rp.Status = req.Status - } - default: - { - statusCode = http.StatusBadRequest - resp.Message = "Status is invalid" - } - } - - ctx := r.Context() - if err := e.MessageEventResponseStore.Create(ctx, &rp); err != nil { - log.Println("Error:", err) - resp.Message = err.Error() - statusCode = http.StatusInternalServerError - } else { - log.Println("Created Message Event Response") - statusCode = http.StatusCreated - resp.Message = "Successful" - resp.Data = append(resp.Data, &rp) - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -type GetMessageEventResponseFetchedResponse struct { - Message string `json:"message"` - Data []*event.MessageEventResponse `json:"data"` -} - -// FetchtMessageEventResponse godoc -// @Summary Fetcht MessageEventResponse -// @Description Fetches a MessageEventResponse given a user id (requires JWT) -// @Tags message -// @Accept json -// @Produce json -// @Param user_id path string true "User Id" -// @Security BearerAuth -// @Success 200 {object} GetMessageEventResponseFetchedResponse -// @Failure 400 {object} GetMessageEventResponseFetchedResponse -// @Failure 500 {object} GetMessageEventResponseFetchedResponse -// @Router /schedule/message/event/response [get] -func (e *EventResponseHandler) Fetch(w http.ResponseWriter, r *http.Request) { - queryParams := r.URL.Query() - var userId uuid.UUID - - if _, exists := queryParams["user_id"]; !exists { - http.Error(w, "Query params", http.StatusBadRequest) - return - } else { - var err error - userId, err = uuid.Parse(queryParams.Get("user_id")) - if err != nil { - http.Error(w, "Query params", http.StatusBadRequest) - return - } - } - - var statusCode int - var resp GetMessageEventResponseFetchedResponse - - if userId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "User Id is nil" - } else { - ctx := r.Context() - if sentMessages, err := e.MessageEventResponseStore.GetWithUserId(ctx, userId); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - if len(sentMessages) == 0 { - statusCode = http.StatusNotFound - resp.Message = "Not MessageEventResponses found" - } else { - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = sentMessages - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} diff --git a/internal/handler/message_event_response_test.go b/internal/handler/message_event_response_test.go deleted file mode 100644 index 0792ffa..0000000 --- a/internal/handler/message_event_response_test.go +++ /dev/null @@ -1,162 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - evnt "git.kundeng.us/phoenix/textsender-models/tx0/message/event" - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - "git.kundeng.us/phoenix/textsender-api/internal/store/mock" -) - -func TestRecordMessageEventResponseWithMock(t *testing.T) { - now := time.Now() - - mockStore := mock.NewMockScheduledMessageEventStore() - contactStore := mock.NewMockContactStore() - messageStore := mock.NewMockMessageStore() - schMsgStore := mock.NewMockScheduledMessageStore() - merStore := mock.NewMockMessageEventResponseStore() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewEventResponseHandler(apiApp, merStore) - - contactId := uuid.New() - messageId := uuid.New() - scheduledMessageId := uuid.New() - testUserId := uuid.New() - - con := ContactTest(contactId, testUserId) - msg := MessageTest(messageId, testUserId) - schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now) - event := scheduling.ScheduledMessageEvent{} - - ctx := t.Context() - - if err := contactStore.CreateContact(ctx, &con); err != nil { - assert.NoError(t, err, "Error creating contact: %v", err) - } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { - assert.NoError(t, err, "Error creating message: %v", err) - } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error creating scheduled message: %v", err) - } - - event.MessageId = *msg.Id - event.ContactId = *con.Id - event.ScheduledMessageId = schMsg.Id - if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil { - assert.NoError(t, err, "Error creating scheduled message event: %v", err) - } - - sent := now.Add(30 * time.Minute) - var merResponse map[string]any - merResponse, err = ConvertStringToMapOfAny(MERResponseInString()) - assert.NoError(t, err, "Error Converting to map[string]any") - - testReq := RecordEventRequest{ScheduledMessageEventId: event.Id, Response: merResponse, UserId: schMsg.UserId, Sent: &sent, Status: evnt.Message_Event_Response_Status_Scheduled} - jsonValue, _ := json.Marshal(testReq) - - req, _ := http.NewRequest("POST", endpoint.RecordEventResponse, strings.NewReader(string(jsonValue))) - rr := httptest.NewRecorder() - - handler.RecordResponse(rr, req) - - assert.Equal(t, http.StatusCreated, rr.Code) - - var response RecordEventResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error parsing message event response:%v", err) - - assert.NotEmpty(t, response.Data, "No message event response created") - - var msgEventResp evnt.MessageEventResponse - msgEventResp = *response.Data[0] - - assert.NotEmpty(t, msgEventResp.Response, "The response of the message event response is empty") - assert.Equal(t, msgEventResp.Response, testReq.Response, "Responses do not match") -} - -func TestGetMessageEventResponse(t *testing.T) { - now := time.Now() - - mockStore := mock.NewMockScheduledMessageEventStore() - contactStore := mock.NewMockContactStore() - messageStore := mock.NewMockMessageStore() - schMsgStore := mock.NewMockScheduledMessageStore() - merStore := mock.NewMockMessageEventResponseStore() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewEventResponseHandler(apiApp, merStore) - - contactId := uuid.New() - messageId := uuid.New() - scheduledMessageId := uuid.New() - testUserId := uuid.New() - - con := ContactTest(contactId, testUserId) - msg := MessageTest(messageId, testUserId) - schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now) - - ctx := t.Context() - - if err := contactStore.CreateContact(ctx, &con); err != nil { - assert.NoError(t, err, "Error creating contact: %v", err) - } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { - assert.NoError(t, err, "Error creating message: %v", err) - } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error creating scheduled message: %v", err) - } - - event := scheduling.ScheduledMessageEvent{} - event.MessageId = *msg.Id - event.ContactId = *con.Id - event.ScheduledMessageId = schMsg.Id - sent := now.Add(30 * time.Minute) - if err := mockStore.CreateScheduledMessageEvent(ctx, &event); err != nil { - assert.NoError(t, err, "Error creating scheduled message event: %v", err) - } else { - m := evnt.MessageEventResponse{} - m.ScheduledMessageEventId = event.Id - m.UserId = schMsg.UserId - m.Status = evnt.Message_Event_Response_Status_Scheduled - m.Sent = sent - var merResponse map[string]any - merResponse, err = ConvertStringToMapOfAny(MERResponseInString()) - assert.NoError(t, err, "Error Converting to map[string]any") - m.Response = merResponse - - if err := merStore.Create(ctx, &m); err != nil { - assert.NoError(t, err, "Error creating MessageEventResponse: %v", err) - } - } - - url := fmt.Sprintf("%s?user_id=%s", endpoint.FetchMessageEventResponse, testUserId) - req, _ := http.NewRequest("GET", url, nil) - rr := httptest.NewRecorder() - - handler.Fetch(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response GetMessageEventResponseFetchedResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error parsing message event response: %v", err) - - assert.NotEmpty(t, response.Data, "No message event response found") - - var msgEventResp evnt.MessageEventResponse - msgEventResp = *response.Data[0] - - assert.NotEmpty(t, msgEventResp.Response, "The response of the message event response is empty") -} diff --git a/internal/handler/message_test.go b/internal/handler/message_test.go deleted file mode 100644 index 1781073..0000000 --- a/internal/handler/message_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "git.kundeng.us/phoenix/textsender-models/tx0/message" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - "git.kundeng.us/phoenix/textsender-api/internal/store/mock" -) - -func TestCreateMessageWithMock(t *testing.T) { - mockStore := mock.NewMockMessageStore() - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewMessageHandler(apiApp, mockStore) - - testUserId := uuid.New() - testBody := RequestAddMessage{Content: "Who could tell?", UserId: testUserId} - jsonValue, _ := json.Marshal(testBody) - - req, _ := http.NewRequest("POST", endpoint.ADD_MESSAGE, strings.NewReader(string(jsonValue))) - rr := httptest.NewRecorder() - - handler.AddMessage(rr, req) - - assert.Equal(t, http.StatusCreated, rr.Code) - - var response AddMessageResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error Creating message %v", err) - - assert.NotEmpty(t, response.Data, "No Message created") - - assert.NotNil(t, response.Data[0].Id, "Id should not be nil") -} - -func TestGetMessageWithMock(t *testing.T) { - mockstore := mock.NewMockMessageStore() - testUserId := uuid.New() - - testCon := message.Message{Content: "Who is the one that benefits?", UserId: &testUserId} - ctx := t.Context() - if err := mockstore.CreateMessage(ctx, &testCon); err != nil { - assert.NoError(t, err, "Error creating message") - return - } - - url := fmt.Sprintf("%s?user_id=%s", endpoint.GET_MESSAGE, testCon.UserId) - req, _ := http.NewRequest("GET", url, nil) - rr := httptest.NewRecorder() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - messageHandler := NewMessageHandler(apiApp, mockstore) - messageHandler.GetMessage(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response GetMessageResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error getting message %v", err) - - assert.NotEmpty(t, response.Data, "No Message retrieved") - - assert.NotNil(t, response.Data[0].Id, "Id should not be nil") -} diff --git a/internal/handler/schedule_message_status.go b/internal/handler/schedule_message_status.go deleted file mode 100644 index f935d75..0000000 --- a/internal/handler/schedule_message_status.go +++ /dev/null @@ -1,169 +0,0 @@ -package handler - -import ( - "net/http" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -type ScheduledMessageStatusHandler struct { - App *app.App - ScheduledMessageEventStore store.ScheduledMessageEventStore - ScheduledMessageStore store.ScheduledMessageStore -} - -func NewScheduledMessageStatusHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageStatusHandler { - return &ScheduledMessageStatusHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore} -} - -type RequestScheduledMessageStatus struct { - ScheduledMessageId uuid.UUID `json:"scheduled_message_id"` - Status string `json:"status"` -} - -type ScheduledMessageChange struct { - OldStatus string `json:"old_status"` - ScheduledMessage scheduling.ScheduledMessage `json:"scheduled_message"` -} - -type ScheduledMessageStatusResponse struct { - Message string `json:"message"` - Data []ScheduledMessageChange `json:"data"` -} - -// UpdateStatus godoc -// @Summary Update status -// @Description Update the status of a scheduled message (requires JWT) -// @Tags status -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body RequestScheduledMessageStatus true "Used to update scheduled message status" -// @Success 201 {object} ScheduledMessageStatusResponse -// @Failure 400 {object} ScheduledMessageStatusResponse -// @Failure 404 {object} ScheduledMessageStatusResponse -// @Failure 500 {object} ScheduledMessageStatusResponse -// @Router /schedule/message/status/update [patch] -func (s *ScheduledMessageStatusHandler) UpdateStatus(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPatch { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req RequestScheduledMessageStatus - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - } - defer r.Body.Close() - - var statusCode int - var resp ScheduledMessageStatusResponse - - if req.ScheduledMessageId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Scheduled messaged Id is nil" - } else if len(req.Status) == 0 { - statusCode = http.StatusBadRequest - resp.Message = "No status provided" - } else { - ctx := r.Context() - - if schMsg, err := s.ScheduledMessageStore.Get(ctx, req.ScheduledMessageId); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - if schMsg.Status == scheduling.Done { - statusCode = http.StatusNotFound - resp.Message = "Scheduled message has already completed" - } else { - switch req.Status { - case scheduling.Pending: - { - if schMsg.Status == scheduling.Processing { - statusCode = http.StatusBadRequest - resp.Message = "Message is currently processing" - } else { - if returnedStatus, err := s.ScheduledMessageStore.UpdateStatus(ctx, schMsg.Id, req.Status); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - chg := initScheduledMessageChange(schMsg, schMsg.Status, *returnedStatus) - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, chg) - } - } - } - case scheduling.Ready: - { - if events, err := s.ScheduledMessageEventStore.GetWithScheduleMessageId(ctx, schMsg.Id); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - oldStatus := schMsg.Status - if schMsg.Status != scheduling.Processing && schMsg.Status != scheduling.Ready { - // Update status - if len(events) > 0 { - if returnedStatus, err := s.ScheduledMessageStore.UpdateStatus(ctx, schMsg.Id, req.Status); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - chg := initScheduledMessageChange(schMsg, oldStatus, *returnedStatus) - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, chg) - } - } else { - statusCode = http.StatusInternalServerError - resp.Message = "Not enough scheduled message events" - } - } else { - if schMsg.Status == scheduling.Processing { - statusCode = http.StatusBadRequest - resp.Message = "Scheduled messages are processing" - } else { - statusCode = http.StatusNotModified - resp.Message = "Status is already set" - } - } - } - } - case scheduling.Processing, scheduling.Done: - { - if events, err := s.ScheduledMessageEventStore.GetWithScheduleMessageId(ctx, schMsg.Id); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - if len(events) > 0 { - if returnedStatus, err := s.ScheduledMessageStore.UpdateStatus(ctx, schMsg.Id, req.Status); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - chg := initScheduledMessageChange(schMsg, schMsg.Status, *returnedStatus) - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, chg) - } - } else { - statusCode = http.StatusInternalServerError - resp.Message = "Not enough scheduled message events" - } - } - } - } - } - } - - } - - RespondWithJSON(w, statusCode, &resp) -} - -func initScheduledMessageChange(schMsg *scheduling.ScheduledMessage, oldStatus, newStatus string) ScheduledMessageChange { - schMsg.Status = newStatus - return ScheduledMessageChange{OldStatus: oldStatus, ScheduledMessage: *schMsg} -} diff --git a/internal/handler/schedule_message_status_test.go b/internal/handler/schedule_message_status_test.go deleted file mode 100644 index d868620..0000000 --- a/internal/handler/schedule_message_status_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package handler - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - "git.kundeng.us/phoenix/textsender-api/internal/store/mock" -) - -func TestUpdateScheduledMessageStatusWithMock(t *testing.T) { - now := time.Now() - - schMsgEventStore := mock.NewMockScheduledMessageEventStore() - contactStore := mock.NewMockContactStore() - messageStore := mock.NewMockMessageStore() - schMsgStore := mock.NewMockScheduledMessageStore() - - contactId := uuid.New() - messageId := uuid.New() - scheduledMessageId := uuid.New() - testUserId := uuid.New() - - con := ContactTest(contactId, testUserId) - msg := MessageTest(messageId, testUserId) - schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now) - event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id) - - ctx := t.Context() - - if err := contactStore.CreateContact(ctx, &con); err != nil { - assert.NoError(t, err, "Error creating contact: %v", err) - } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { - assert.NoError(t, err, "Error creating message: %v", err) - } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error creating scheduled message: %v", err) - } else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil { - assert.NoError(t, err, "Error creating scheduled message event: %v", err) - } - - testReq := RequestScheduledMessageStatus{} - testReq.Status = scheduling.Ready - testReq.ScheduledMessageId = schMsg.Id - jsonValue, _ := json.Marshal(testReq) - jsonBody := strings.NewReader(string(jsonValue)) - - req, _ := http.NewRequest("PATCH", endpoint.UpdateScheduledMessageStatusEndpoint, jsonBody) - rr := httptest.NewRecorder() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewScheduledMessageStatusHandler(apiApp, schMsgEventStore, schMsgStore) - handler.UpdateStatus(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response ScheduledMessageStatusResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error creating event %v", err) - - assert.NotEmpty(t, response.Data, "Data should not be empty") - - changes := response.Data[0] - assert.NotEqual(t, changes.OldStatus, changes.ScheduledMessage.Status, "The status should not match") - assert.Equal(t, scheduling.Pending, changes.OldStatus, "The Old status does not match Old %s New %s", changes.OldStatus, scheduling.Pending) - assert.Equal(t, scheduling.Ready, changes.ScheduledMessage.Status, "Status has not been updated") -} diff --git a/internal/handler/scheduled_message.go b/internal/handler/scheduled_message.go deleted file mode 100644 index 8af8ee2..0000000 --- a/internal/handler/scheduled_message.go +++ /dev/null @@ -1,242 +0,0 @@ -package handler - -import ( - "fmt" - "net/http" - "strings" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -type ScheduledMessageHandler struct { - App *app.App - ScheduledMessageStore store.ScheduledMessageStore -} - -func NewScheduledMessageHandler(apiApp *app.App, str store.ScheduledMessageStore) *ScheduledMessageHandler { - return &ScheduledMessageHandler{App: apiApp, ScheduledMessageStore: str} -} - -type RequestAddScheduledMessage struct { - Scheduled time.Time `json:"scheduled"` - Status string `json:"status"` - UserId uuid.UUID `json:"user_id"` -} - -type AddScheduledMessageResponse struct { - Message string `json:"message"` - Data []scheduling.ScheduledMessage `json:"data"` -} - -// AddScheduledMessage godoc -// @Summary Add scheduled message -// @Description Adds a scheduled message (requires JWT) -// @Tags scheduled messages -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body RequestAddScheduledMessage true "Data to add scheduled message" -// @Success 201 {object} AddScheduledMessageResponse -// @Failure 400 {object} AddScheduledMessageResponse -// @Failure 500 {object} AddScheduledMessageResponse -// @Router /schedule/message [post] -func (s *ScheduledMessageHandler) AddScheduledMessage(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req RequestAddScheduledMessage - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - } - defer r.Body.Close() - - scheduledMessage := scheduling.ScheduledMessage{Scheduled: req.Scheduled, Status: req.Status, UserId: req.UserId} - - var statusCode int - var resp AddScheduledMessageResponse - - if scheduledMessage.UserId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "No UserId" - } else { - ctx := r.Context() - - if validStatus, err := IsStatusValid(&scheduledMessage); err == nil { - if validStatus { - if valid, err := isScheduledTimeValid(&scheduledMessage); err == nil && valid { - if err = s.ScheduledMessageStore.CreateScheduledMessage(ctx, &scheduledMessage); err == nil { - statusCode = http.StatusCreated - resp.Data = append(resp.Data, scheduledMessage) - resp.Message = "Successful" - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } else { - statusCode = http.StatusBadRequest - resp.Message = "Invalid scheduled time" - } - } else { - statusCode = http.StatusBadRequest - resp.Message = "Invalid status" - } - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -type GetScheduledMessageResponse struct { - Message string `json:"message"` - Data []scheduling.ScheduledMessage `json:"data"` -} - -// GetScheduledMessage godoc -// @Summary Get scheduled message -// @Description Get a scheduled message (requires JWT) -// @Tags scheduled messages -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path string true "Contact Id" -// @Param user_id path string true "User Id" -// @Success 200 {object} GetScheduledMessageResponse -// @Failure 400 {object} GetScheduledMessageResponse -// @Failure 500 {object} GetScheduledMessageResponse -// @Router /schedule/message [get] -func (s *ScheduledMessageHandler) GetScheduledMessage(w http.ResponseWriter, r *http.Request) { - var id, userId uuid.UUID - - if idParam, err := ParseQueryParams(r, "id"); err == nil { - var err error - id, err = uuid.Parse(*idParam) - if err != nil { - http.Error(w, "Error parsing Id", http.StatusBadRequest) - return - } - } - if userIdParam, err := ParseQueryParams(r, "user_id"); err == nil { - userId, err = uuid.Parse(*userIdParam) - if err != nil { - http.Error(w, "Error parsing Id", http.StatusBadRequest) - return - } - } - - var resp GetScheduledMessageResponse - var statusCode int - ctx := r.Context() - - if userId == uuid.Nil && id == uuid.Nil { - http.Error(w, "Invalid query parameters", http.StatusBadRequest) - return - } else if userId != uuid.Nil && id != uuid.Nil { - if schMsgs, err := s.ScheduledMessageStore.GetWithUserId(ctx, userId); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - for _, schMsg := range schMsgs { - if schMsg.Id == id { - resp.Data = append(resp.Data, *schMsg) - } - } - statusCode = http.StatusOK - resp.Message = "Successful" - } - } else if id != uuid.Nil { - if schMsg, err := s.ScheduledMessageStore.Get(ctx, id); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, *schMsg) - } - } else { - if schMsgs, err := s.ScheduledMessageStore.GetWithUserId(ctx, userId); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - statusCode = http.StatusOK - resp.Message = "Successful" - for _, schMsg := range schMsgs { - resp.Data = append(resp.Data, *schMsg) - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -type FetchNextMessageResponse struct { - Message string `json:"message"` - Data []scheduling.ScheduledMessage `json:"data"` -} - -// FetchNextMessage godoc -// @Summary Fetch scheduled message -// @Description Fetches a scheduled message that is available to be processed (requires JWT) -// @Tags scheduled messages -// @Accept json -// @Produce json -// @Security BearerAuth -// @Success 200 {object} FetchNextMessageResponse -// @Failure 400 {object} FetchNextMessageResponse -// @Failure 500 {object} FetchNextMessageResponse -// @Router /schedule/message/fetch [get] -func (s *ScheduledMessageHandler) FetchNextMessage(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - var resp FetchNextMessageResponse - var statusCode int - - if schMsg, err := s.ScheduledMessageStore.FetchNextScheduledMessage(ctx); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - if schMsg != nil { - statusCode = http.StatusOK - resp.Message = "Successful" - resp.Data = append(resp.Data, *schMsg) - } else { - statusCode = http.StatusNotFound - resp.Message = "No item" - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -func isScheduledTimeValid(schMsg *scheduling.ScheduledMessage) (bool, error) { - now := time.Now() - timeCutOff := now.Add(-5 * time.Minute) - futureCutOff := now.Add((24 * 7) * time.Hour) - - if schMsg.Scheduled.After(timeCutOff) { - if schMsg.Scheduled.Before(futureCutOff) { - return true, nil - } else { - return false, fmt.Errorf("Scheduled Time is too far in the future") - } - } else { - return false, nil - } -} - -func IsStatusValid(schMsg *scheduling.ScheduledMessage) (bool, error) { - schMsg.Status = strings.ToUpper(schMsg.Status) - if schMsg.Status == scheduling.Pending { - return true, nil - } else { - return false, fmt.Errorf("Status is not valid for creating scheduled message %s", schMsg.Status) - } -} diff --git a/internal/handler/scheduled_message_event.go b/internal/handler/scheduled_message_event.go deleted file mode 100644 index 9c4e17e..0000000 --- a/internal/handler/scheduled_message_event.go +++ /dev/null @@ -1,247 +0,0 @@ -package handler - -import ( - "net/http" - "strings" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/go-chi/chi/v5" - "github.com/google/uuid" - - "git.kundeng.us/phoenix/textsender-api/internal/app" - "git.kundeng.us/phoenix/textsender-api/internal/store" -) - -type ScheduledMessageEventHandler struct { - App *app.App - ScheduledMessageEventStore store.ScheduledMessageEventStore - ScheduledMessageStore store.ScheduledMessageStore -} - -func NewScheduledMessageEventHandler(apiApp *app.App, str store.ScheduledMessageEventStore, schStore store.ScheduledMessageStore) *ScheduledMessageEventHandler { - return &ScheduledMessageEventHandler{App: apiApp, ScheduledMessageEventStore: str, ScheduledMessageStore: schStore} -} - -type RequestAddScheduledMessageEvent struct { - ContactId uuid.UUID `json:"contact_id"` - MessageId uuid.UUID `json:"message_id"` - ScheduledMessageId uuid.UUID `json:"scheduled_message_id"` -} - -type AddScheduledMessageEventResponse struct { - Message string `json:"message"` - Data []scheduling.ScheduledMessageEvent `json:"data"` -} - -// AddScheduledMessageEvent godoc -// @Summary Add scheduled message event -// @Description Add a scheduled message event (requires JWT) -// @Tags scheduled message events -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param request body RequestAddScheduledMessageEvent true "Data to add scheduled message event" -// @Success 201 {object} AddScheduledMessageEventResponse -// @Failure 400 {object} AddScheduledMessageEventResponse -// @Failure 500 {object} AddScheduledMessageEventResponse -// @Router /schedule/message/event [post] -func (s *ScheduledMessageEventHandler) AddScheduledMessageEvent(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - return - } - - var req RequestAddScheduledMessageEvent - if err := ExtractFromRequest(r, &req); err != nil { - http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) - } - defer r.Body.Close() - - event := scheduling.ScheduledMessageEvent{ContactId: req.ContactId, MessageId: req.MessageId, ScheduledMessageId: req.ScheduledMessageId} - - var statusCode int - var resp AddScheduledMessageEventResponse - - if event.ContactId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Contact Id is nil" - } else if event.MessageId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Message Id is nil" - } else if event.ScheduledMessageId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Scheduled Message Id is nil" - } else { - ctx := r.Context() - - if schMsg, err := s.ScheduledMessageStore.Get(ctx, event.ScheduledMessageId); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - if schMsg.Status == scheduling.Pending { - if exists, err := s.ScheduledMessageEventStore.Exists(ctx, &event); err == nil { - if exists { - statusCode = http.StatusBadRequest - resp.Message = "Event already present" - } else { - if err = s.ScheduledMessageEventStore.CreateScheduledMessageEvent(ctx, &event); err == nil { - statusCode = http.StatusCreated - resp.Message = "Successful" - resp.Data = append(resp.Data, event) - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } - } else { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } - } else { - statusCode = http.StatusBadRequest - resp.Message = "Scheduled message cannot be modified due to the status" - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -type GetScheduledMessageEventResponse struct { - Message string `json:"message"` - Data []scheduling.ScheduledMessageEvent `json:"data"` -} - -// GetScheduledMessageEvent godoc -// @Summary Get scheduled message event -// @Description Gets a scheduled message event (requires JWT) -// @Tags scheduled message events -// @Accept json -// @Produce json -// @Param id path string true "Contact Id" -// @Param scheduled_message_id path string true "Scheduled Message Id" -// @Security BearerAuth -// @Success 200 {object} GetScheduledMessageEventResponse -// @Failure 400 {object} GetScheduledMessageEventResponse -// @Failure 500 {object} GetScheduledMessageEventResponse -// @Router /schedule/message/event [get] -func (s *ScheduledMessageEventHandler) GetScheduledMessageEvent(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Metnot allowed", http.StatusMethodNotAllowed) - return - } - - var id, scheduledMessageId uuid.UUID - if idParam, err := ParseQueryParams(r, "id"); err == nil { - id, err = uuid.Parse(*idParam) - } - if scheduledMessageIdParam, err := ParseQueryParams(r, "scheduled_message_id"); err == nil { - scheduledMessageId, err = uuid.Parse(*scheduledMessageIdParam) - } - - var statusCode int - var resp GetScheduledMessageEventResponse - ctx := r.Context() - - if id == uuid.Nil && scheduledMessageId == uuid.Nil { - statusCode = http.StatusBadRequest - resp.Message = "Query parameters missing" - } else if id != uuid.Nil { - if event, err := s.ScheduledMessageEventStore.Get(ctx, id); err != nil { - resp.Message = err.Error() - statusCode = http.StatusInternalServerError - } else { - if event != nil { - resp.Message = "Successful" - statusCode = http.StatusOK - resp.Data = append(resp.Data, *event) - } else { - statusCode = http.StatusNotFound - resp.Message = "Scheduled message event not found" - } - } - } else { - if events, err := s.ScheduledMessageEventStore.GetWithScheduleMessageId(ctx, scheduledMessageId); err != nil { - statusCode = http.StatusInternalServerError - resp.Message = err.Error() - } else { - statusCode = http.StatusOK - resp.Message = "Successful" - for _, event := range events { - resp.Data = append(resp.Data, *event) - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} - -type DeleteScheduledMessageEventResponse struct { - Message string `json:"message"` - Data []scheduling.ScheduledMessageEvent `json:"data"` -} - -// DeleteScheduledMessageEvent godoc -// @Summary Delete scheduled message event -// @Description Deletes a scheduled message event (requires JWT) -// @Tags scheduled message events -// @Accept json -// @Produce json -// @Security BearerAuth -// @Param id path string true "scheduled message event Id" Format(uuid) -// @Success 201 {object} DeleteScheduledMessageEventResponse -// @Failure 400 {object} DeleteScheduledMessageEventResponse -// @Failure 500 {object} DeleteScheduledMessageEventResponse -// @Router /schedule/message/event/{id} [delete] -func (s *ScheduledMessageEventHandler) DeleteScheduledMessageEvent(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodDelete { - http.Error(w, "Metnot allowed", http.StatusMethodNotAllowed) - return - } - - id := chi.URLParam(r, "id") - if len(id) == 0 { - pathParts := strings.Split(r.URL.Path, "/") - if len(pathParts) < 7 { - http.Error(w, "Id not provided", http.StatusBadRequest) - return - } else { - id = pathParts[6] - } - } - - var resp DeleteScheduledMessageEventResponse - var statusCode int - - if parsedId, err := uuid.Parse(id); err != nil { - resp.Message = err.Error() - statusCode = http.StatusBadRequest - } else { - ctx := r.Context() - if event, err := s.ScheduledMessageEventStore.Get(ctx, parsedId); err != nil { - resp.Message = err.Error() - statusCode = http.StatusInternalServerError - } else { - if schMsg, err := s.ScheduledMessageStore.Get(ctx, event.ScheduledMessageId); err != nil { - resp.Message = err.Error() - statusCode = http.StatusInternalServerError - } else { - if schMsg.Status == scheduling.Pending { - if err = s.ScheduledMessageEventStore.Delete(ctx, event.Id); err != nil { - resp.Message = err.Error() - statusCode = http.StatusInternalServerError - } else { - resp.Message = "Successful" - resp.Data = append(resp.Data, *event) - statusCode = http.StatusOK - } - } else { - statusCode = http.StatusBadRequest - resp.Message = "Invalid status" - } - } - } - } - - RespondWithJSON(w, statusCode, &resp) -} diff --git a/internal/handler/scheduled_message_event_test.go b/internal/handler/scheduled_message_event_test.go deleted file mode 100644 index 2889cc8..0000000 --- a/internal/handler/scheduled_message_event_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - "git.kundeng.us/phoenix/textsender-api/internal/store/mock" -) - -var ( - contactId = uuid.New() - messageId = uuid.New() - scheduledMessageId = uuid.New() - testUserId = uuid.New() -) - -type CreateScheduledMessageEventRequest struct { - ContactId uuid.UUID `json:"contact_id"` - MessageId uuid.UUID `json:"message_id"` - ScheduledMessageId uuid.UUID `json:"scheduled_message_id"` -} - -func TestCreateScheduledMessageEventWithMock(t *testing.T) { - now := time.Now() - - mockStore := mock.NewMockScheduledMessageEventStore() - contactStore := mock.NewMockContactStore() - messageStore := mock.NewMockMessageStore() - schMsgStore := mock.NewMockScheduledMessageStore() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewScheduledMessageEventHandler(apiApp, mockStore, schMsgStore) - - contactId := uuid.New() - messageId := uuid.New() - scheduledMessageId := uuid.New() - testUserId := uuid.New() - - con := ContactTest(contactId, testUserId) - msg := MessageTest(messageId, testUserId) - schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now) - - ctx := t.Context() - - if err := contactStore.CreateContact(ctx, &con); err != nil { - assert.NoError(t, err, "Error creating contact: %v", err) - } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { - assert.NoError(t, err, "Error creating message: %v", err) - } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error creating scheduled message: %v", err) - } - - testReq := CreateScheduledMessageEventRequest{ContactId: contactId, MessageId: messageId, ScheduledMessageId: scheduledMessageId} - jsonValue, _ := json.Marshal(testReq) - - req, _ := http.NewRequest("POST", endpoint.AddEventToScheduledMessageEndpoint, strings.NewReader(string(jsonValue))) - rr := httptest.NewRecorder() - - handler.AddScheduledMessageEvent(rr, req) - - assert.Equal(t, http.StatusCreated, rr.Code) - - var response AddScheduledMessageEventResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error creating event %v", err) - - assert.NotEmpty(t, response.Data, "No event created") - - var event scheduling.ScheduledMessageEvent - event = response.Data[0] - - assert.NotEmpty(t, event.Created, "Created date should not be empty") -} - -func TestGetScheduledMessageEventWithMock(t *testing.T) { - now := time.Now() - - contactStore := mock.NewMockContactStore() - messageStore := mock.NewMockMessageStore() - schMsgStore := mock.NewMockScheduledMessageStore() - schMsgEventStore := mock.NewMockScheduledMessageEventStore() - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore) - - contactId := uuid.New() - messageId := uuid.New() - scheduledMessageId := uuid.New() - testUserId := uuid.New() - - con := ContactTest(contactId, testUserId) - msg := MessageTest(messageId, testUserId) - schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now) - event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id) - - ctx := t.Context() - - if err := contactStore.CreateContact(ctx, &con); err != nil { - assert.NoError(t, err, "Error creating contact: %v", err) - } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { - assert.NoError(t, err, "Error creating message: %v", err) - } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error creating scheduled message: %v", err) - } else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil { - assert.NoError(t, err, "Error creating scheduled message event: %v", err) - } - - url := fmt.Sprintf("%s?id=%s", endpoint.GetScheduledMessageEventEndpoint, event.Id.String()) - req, _ := http.NewRequest("GET", url, nil) - rr := httptest.NewRecorder() - - handler.GetScheduledMessageEvent(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response GetScheduledMessageEventResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error creating event %v", err) - - assert.NotEmpty(t, response.Data, "No event created") - - msgEvent := response.Data[0] - - assert.NotEmpty(t, msgEvent.Created, "Created date should not be empty") -} - -func TestDeleteScheduledMessageEventWithMock(t *testing.T) { - now := time.Now() - contactStore := mock.NewMockContactStore() - messageStore := mock.NewMockMessageStore() - schMsgStore := mock.NewMockScheduledMessageStore() - schMsgEventStore := mock.NewMockScheduledMessageEventStore() - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewScheduledMessageEventHandler(apiApp, schMsgEventStore, schMsgStore) - - con := ContactTest(contactId, testUserId) - msg := MessageTest(messageId, testUserId) - schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now) - event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id) - - ctx := t.Context() - - if err := contactStore.CreateContact(ctx, &con); err != nil { - assert.NoError(t, err, "Error creating contact: %v", err) - } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { - assert.NoError(t, err, "Error creating message: %v", err) - } else if err = schMsgStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error creating scheduled message: %v", err) - } else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil { - assert.NoError(t, err, "Error creating scheduled message event: %v", err) - } - - endpointValue := strings.Replace(endpoint.DeleteScheduledMessageEventEndpoint, "{id}", event.Id.String(), 1) - req, _ := http.NewRequest("DELETE", endpointValue, nil) - rr := httptest.NewRecorder() - - handler.DeleteScheduledMessageEvent(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response DeleteScheduledMessageEventResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error creating event %v", err) - - assert.NotEmpty(t, response.Data, "No event created") - - msgEvent := response.Data[0] - - assert.NotEmpty(t, msgEvent.Created, "Created date should not be empty") -} diff --git a/internal/handler/scheduled_message_test.go b/internal/handler/scheduled_message_test.go deleted file mode 100644 index 95a4936..0000000 --- a/internal/handler/scheduled_message_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package handler - -import ( - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - - "git.kundeng.us/phoenix/textsender-api/internal/handler/endpoint" - "git.kundeng.us/phoenix/textsender-api/internal/store/mock" -) - -type CreateScheduledMessageRequest struct { - Scheduled time.Time `json:"scheduled"` - Status string `json:"status"` - UserId uuid.UUID `json:"user_id"` -} - -func TestCreateScheduledMessageWithMock(t *testing.T) { - now := time.Now() - mockStore := mock.NewMockScheduledMessageStore() - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewScheduledMessageHandler(apiApp, mockStore) - - testUserId := uuid.New() - testBody := createScheduledMessageRequest(testUserId, now) - jsonValue, _ := json.Marshal(testBody) - - req, _ := http.NewRequest("POST", endpoint.ScheduleMessageEndpoint, strings.NewReader(string(jsonValue))) - rr := httptest.NewRecorder() - - handler.AddScheduledMessage(rr, req) - - assert.Equal(t, http.StatusCreated, rr.Code) - - var response AddScheduledMessageResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error Creating message %v", err) - - assert.NotEmpty(t, response.Data, "No Message created") - - assert.NotNil(t, response.Data[0].Id, "Id should not be nil") -} - -func TestGetScheduledMessageWithMock(t *testing.T) { - now := time.Now() - mockStore := mock.NewMockScheduledMessageStore() - - testUserId := uuid.New() - testBody := createScheduledMessageRequest(testUserId, now) - - ctx := t.Context() - schMsg := scheduling.ScheduledMessage{} - schMsg.Scheduled = testBody.Scheduled - schMsg.Status = testBody.Status - schMsg.UserId = testBody.UserId - if err := mockStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error Creating message %v", err) - } - - url := fmt.Sprintf("%s?id=%s&user_id=%s", endpoint.GetScheduledMessageEndpoint, schMsg.Id, schMsg.UserId) - req, _ := http.NewRequest("GET", url, nil) - rr := httptest.NewRecorder() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewScheduledMessageHandler(apiApp, mockStore) - handler.GetScheduledMessage(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response GetScheduledMessageResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error parsing response %v", err) -} - -func TestFetchScheduledMessageWithMock(t *testing.T) { - now := time.Now() - mockStore := mock.NewMockScheduledMessageStore() - contactStore := mock.NewMockContactStore() - messageStore := mock.NewMockMessageStore() - schMsgEventStore := mock.NewMockScheduledMessageEventStore() - - contactId := uuid.New() - messageId := uuid.New() - scheduledMessageId := uuid.New() - testUserId := uuid.New() - - con := ContactTest(contactId, testUserId) - msg := MessageTest(messageId, testUserId) - schMsg := ScheduledMessageTest(scheduledMessageId, testUserId, now) - event := ScheduledMessageEventTest(*msg.Id, *con.Id, schMsg.Id) - - ctx := t.Context() - - if err := contactStore.CreateContact(ctx, &con); err != nil { - assert.NoError(t, err, "Error creating contact: %v", err) - } else if err = messageStore.CreateMessage(ctx, &msg); err != nil { - assert.NoError(t, err, "Error creating message: %v", err) - } else if err = mockStore.CreateScheduledMessage(ctx, &schMsg); err != nil { - assert.NoError(t, err, "Error creating scheduled message: %v", err) - } else if err = schMsgEventStore.CreateScheduledMessageEvent(ctx, &event); err != nil { - assert.NoError(t, err, "Error creating scheduled message event: %v", err) - } else if newStatus, err := mockStore.UpdateStatus(ctx, schMsg.Id, scheduling.Ready); err != nil { - assert.NoError(t, err, "Error updating status: %v", err) - } else { - assert.Equal(t, *newStatus, scheduling.Ready) - } - - req, _ := http.NewRequest("GET", endpoint.FetchNextScheduledMessageEndpoint, nil) - rr := httptest.NewRecorder() - - apiApp, err := GetApp() - assert.NoError(t, err, "Error getting app") - handler := NewScheduledMessageHandler(apiApp, mockStore) - handler.FetchNextMessage(rr, req) - - assert.Equal(t, http.StatusOK, rr.Code) - - var response FetchNextMessageResponse - err = json.Unmarshal(rr.Body.Bytes(), &response) - assert.NoError(t, err, "Error fetching scheduled message: %v", err) - - assert.NotEmpty(t, response.Data, "No data") - - fetchedSchMsg := response.Data[0] - assert.Equal(t, scheduling.Processing, fetchedSchMsg.Status, "The statuses do not match") -} - -func createScheduledMessageRequest(userId uuid.UUID, now time.Time) CreateScheduledMessageRequest { - scheduled := now.Add(5 * time.Minute) - return CreateScheduledMessageRequest{Status: scheduling.Pending, UserId: userId, Scheduled: scheduled} -} diff --git a/internal/middleware/auth.go b/internal/middleware/auth.go deleted file mode 100644 index 1cbceb1..0000000 --- a/internal/middleware/auth.go +++ /dev/null @@ -1,47 +0,0 @@ -package middleware - -import ( - "context" - "net/http" - "strings" - - "git.kundeng.us/phoenix/textsender-api/internal/services" -) - -type contextKey string - -const ( - UserContextKey contextKey = "user" -) - -func AuthMiddleware(authService *services.JWTService) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - authHeader := r.Header.Get("Authorization") - if authHeader == "" { - http.Error(w, "Authorization header required", http.StatusUnauthorized) - return - } - - // Extract token from "Bearer " - parts := strings.Split(authHeader, " ") - if len(parts) != 2 || parts[0] != "Bearer" { - http.Error(w, "Invalid authorization header format", http.StatusUnauthorized) - return - } - - token := parts[1] - - // Validate token with auth service - user, err := authService.ValidateToken(token) - if err != nil { - http.Error(w, "Invalid token", http.StatusUnauthorized) - return - } - - // Add user to context - ctx := context.WithValue(r.Context(), UserContextKey, user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} diff --git a/internal/middleware/middleware.go b/internal/middleware/middleware.go deleted file mode 100644 index 86739a5..0000000 --- a/internal/middleware/middleware.go +++ /dev/null @@ -1,22 +0,0 @@ -package middleware - -import ( - "log" - "net/http" - "time" -) - -func Logging(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - next.ServeHTTP(w, r) - log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start)) - }) -} - -func JSONContentType(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - next.ServeHTTP(w, r) - }) -} diff --git a/internal/model/messageDraft.go b/internal/model/messageDraft.go deleted file mode 100644 index 7366ed5..0000000 --- a/internal/model/messageDraft.go +++ /dev/null @@ -1,6 +0,0 @@ -package model - -type MessageItem struct { - Id int `json:"id"` - Message string `json:"message"` -} diff --git a/internal/services/jwt_service.go b/internal/services/jwt_service.go deleted file mode 100644 index ee38e90..0000000 --- a/internal/services/jwt_service.go +++ /dev/null @@ -1,49 +0,0 @@ -package services - -import ( - "time" - - txtmodels_token "git.kundeng.us/phoenix/textsender-models/tx0/token" - txtmodels_user "git.kundeng.us/phoenix/textsender-models/tx0/user" - "github.com/golang-jwt/jwt/v5" -) - -type JWTService struct { - secretKey []byte -} - -func NewJWTService(secretKey string) *JWTService { - return &JWTService{ - secretKey: []byte(secretKey), - } -} - -func (s *JWTService) ValidateToken(tokenString string) (*txtmodels_user.User, error) { - // TODO: Include more user information in the claims to populate user - claims := &txtmodels_token.Claims{} - - token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { - // Validate the signing method - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - return nil, jwt.ErrSignatureInvalid - } - return s.secretKey, nil - }) - - if err != nil { - return nil, err - } - - if !token.Valid { - return nil, jwt.ErrSignatureInvalid - } - - // Check token expiration - if time.Now().After(claims.ExpiresAt.Time) { - return nil, jwt.ErrTokenExpired - } - - return &txtmodels_user.User{ - Id: claims.UserId, - }, nil -} diff --git a/internal/store/contact_store.go b/internal/store/contact_store.go deleted file mode 100644 index bc67a10..0000000 --- a/internal/store/contact_store.go +++ /dev/null @@ -1,182 +0,0 @@ -package store - -import ( - "context" - "fmt" - - "git.kundeng.us/phoenix/textsender-models/tx0/contact" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" -) - -type ContactStore interface { - CreateContact(ctx context.Context, con *contact.Contact) error - GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) - GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error) - GetAllContacts(ctx context.Context) ([]*contact.Contact, error) - ContactExists(ctx context.Context, phone string, userId uuid.UUID) (bool, error) - UpdateNames(ctx context.Context, con *contact.Contact, firstname *string, lastname *string, nickname *string) (int64, error) -} - -type PGContactStore struct { - db *pgxpool.Pool -} - -func NewContactStore(db *pgxpool.Pool) *PGContactStore { - return &PGContactStore{db: db} -} - -func (s *PGContactStore) CreateContact(ctx context.Context, con *contact.Contact) error { - query := ` - INSERT INTO contacts (phone_number, user_id, first_name, last_name, nickname) - VALUES ($1, $2, $3, $4, $5) - RETURNING id, phone_number, user_id - ` - - return s.db.QueryRow(ctx, query, con.PhoneNumber, con.UserId, con.Firstname, con.Lastname, con.Nickname).Scan( - &con.Id, &con.PhoneNumber, &con.UserId, - ) -} - -func (s *PGContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) { - query := `SELECT id, phone_number, user_id, first_name, last_name, nickname FROM contacts WHERE id = $1` - - var con contact.Contact - err := s.db.QueryRow(ctx, query, id).Scan( - &con.Id, &con.PhoneNumber, &con.UserId, &con.Firstname, &con.Lastname, &con.Nickname, - ) - - if err == pgx.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, fmt.Errorf("getting contact by ID: %w", err) - } else { - return &con, nil - } -} - -func (s *PGContactStore) GetContactByPhone(ctx context.Context, phone string, userId uuid.UUID) (*contact.Contact, error) { - query := `SELECT id, phone_number, user_id, first_name, last_name, nickname FROM contacts WHERE phone_number = $1 AND user_id = $2` - - var con contact.Contact - err := s.db.QueryRow(ctx, query, phone, userId).Scan( - &con.Id, &con.PhoneNumber, &con.UserId, &con.Firstname, &con.Lastname, &con.Nickname, - ) - - if err == pgx.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, fmt.Errorf("getting contact by phone and User ID: %w", err) - } else { - return &con, nil - } -} - -func (s *PGContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) { - query := `SELECT id, phone_number, user_id, first_name, last_name, nickname FROM contacts` - - rows, err := s.db.Query(ctx, query) - if err != nil { - return nil, fmt.Errorf("querying all contacts: %w", err) - } - defer rows.Close() - - var cons []*contact.Contact - for rows.Next() { - var con contact.Contact - if err := rows.Scan( - &con.Id, &con.PhoneNumber, &con.UserId, &con.Firstname, &con.Lastname, &con.Nickname, - ); err != nil { - return nil, fmt.Errorf("scanning contact row: %w", err) - } - cons = append(cons, &con) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("iterating contact rows: %w", err) - } - - return cons, nil -} - -func (s *PGContactStore) ContactExists(ctx context.Context, phone string, userId uuid.UUID) (bool, error) { - query := `SELECT EXISTS(SELECT 1 FROM contacts WHERE phone_number = $1 AND user_id = $2)` - - var exists bool - err := s.db.QueryRow(ctx, query, phone, userId).Scan(&exists) - if err != nil { - return false, fmt.Errorf("checking if contact exists: %w", err) - } - - return exists, nil -} - -func (s *PGContactStore) UpdateNames(ctx context.Context, con *contact.Contact, firstname *string, lastname *string, nickname *string) (int64, error) { - if firstname == nil && lastname == nil && nickname == nil { - return 0, fmt.Errorf("Names are nil") - } else { - if firstname != nil && lastname != nil && nickname != nil { - query := "UPDATE contacts SET first_name = $1, last_name = $2, nickname = $3 WHERE id = $4" - if affected, err := s.db.Exec(ctx, query, firstname, lastname, nickname, con.Id); err != nil { - return 0, err - } else { - con.Firstname = firstname - con.Lastname = lastname - con.Nickname = nickname - return affected.RowsAffected(), nil - } - } else if firstname != nil && lastname != nil { - query := "UPDATE contacts SET first_name = $1, last_name = $2 WHERE id = $3" - if affected, err := s.db.Exec(ctx, query, firstname, lastname, con.Id); err != nil { - return 0, err - } else { - con.Firstname = firstname - con.Lastname = lastname - return affected.RowsAffected(), nil - } - } else if lastname != nil && nickname != nil { - query := "UPDATE contacts SET last_name = $1, nickname = $2 WHERE id = $3" - if affected, err := s.db.Exec(ctx, query, lastname, nickname, con.Id); err != nil { - return 0, err - } else { - con.Lastname = lastname - con.Nickname = nickname - return affected.RowsAffected(), nil - } - } else if nickname != nil && firstname != nil { - query := "UPDATE contacts SET nickname = $1, firstname = $2 WHERE id = $3" - if affected, err := s.db.Exec(ctx, query, nickname, firstname, con.Id); err != nil { - return 0, err - } else { - con.Firstname = firstname - con.Nickname = nickname - return affected.RowsAffected(), nil - } - } else if firstname != nil { - query := "UPDATE contacts SET firstname = $1 WHERE id = $2" - if affected, err := s.db.Exec(ctx, query, firstname, con.Id); err != nil { - return 0, err - } else { - con.Firstname = firstname - return affected.RowsAffected(), nil - } - } else if lastname != nil { - query := "UPDATE contacts SET lastname = $1 WHERE id = $2" - if affected, err := s.db.Exec(ctx, query, lastname, con.Id); err != nil { - return 0, err - } else { - con.Lastname = lastname - return affected.RowsAffected(), nil - } - } else { - query := "UPDATE contacts SET nickname = $1 WHERE id = $2" - if affected, err := s.db.Exec(ctx, query, nickname, con.Id); err != nil { - return 0, err - } else { - con.Nickname = nickname - return affected.RowsAffected(), nil - } - } - } -} diff --git a/internal/store/message_event_response_store.go b/internal/store/message_event_response_store.go deleted file mode 100644 index 4cf0a19..0000000 --- a/internal/store/message_event_response_store.go +++ /dev/null @@ -1,61 +0,0 @@ -package store - -import ( - "context" - "fmt" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/event" - "github.com/google/uuid" - "github.com/jackc/pgx/v5/pgxpool" -) - -type MessageEventResponseStore interface { - Create(ctx context.Context, mer *event.MessageEventResponse) error - GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*event.MessageEventResponse, error) -} - -type PGMessageEventResponseStore struct { - db *pgxpool.Pool -} - -func NewMessageEventResponseStore(db *pgxpool.Pool) *PGMessageEventResponseStore { - return &PGMessageEventResponseStore{db: db} -} - -func (m *PGMessageEventResponseStore) Create(ctx context.Context, mer *event.MessageEventResponse) error { - var query string - queryBase := "INSERT INTO message_event_responses" - queryReturn := "RETURNING id" - - if mer.ScheduledMessageEventId == uuid.Nil { - query = fmt.Sprintf("%s (response, user_id, sent, contact_id, message_id, status) VALUES ($1, $2, $3, $4, $5, $6) %s", queryBase, queryReturn) - return m.db.QueryRow(ctx, query, mer.Response, mer.UserId, mer.Sent, mer.ContactId, mer.MessageId, mer.Status).Scan(&mer.Id) - } else { - query = fmt.Sprintf("%s (scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status) VALUES ($1, $2, $3, $4, $5, $6, $7) %s", queryBase, queryReturn) - return m.db.QueryRow(ctx, query, mer.ScheduledMessageEventId, mer.Response, mer.UserId, mer.Sent, mer.ContactId, mer.MessageId, mer.Status).Scan(&mer.Id) - } -} - -func (m *PGMessageEventResponseStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*event.MessageEventResponse, error) { - query := "SELECT id, scheduled_message_event_id, response, user_id, sent, contact_id, message_id, status FROM message_event_responses WHERE user_id = $1" - rows, err := m.db.Query(ctx, query, userId) - if err != nil { - return nil, fmt.Errorf("Error querying: %w", err) - } - defer rows.Close() - - var sentMessages []*event.MessageEventResponse - for rows.Next() { - var msg event.MessageEventResponse - if err := rows.Scan(&msg.Id, &msg.ScheduledMessageEventId, &msg.Response, &msg.UserId, &msg.Sent, &msg.ContactId, &msg.MessageId, &msg.Status); err != nil { - return nil, fmt.Errorf("Scanning row: %w", err) - } - sentMessages = append(sentMessages, &msg) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("Iterating message_event_responses: %w", err) - } - - return sentMessages, nil -} diff --git a/internal/store/message_store.go b/internal/store/message_store.go deleted file mode 100644 index 29528af..0000000 --- a/internal/store/message_store.go +++ /dev/null @@ -1,95 +0,0 @@ -package store - -import ( - "context" - "fmt" - - "git.kundeng.us/phoenix/textsender-models/tx0/message" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" -) - -type MessageStore interface { - GetMessageByID(ctx context.Context, id uuid.UUID) (*message.Message, error) - CreateMessage(ctx context.Context, msg *message.Message) error - GetAllMessages(ctx context.Context) ([]*message.Message, error) - MessageExists(ctx context.Context, msg *message.Message) (bool, error) -} - -type PGMessageStore struct { - db *pgxpool.Pool -} - -func NewMessageStore(db *pgxpool.Pool) *PGMessageStore { - return &PGMessageStore{db: db} -} - -func (m *PGMessageStore) CreateMessage(ctx context.Context, msg *message.Message) error { - query := ` - INSERT INTO messages (content, user_id) - VALUES ($1, $2) - RETURNING id - ` - - return m.db.QueryRow(ctx, query, msg.Content, msg.UserId).Scan( - &msg.Id, - ) -} - -func (m *PGMessageStore) MessageExists(ctx context.Context, msg *message.Message) (bool, error) { - query := `SELECT EXISTS(SELECT 1 FROM messages WHERE content = $1 AND user_id = $2)` - - var exists bool - err := m.db.QueryRow(ctx, query, msg.Content, msg.UserId).Scan(&exists) - if err != nil { - return false, fmt.Errorf("checking if message exists: %w", err) - } else { - return exists, nil - } -} - -func (m *PGMessageStore) GetAllMessages(ctx context.Context) ([]*message.Message, error) { - query := `SELECT id, content, user_id FROM messages` - - rows, err := m.db.Query(ctx, query) - if err != nil { - return nil, fmt.Errorf("querying all messages: %w", err) - } - defer rows.Close() - - var messages []*message.Message - for rows.Next() { - var msg message.Message - if err := rows.Scan( - &msg.Id, &msg.Content, &msg.UserId, - ); err != nil { - return nil, fmt.Errorf("scanning message row: %w", err) - } - messages = append(messages, &msg) - } - - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("iterating message rows: %w", err) - } - - return messages, nil -} - -func (m *PGMessageStore) GetMessageByID(ctx context.Context, id uuid.UUID) (*message.Message, error) { - query := `SELECT id, content, user_id FROM messages WHERE id = $1` - - var msg message.Message - err := m.db.QueryRow(ctx, query, id).Scan( - &msg.Id, &msg.Content, &msg.UserId, - ) - - if err == pgx.ErrNoRows { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf("getting message by ID: %w", err) - } - - return &msg, nil -} diff --git a/internal/store/mock/contact_store.go b/internal/store/mock/contact_store.go deleted file mode 100644 index d337c40..0000000 --- a/internal/store/mock/contact_store.go +++ /dev/null @@ -1,146 +0,0 @@ -package mock - -import ( - "context" - "errors" - "fmt" - "sync" - - "git.kundeng.us/phoenix/textsender-models/tx0/contact" - "github.com/google/uuid" -) - -type Key struct { - PhoneNumber string - UserId uuid.UUID -} - -type MockContactStore struct { - Contacts map[uuid.UUID]*contact.Contact - ContactsByKey map[Key]*contact.Contact - mu sync.RWMutex - Error error // Optional: simulate errors -} - -func NewMockContactStore() *MockContactStore { - return &MockContactStore{ - Contacts: make(map[uuid.UUID]*contact.Contact), - ContactsByKey: make(map[Key]*contact.Contact), - } -} - -func (m *MockContactStore) CreateContact(ctx context.Context, con *contact.Contact) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return m.Error - } - - var id uuid.UUID - if con.Id == nil { - id = uuid.New() - con.Id = &id - } - - key := Key{PhoneNumber: con.PhoneNumber, UserId: *con.UserId} - if _, exists := m.ContactsByKey[key]; exists { - return errors.New("Contact with phone number already exists") - } - - m.Contacts[*con.Id] = con - m.ContactsByKey[key] = con - return nil -} - -func (m *MockContactStore) GetContactByID(ctx context.Context, id uuid.UUID) (*contact.Contact, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - if m.Error != nil { - return nil, m.Error - } - - con, exists := m.Contacts[id] - if !exists { - return nil, errors.New("Contact not found") - } - - return con, nil -} - -func (m *MockContactStore) GetContactByPhone(ctx context.Context, phoneNumber string, userId uuid.UUID) (*contact.Contact, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - con, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}] - if !exists { - return nil, errors.New("Contact not found") - } - - return con, nil -} - -func (m *MockContactStore) GetAllContacts(ctx context.Context) ([]*contact.Contact, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - cons := make([]*contact.Contact, 0, len(m.Contacts)) - for _, con := range m.Contacts { - cons = append(cons, con) - } - - return cons, nil -} - -func (m *MockContactStore) ContactExists(ctx context.Context, phoneNumber string, userId uuid.UUID) (bool, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return false, m.Error - } - - _, exists := m.ContactsByKey[Key{PhoneNumber: phoneNumber, UserId: userId}] - - return exists, nil -} - -func (m *MockContactStore) UpdateNames(ctx context.Context, con *contact.Contact, firstname *string, lastname *string, nickname *string) (int64, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return 0, m.Error - } else if firstname == nil && lastname == nil && nickname == nil { - return 0, fmt.Errorf("Names are not provided") - } else { - if c, exists := m.Contacts[*con.Id]; !exists { - return 0, fmt.Errorf("Contact does not exist") - } else { - if firstname != nil { - c.Firstname = firstname - } - if lastname != nil { - c.Lastname = lastname - } - if nickname != nil { - c.Nickname = nickname - } - } - } - - return 0, nil -} diff --git a/internal/store/mock/message_event_response_store.go b/internal/store/mock/message_event_response_store.go deleted file mode 100644 index 5ffc0ff..0000000 --- a/internal/store/mock/message_event_response_store.go +++ /dev/null @@ -1,81 +0,0 @@ -package mock - -import ( - "context" - "fmt" - "sync" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/event" - "github.com/google/uuid" -) - -type MessageEventResponseKey struct { - ScheduledMessageEventId uuid.UUID - UserId uuid.UUID -} -type MockMessageEventResponseStore struct { - MessageEventResponses map[uuid.UUID]*event.MessageEventResponse - MessageEventResponsesByKey map[MessageEventResponseKey]*event.MessageEventResponse - mu sync.RWMutex - Error error -} - -func NewMockMessageEventResponseStore() *MockMessageEventResponseStore { - return &MockMessageEventResponseStore{ - MessageEventResponses: make(map[uuid.UUID]*event.MessageEventResponse), - MessageEventResponsesByKey: make(map[MessageEventResponseKey]*event.MessageEventResponse), - } -} - -func (m *MockMessageEventResponseStore) Create(ctx context.Context, mer *event.MessageEventResponse) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return m.Error - } - - if mer.Id == uuid.Nil { - mer.Id = uuid.New() - } - - if len(mer.Response) == 0 { - return fmt.Errorf("Response is empty") - } else if mer.UserId == uuid.Nil { - return fmt.Errorf("User Id is nil") - } else { - key := MessageEventResponseKey{ScheduledMessageEventId: mer.ScheduledMessageEventId, UserId: mer.UserId} - if mer.ScheduledMessageEventId == uuid.Nil { - key.ScheduledMessageEventId = uuid.Nil - } else { - key.ScheduledMessageEventId = mer.ScheduledMessageEventId - } - sent := time.Now() - mer.Sent = sent - - m.MessageEventResponses[mer.Id] = mer - m.MessageEventResponsesByKey[key] = mer - - return nil - } -} - -// TODO: Add code to get mock MessageEventResponse -func (m *MockMessageEventResponseStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*event.MessageEventResponse, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - var messages []*event.MessageEventResponse - for _, msg := range m.MessageEventResponses { - if msg.UserId == userId { - messages = append(messages, msg) - } - } - - return messages, nil -} diff --git a/internal/store/mock/scheduled_message_event_store.go b/internal/store/mock/scheduled_message_event_store.go deleted file mode 100644 index f8631f8..0000000 --- a/internal/store/mock/scheduled_message_event_store.go +++ /dev/null @@ -1,159 +0,0 @@ -package mock - -import ( - "context" - "fmt" - "sync" - "time" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" -) - -type ScheduledMessageEventKey struct { - ContactId uuid.UUID - MessageId uuid.UUID - ScheduledMessageId uuid.UUID -} - -type MockScheduledMessageEventStore struct { - ScheduledMessageEvents map[uuid.UUID]*scheduling.ScheduledMessageEvent - ScheduledMessageEventsByKey map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent - mu sync.RWMutex - Error error // Optional: simulate errors -} - -func NewMockScheduledMessageEventStore() *MockScheduledMessageEventStore { - return &MockScheduledMessageEventStore{ - ScheduledMessageEvents: make(map[uuid.UUID]*scheduling.ScheduledMessageEvent), - ScheduledMessageEventsByKey: make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent), - } -} - -func (m *MockScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - if id == uuid.Nil { - return nil, fmt.Errorf("Id is nil") - } - - if _, exists := m.ScheduledMessageEvents[id]; exists { - return m.ScheduledMessageEvents[id], nil - } else { - return nil, fmt.Errorf("Not found") - } -} - -func (m *MockScheduledMessageEventStore) GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - if schMsgId == uuid.Nil { - return nil, fmt.Errorf("Scheduled message Id is nil") - } - - var events []*scheduling.ScheduledMessageEvent - - for _, event := range m.ScheduledMessageEvents { - if event.ScheduledMessageId == schMsgId { - events = append(events, event) - } - } - - if len(events) > 0 { - return events, nil - } else { - return nil, nil - } -} - -func (m *MockScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUID) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return m.Error - } - - if id == uuid.Nil { - return fmt.Errorf("Id is nil") - } - - if _, exists := m.ScheduledMessageEvents[id]; exists { - originalAmount := len(m.ScheduledMessageEvents) - copiedEvents := make(map[uuid.UUID]*scheduling.ScheduledMessageEvent) - copiedEventsKey := make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent) - - for i, schMsgEvent := range m.ScheduledMessageEvents { - key := ScheduledMessageEventKey{ContactId: schMsgEvent.ContactId, MessageId: schMsgEvent.MessageId, ScheduledMessageId: schMsgEvent.ScheduledMessageId} - if schMsgEvent.Id != id { - copiedEvents[i] = schMsgEvent - copiedEventsKey[key] = schMsgEvent - } - } - - if originalAmount > 1 { - if len(copiedEvents) > 0 && len(copiedEventsKey) > 0 { - m.ScheduledMessageEvents = copiedEvents - m.ScheduledMessageEventsByKey = copiedEventsKey - return nil - } else { - return fmt.Errorf("Not removed") - } - } else { - m.ScheduledMessageEvents = make(map[uuid.UUID]*scheduling.ScheduledMessageEvent) - m.ScheduledMessageEventsByKey = make(map[ScheduledMessageEventKey]*scheduling.ScheduledMessageEvent) - - return nil - } - } else { - return fmt.Errorf("Not found") - } -} - -func (m *MockScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return m.Error - } - - if event.Id == uuid.Nil { - event.Id = uuid.New() - } - - key := ScheduledMessageEventKey{ContactId: event.ContactId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId} - - if _, exists := m.ScheduledMessageEventsByKey[key]; exists { - return fmt.Errorf("Already exists") - } else { - event.Created = time.Now() - m.ScheduledMessageEvents[event.Id] = event - m.ScheduledMessageEventsByKey[key] = event - return nil - } -} - -func (m *MockScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return false, m.Error - } - - key := ScheduledMessageEventKey{ContactId: event.ContactId, MessageId: event.MessageId, ScheduledMessageId: event.ScheduledMessageId} - _, exists := m.ScheduledMessageEventsByKey[key] - - return exists, nil -} diff --git a/internal/store/mock/scheduled_message_store.go b/internal/store/mock/scheduled_message_store.go deleted file mode 100644 index 75a0853..0000000 --- a/internal/store/mock/scheduled_message_store.go +++ /dev/null @@ -1,141 +0,0 @@ -package mock - -import ( - "context" - "fmt" - "sync" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" -) - -type ScheduledMessageKey struct { - UserId uuid.UUID -} - -type MockScheduledMessageStore struct { - ScheduledMessages map[uuid.UUID]*scheduling.ScheduledMessage - ScheduledMessagesByKey map[ScheduledMessageKey]*scheduling.ScheduledMessage - mu sync.RWMutex - Error error // Optional: simulate errors -} - -func NewMockScheduledMessageStore() *MockScheduledMessageStore { - return &MockScheduledMessageStore{ - ScheduledMessages: make(map[uuid.UUID]*scheduling.ScheduledMessage), - ScheduledMessagesByKey: make(map[ScheduledMessageKey]*scheduling.ScheduledMessage), - } -} - -func (m *MockScheduledMessageStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessage, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - if id == uuid.Nil { - return nil, fmt.Errorf("Id is nil") - } else { - if schMsg := m.ScheduledMessages[id]; schMsg != nil { - return schMsg, nil - } else { - return nil, fmt.Errorf("Scheduled message does not exist") - } - } -} - -func (m *MockScheduledMessageStore) FetchNextScheduledMessage(ctx context.Context) (*scheduling.ScheduledMessage, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - var schMsg *scheduling.ScheduledMessage - for _, msg := range m.ScheduledMessages { - if msg.Status == scheduling.Ready { - msg.Status = scheduling.Processing - schMsg = msg - break - } - } - - if schMsg == nil { - return nil, fmt.Errorf("No scheduled message is ready") - } else { - return schMsg, nil - } -} - -func (m *MockScheduledMessageStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*scheduling.ScheduledMessage, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - if userId == uuid.Nil { - return nil, fmt.Errorf("User Id is nil") - } else { - var schMsgs []*scheduling.ScheduledMessage - for _, schMsg := range m.ScheduledMessages { - if schMsg.UserId == userId { - schMsgs = append(schMsgs, schMsg) - } - } - - if len(schMsgs) == 0 { - return nil, nil - } else { - return schMsgs, nil - } - } -} - -func (m *MockScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return m.Error - } - - if schedMsg.Id == uuid.Nil { - schedMsg.Id = uuid.New() - } - - key := ScheduledMessageKey{UserId: schedMsg.UserId} - - m.ScheduledMessages[schedMsg.Id] = schedMsg - m.ScheduledMessagesByKey[key] = schedMsg - return nil -} - -func (m *MockScheduledMessageStore) UpdateStatus(ctx context.Context, id uuid.UUID, updatedStatus string) (*string, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - if id == uuid.Nil { - return nil, fmt.Errorf("Id is nil") - } else { - if schMsg := m.ScheduledMessages[id]; schMsg != nil { - key := ScheduledMessageKey{UserId: schMsg.UserId} - - schMsg.Status = updatedStatus - m.ScheduledMessages[schMsg.Id] = schMsg - m.ScheduledMessagesByKey[key] = schMsg - - return &updatedStatus, nil - } else { - return nil, fmt.Errorf("Scheduled message does not exist") - } - } -} diff --git a/internal/store/mock/store.go b/internal/store/mock/store.go deleted file mode 100644 index ec48680..0000000 --- a/internal/store/mock/store.go +++ /dev/null @@ -1,101 +0,0 @@ -package mock - -import ( - "context" - "errors" - "fmt" - "sync" - - "git.kundeng.us/phoenix/textsender-models/tx0/message" - "github.com/google/uuid" -) - -type MessageKey struct { - Content string - UserId uuid.UUID -} - -type MockMessageStore struct { - Messages map[uuid.UUID]*message.Message - MessagesByKey map[MessageKey]*message.Message - mu sync.RWMutex - Error error // Optional: simulate errors -} - -func NewMockMessageStore() *MockMessageStore { - return &MockMessageStore{ - Messages: make(map[uuid.UUID]*message.Message), - MessagesByKey: make(map[MessageKey]*message.Message), - } -} - -func (m *MockMessageStore) CreateMessage(ctx context.Context, msg *message.Message) error { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return m.Error - } - - var id uuid.UUID - if msg != nil { - if msg.Id == nil || *msg.Id == uuid.Nil { - id = uuid.New() - msg.Id = &id - } - } else { - return fmt.Errorf("Message not populated") - } - - key := MessageKey{Content: msg.Content, UserId: *msg.UserId} - if _, exists := m.MessagesByKey[key]; exists { - return errors.New("Message already exists") - } - - m.Messages[*msg.Id] = msg - m.MessagesByKey[key] = msg - return nil -} - -func (m *MockMessageStore) GetAllMessages(ctx context.Context) ([]*message.Message, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - var msgs []*message.Message - - for _, msg := range m.Messages { - msgs = append(msgs, msg) - } - - return msgs, nil -} - -func (m *MockMessageStore) GetMessageByID(ctx context.Context, id uuid.UUID) (*message.Message, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return nil, m.Error - } - - msg := m.Messages[id] - - return msg, nil -} - -func (m *MockMessageStore) MessageExists(ctx context.Context, msg *message.Message) (bool, error) { - m.mu.Lock() - defer m.mu.Unlock() - - if m.Error != nil { - return false, m.Error - } - - _, exists := m.MessagesByKey[MessageKey{Content: msg.Content, UserId: *msg.UserId}] - - return exists, nil -} diff --git a/internal/store/scheduled_message_event_store.go b/internal/store/scheduled_message_event_store.go deleted file mode 100644 index ae27c50..0000000 --- a/internal/store/scheduled_message_event_store.go +++ /dev/null @@ -1,113 +0,0 @@ -package store - -import ( - "context" - "fmt" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" -) - -type ScheduledMessageEventStore interface { - Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) - GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error) - Delete(ctx context.Context, id uuid.UUID) error - CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error - Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) -} - -type PGScheduledMessageEventStore struct { - db *pgxpool.Pool -} - -func NewScheduledMessageEventStore(db *pgxpool.Pool) *PGScheduledMessageEventStore { - return &PGScheduledMessageEventStore{db: db} -} - -func (s *PGScheduledMessageEventStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessageEvent, error) { - query := `SELECT id, contact_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE id = $1` - - var event scheduling.ScheduledMessageEvent - err := s.db.QueryRow(ctx, query, id).Scan( - &event.Id, &event.ContactId, &event.MessageId, &event.ScheduledMessageId, &event.Created, - ) - - if err == pgx.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, fmt.Errorf("getting scheduled message event by ID: %w", err) - } else { - return &event, nil - } -} - -func (s *PGScheduledMessageEventStore) GetWithScheduleMessageId(ctx context.Context, schMsgId uuid.UUID) ([]*scheduling.ScheduledMessageEvent, error) { - var events []*scheduling.ScheduledMessageEvent - query := `SELECT id, contact_id, message_id, scheduled_message_id, created FROM scheduled_message_events WHERE scheduled_message_id = $1 - ` - - rows, err := s.db.Query(ctx, query, schMsgId) - - if err != nil { - return nil, fmt.Errorf("Error retrieving rows from scheduled_message_Events: %w", err) - } - - for rows.Next() { - var event scheduling.ScheduledMessageEvent - - if err := rows.Scan(&event.Id, &event.ContactId, &event.MessageId, &event.ScheduledMessageId, &event.Created); err != nil { - return nil, fmt.Errorf("Error fetching data from row: %w", err) - } else { - events = append(events, &event) - } - } - - if err := rows.Err(); err != nil { - return nil, err - } - - return events, nil -} - -func (s *PGScheduledMessageEventStore) Delete(ctx context.Context, id uuid.UUID) error { - query := ` - DELETE FROM scheduled_message_events WHERE id = $1 - ` - - commandTag, err := s.db.Exec(ctx, query, id) - if err != nil { - return fmt.Errorf("error deleting event: %w", err) - } - - if commandTag.RowsAffected() == 0 { - return fmt.Errorf("no event found with id %d", id) - } else { - return nil - } -} - -func (s *PGScheduledMessageEventStore) CreateScheduledMessageEvent(ctx context.Context, event *scheduling.ScheduledMessageEvent) error { - query := ` - INSERT INTO scheduled_message_events (contact_id, message_id, scheduled_message_id) - VALUES ($1, $2, $3) - RETURNING id, created - ` - - return s.db.QueryRow(ctx, query, event.ContactId, event.MessageId, event.ScheduledMessageId).Scan( - &event.Id, &event.Created, - ) -} - -func (s *PGScheduledMessageEventStore) Exists(ctx context.Context, event *scheduling.ScheduledMessageEvent) (bool, error) { - query := `SELECT EXISTS(SELECT 1 FROM scheduled_message_events WHERE scheduled_message_id = $1 AND contact_id = $2)` - - var exists bool - err := s.db.QueryRow(ctx, query, event.ScheduledMessageId, event.ContactId).Scan(&exists) - if err != nil { - return false, fmt.Errorf("Scheduled message event does not exist: %w", err) - } else { - return exists, nil - } -} diff --git a/internal/store/scheduled_message_store.go b/internal/store/scheduled_message_store.go deleted file mode 100644 index d1aeea6..0000000 --- a/internal/store/scheduled_message_store.go +++ /dev/null @@ -1,127 +0,0 @@ -package store - -import ( - "context" - "fmt" - - "git.kundeng.us/phoenix/textsender-models/tx0/message/scheduling" - "github.com/google/uuid" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" -) - -type ScheduledMessageStore interface { - Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessage, error) - FetchNextScheduledMessage(ctx context.Context) (*scheduling.ScheduledMessage, error) - GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*scheduling.ScheduledMessage, error) - CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error - UpdateStatus(ctx context.Context, id uuid.UUID, updatedStatus string) (*string, error) -} - -type PGScheduledMessageStore struct { - db *pgxpool.Pool -} - -func NewScheduledMessageStore(db *pgxpool.Pool) *PGScheduledMessageStore { - return &PGScheduledMessageStore{db: db} -} - -func (s *PGScheduledMessageStore) Get(ctx context.Context, id uuid.UUID) (*scheduling.ScheduledMessage, error) { - query := ` - SELECT id, scheduled, created, status, user_id FROM scheduled_messages WHERE id = $1 - ` - - var schMsg scheduling.ScheduledMessage - err := s.db.QueryRow(ctx, query, id).Scan( - &schMsg.Id, &schMsg.Scheduled, &schMsg.Created, &schMsg.Status, &schMsg.UserId, - ) - - if err == pgx.ErrNoRows { - return nil, fmt.Errorf("No rows") - } else if err != nil { - return nil, fmt.Errorf("Getting scheduled message: %w", err) - } - - return &schMsg, nil -} - -func (s *PGScheduledMessageStore) FetchNextScheduledMessage(ctx context.Context) (*scheduling.ScheduledMessage, error) { - query := ` - UPDATE scheduled_messages - SET status = $1 - WHERE id = ( - SELECT id FROM scheduled_messages - WHERE status = $2 - ORDER BY id - FOR UPDATE SKIP LOCKED - LIMIT 1 - ) - RETURNING id, scheduled, created, status, user_id - ` - var schMsg scheduling.ScheduledMessage - err := s.db.QueryRow(ctx, query, scheduling.Processing, scheduling.Ready).Scan( - &schMsg.Id, &schMsg.Scheduled, &schMsg.Created, &schMsg.Status, &schMsg.UserId, - ) - - if err == pgx.ErrNoRows { - return nil, nil - } else if err != nil { - return nil, fmt.Errorf("Getting scheduled message: %w", err) - } else { - return &schMsg, nil - } -} - -func (s *PGScheduledMessageStore) GetWithUserId(ctx context.Context, userId uuid.UUID) ([]*scheduling.ScheduledMessage, error) { - query := ` - SELECT id, scheduled, created, status, user_id FROM scheduled_messages WHERE user_id = $1 - ` - rows, err := s.db.Query(ctx, query, userId) - if err != nil { - return nil, fmt.Errorf("Error retrieving rows from scheduled_messages: %w", err) - } - - var schMsgs []*scheduling.ScheduledMessage - - for rows.Next() { - var schMsg scheduling.ScheduledMessage - if err := rows.Scan(&schMsg.Id, &schMsg.Scheduled, &schMsg.Created, &schMsg.Status, &schMsg.UserId); err != nil { - return nil, fmt.Errorf("Error fetching data from row: %w", err) - } else { - schMsgs = append(schMsgs, &schMsg) - } - } - - if err := rows.Err(); err != nil { - return nil, err - } else { - return schMsgs, nil - } -} - -func (s *PGScheduledMessageStore) CreateScheduledMessage(ctx context.Context, schedMsg *scheduling.ScheduledMessage) error { - query := ` - INSERT INTO scheduled_messages (scheduled, status, user_id) - VALUES ($1, $2, $3) - RETURNING id, created - ` - - return s.db.QueryRow(ctx, query, schedMsg.Scheduled, schedMsg.Status, schedMsg.UserId).Scan( - &schedMsg.Id, &schedMsg.Created, - ) -} - -func (s *PGScheduledMessageStore) UpdateStatus(ctx context.Context, id uuid.UUID, updatedStatus string) (*string, error) { - query := ` - UPDATE scheduled_messages set status = $1 WHERE id = $2 - RETURNING status - ` - - var returnedStatus string - - if err := s.db.QueryRow(ctx, query, updatedStatus, id).Scan(&returnedStatus); err != nil { - return nil, err - } else { - return &returnedStatus, nil - } -} diff --git a/internal/version/version.go b/internal/version/version.go deleted file mode 100644 index bcac161..0000000 --- a/internal/version/version.go +++ /dev/null @@ -1,17 +0,0 @@ -package version - -import "fmt" - -var ( - Version = "dev" - BuildTime = "none" - Commit = "unknown" - GoVersion = "unknown" -) - -func String() string { - return fmt.Sprintf( - "Version: %s\nBuild Date: %s\nCommit: %s\nGo Version: %s", - Version, BuildTime, Commit, GoVersion, - ) -} From 2610162da144f9ba133a2e88482fda4fa1522052 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sun, 31 May 2026 18:42:08 -0400 Subject: [PATCH 2/5] Adding rust files (#4) Reviewed-on: http://git.kundeng.us/phoenix/textsender-api/pulls/4 --- .gitea/workflows/workflow.yml | 146 ++ .gitignore | 4 + Cargo.lock | 3251 +++++++++++++++++++++++++++++++++ Cargo.toml | 33 + src/main.rs | 19 + 5 files changed, 3453 insertions(+) create mode 100644 .gitea/workflows/workflow.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/main.rs diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml new file mode 100644 index 0000000..2150d5f --- /dev/null +++ b/.gitea/workflows/workflow.yml @@ -0,0 +1,146 @@ +name: Rust Build + +on: + push: + branches: + - v0.2.0 + - v0.2.0 + pull_request: + branches: + - v0.2.0 + - v0.2.0 + +jobs: + check: + name: Check + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.95 + - run: | + mkdir -p ~/.ssh + echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key + chmod 600 ~/.ssh/textsender-models_deploy_key + ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + + eval $(ssh-agent -s) + ssh-add -v ~/.ssh/textsender-models_deploy_key + + cargo check + + test: + name: Test Suite + runs-on: ubuntu-24.04 + # --- Add database service definition --- + services: + postgres: + image: postgres:18.3-alpine + env: + # Use secrets for DB init, with fallbacks for flexibility + POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }} + POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD || 'testpassword' }} + POSTGRES_DB: ${{ secrets.DB_TEST_NAME || 'testdb' }} + # Options to wait until the database is ready + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v5 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.95 + # --- Add this step for explicit verification --- + - name: Verify Docker Environment + run: | + echo "Runner User Info:" + id + echo "Checking Docker Version:" + docker --version + echo "Checking Docker Daemon Status (info):" + docker info + echo "Checking Docker Daemon Status (ps):" + docker ps -a + echo "Docker environment check complete." + # NOTE: Do NOT use continue-on-error here. + # If Docker isn't working as expected, the job SHOULD fail here. + - name: Run tests + env: + # Define DATABASE_URL for tests to use + DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:5432/${{ secrets.DB_TEST_NAME || 'testdb' }} + RUST_LOG: info # Optional: configure test log level + SECRET_KEY: ${{ secrets.TOKEN_SECRET_KEY }} + # Make SSH agent available if tests fetch private dependencies + SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }} + ENABLE_REGISTRATION: 'TRUE' + run: | + mkdir -p ~/.ssh + echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key + chmod 600 ~/.ssh/textsender-models_deploy_key + ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + + eval $(ssh-agent -s) + ssh-add -v ~/.ssh/textsender-models_deploy_key + + cargo test + + fmt: + name: Rustfmt + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.95 + - run: rustup component add rustfmt + - run: | + mkdir -p ~/.ssh + echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key + chmod 600 ~/.ssh/textsender-models_deploy_key + ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + + eval $(ssh-agent -s) + ssh-add -v ~/.ssh/textsender-models_deploy_key + cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.95 + - run: rustup component add clippy + - run: | + mkdir -p ~/.ssh + echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key + chmod 600 ~/.ssh/textsender-models_deploy_key + ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + + eval $(ssh-agent -s) + ssh-add -v ~/.ssh/textsender-models_deploy_key + cargo clippy -- -D warnings + + build: + name: build + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v5 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: 1.95 + - run: | + mkdir -p ~/.ssh + echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key + chmod 600 ~/.ssh/textsender-models_deploy_key + ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + + eval $(ssh-agent -s) + ssh-add -v ~/.ssh/textsender-models_deploy_key + cargo build --release + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aedbae6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.env +.env.local +.env.docker +target/* diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c92f42c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3251 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "multer", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + +[[package]] +name = "common-multipart-rfc7578" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f08d53b5e0c302c5830cfa7511ba0edc3f241c691a95c0d184dfb761e11a6cc2" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "mime_guess", + "rand 0.8.6", + "thiserror 1.0.69", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core 0.10.1", + "wasip2", + "wasip3", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "byteorder", + "num-traits", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "josekit" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a808e078330e6af222eb0044b71d4b1ff981bfef43e7bc8133a88234e0c86a0c" +dependencies = [ + "anyhow", + "base64", + "flate2", + "openssl", + "regex", + "serde", + "serde_json", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "10.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" +dependencies = [ + "base64", + "ed25519-dalek", + "getrandom 0.2.17", + "hmac", + "js-sys", + "p256", + "p384", + "pem", + "rand 0.8.6", + "rsa", + "serde", + "serde_json", + "sha2", + "signature", + "simple_asn1", + "zeroize", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags", + "libc", + "plain", + "redox_syscall 0.8.0", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "memchr", + "mime", + "spin", + "version_check", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_syscall" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rust-embed" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" +dependencies = [ + "sha2", + "walkdir", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "indexmap", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "native-tls", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.6", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.6", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "time", + "tracing", + "uuid", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "time", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "textsender" +version = "0.5.0" +dependencies = [ + "axum", + "axum-extra", + "base64", + "common-multipart-rfc7578", + "futures", + "josekit", + "jsonwebtoken", + "mime_guess", + "serde", + "serde_json", + "sqlx", + "tempfile", + "textsender_models", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing-subscriber", + "url", + "utoipa", + "utoipa-swagger-ui", + "uuid", +] + +[[package]] +name = "textsender_models" +version = "0.3.0" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender-models.git?tag=v0.3.0-v0.3.0-migrate-2be0337da0-111#2be0337da008f1018692234e8043220f6812377d" +dependencies = [ + "const_format", + "dotenvy", + "josekit", + "rand 0.10.1", + "serde", + "serde_json", + "time", + "utoipa", + "uuid", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "hdrhistogram", + "indexmap", + "pin-project-lite", + "slab", + "sync_wrapper", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utoipa" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bde15df68e80b16c7d16b9616e80770ad158988daa56a27dccd1e55558b0160" +dependencies = [ + "indexmap", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "5.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba0b99ee52df3028635d93840c797102da61f8a7bb3cf751032455895b52ef8" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", + "uuid", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "9.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d047458f1b5b65237c2f6dc6db136945667f40a7668627b3490b9513a3d43a55" +dependencies = [ + "axum", + "base64", + "mime_guess", + "regex", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" +dependencies = [ + "arbitrary", + "crc32fast", + "flate2", + "indexmap", + "memchr", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..da1718f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "textsender" +version = "0.5.0" +edition = "2024" +rust-version = "1.95" + +[dependencies] +axum = { version = "0.8.9", features = ["multipart"] } +axum-extra = { version = "0.12.6", features = ["cookie"] } +serde = { version = "1.0.228", features = ["derive"] } +serde_json = { version = "1.0.149" } +tokio = { version = "1.52.3", features = ["full"] } +tokio-util = { version = "0.7.18", features = ["io"] } +tower = { version = "0.5.3", features = ["full"] } +tower-http = { version = "0.6.10", features = ["cors", "timeout"] } +tracing-subscriber = "0.3.23" +futures = { version = "0.3.32" } +mime_guess = { version = "2.0.5" } +uuid = { version = "1.23.1", features = ["v4", "serde"] } +sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] } +time = { version = "0.3.47", features = ["formatting", "macros", "parsing", "serde"] } +thiserror = "2.0.18" +base64 = "0.22.1" +jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } +josekit = { version = "0.10.3" } +utoipa = { version = "5.5.0", features = ["axum_extras"] } +utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender-models.git", tag = "v0.3.0-v0.3.0-migrate-2be0337da0-111" } + +[dev-dependencies] +common-multipart-rfc7578 = { version = "0.7.0" } +url = { version = "2.5.8" } +tempfile = { version = "3.27.0" } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..94ff489 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,19 @@ + +#[tokio::main] +async fn main() { + // initialize tracing + tracing_subscriber::fmt::init(); + + /* + match tokio::net::TcpListener::bind(config::host::get_full()).await { + Ok(listener) => { + // build our application with routes + let app = config::init::app().await; + axum::serve(listener, app).await.unwrap(); + } + Err(err) => { + eprintln!("Error: {err:?}") + } + } + */ +} From 1d9267e036bd36f95aad03b737a5c3284b028433 Mon Sep 17 00:00:00 2001 From: phoenix Date: Fri, 12 Jun 2026 22:17:46 -0400 Subject: [PATCH 3/5] Add docker (#5) Reviewed-on: http://git.kundeng.us/phoenix/textsender-api/pulls/5 --- .gitea/workflows/workflow.yml | 17 +++-- Cargo.lock | 75 +++++--------------- Cargo.toml | 4 +- Dockerfile | 70 ++++++++++++++++++ docker-compose.yml | 129 ++++++++++++++++++++++++++++++++++ src/main.rs | 14 ---- 6 files changed, 229 insertions(+), 80 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index 2150d5f..f8883a7 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -19,11 +19,12 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: 1.95 + - uses: Swatinem/rust-cache@v2 - run: | mkdir -p ~/.ssh echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key chmod 600 ~/.ssh/textsender-models_deploy_key - ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts eval $(ssh-agent -s) ssh-add -v ~/.ssh/textsender-models_deploy_key @@ -73,7 +74,7 @@ jobs: # Define DATABASE_URL for tests to use DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:5432/${{ secrets.DB_TEST_NAME || 'testdb' }} RUST_LOG: info # Optional: configure test log level - SECRET_KEY: ${{ secrets.TOKEN_SECRET_KEY }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} # Make SSH agent available if tests fetch private dependencies SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }} ENABLE_REGISTRATION: 'TRUE' @@ -81,7 +82,7 @@ jobs: mkdir -p ~/.ssh echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key chmod 600 ~/.ssh/textsender-models_deploy_key - ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts eval $(ssh-agent -s) ssh-add -v ~/.ssh/textsender-models_deploy_key @@ -97,11 +98,12 @@ jobs: with: toolchain: 1.95 - run: rustup component add rustfmt + - uses: Swatinem/rust-cache@v2 - run: | mkdir -p ~/.ssh echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key chmod 600 ~/.ssh/textsender-models_deploy_key - ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts eval $(ssh-agent -s) ssh-add -v ~/.ssh/textsender-models_deploy_key @@ -116,11 +118,12 @@ jobs: with: toolchain: 1.95 - run: rustup component add clippy + - uses: Swatinem/rust-cache@v2 - run: | mkdir -p ~/.ssh echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key chmod 600 ~/.ssh/textsender-models_deploy_key - ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts eval $(ssh-agent -s) ssh-add -v ~/.ssh/textsender-models_deploy_key @@ -134,13 +137,13 @@ jobs: - uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: 1.95 + - uses: Swatinem/rust-cache@v2 - run: | mkdir -p ~/.ssh echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/textsender-models_deploy_key chmod 600 ~/.ssh/textsender-models_deploy_key - ssh-keyscan ${{ secret.MYHOST }} >> ~/.ssh/known_hosts + ssh-keyscan ${{ secrets.MY_HOST }} >> ~/.ssh/known_hosts eval $(ssh-agent -s) ssh-add -v ~/.ssh/textsender-models_deploy_key cargo build --release - diff --git a/Cargo.lock b/Cargo.lock index c92f42c..649fe00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,17 +204,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chacha20" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" -dependencies = [ - "cfg-if", - "cpufeatures 0.3.0", - "rand_core 0.10.1", -] - [[package]] name = "common-multipart-rfc7578" version = "0.7.0" @@ -227,7 +216,7 @@ dependencies = [ "http", "mime", "mime_guess", - "rand 0.8.6", + "rand", "thiserror 1.0.69", ] @@ -303,15 +292,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cpufeatures" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" -dependencies = [ - "libc", -] - [[package]] name = "crc" version = "3.4.0" @@ -358,7 +338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" dependencies = [ "generic-array", - "rand_core 0.6.4", + "rand_core", "subtle", "zeroize", ] @@ -380,7 +360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -523,7 +503,7 @@ dependencies = [ "hkdf", "pem-rfc7468", "pkcs8", - "rand_core 0.6.4", + "rand_core", "sec1", "subtle", "zeroize", @@ -588,7 +568,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" dependencies = [ - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -786,7 +766,6 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -798,7 +777,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core", "subtle", ] @@ -1127,7 +1106,7 @@ dependencies = [ "p256", "p384", "pem", - "rand 0.8.6", + "rand", "rsa", "serde", "serde_json", @@ -1351,7 +1330,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.6", + "rand", "smallvec", "zeroize", ] @@ -1633,18 +1612,7 @@ checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" -dependencies = [ - "chacha20", - "getrandom 0.4.2", - "rand_core 0.10.1", + "rand_core", ] [[package]] @@ -1654,7 +1622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -1666,12 +1634,6 @@ dependencies = [ "getrandom 0.2.17", ] -[[package]] -name = "rand_core" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" - [[package]] name = "redox_syscall" version = "0.5.18" @@ -1742,7 +1704,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core 0.6.4", + "rand_core", "signature", "spki", "subtle", @@ -1958,7 +1920,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -1969,7 +1931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures 0.2.17", + "cpufeatures", "digest", ] @@ -2005,7 +1967,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core 0.6.4", + "rand_core", ] [[package]] @@ -2187,7 +2149,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.6", + "rand", "rsa", "serde", "sha1", @@ -2227,7 +2189,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.6", + "rand", "serde", "serde_json", "sha2", @@ -2332,7 +2294,7 @@ dependencies = [ ] [[package]] -name = "textsender" +name = "textsender_api" version = "0.5.0" dependencies = [ "axum", @@ -2364,12 +2326,11 @@ dependencies = [ [[package]] name = "textsender_models" version = "0.3.0" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender-models.git?tag=v0.3.0-v0.3.0-migrate-2be0337da0-111#2be0337da008f1018692234e8043220f6812377d" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender-models.git?tag=v0.3.0#d504108745b7b97a02eac24e16763cdb5e731f2b" dependencies = [ "const_format", "dotenvy", "josekit", - "rand 0.10.1", "serde", "serde_json", "time", diff --git a/Cargo.toml b/Cargo.toml index da1718f..4d327e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "textsender" +name = "textsender_api" version = "0.5.0" edition = "2024" rust-version = "1.95" @@ -25,7 +25,7 @@ jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } josekit = { version = "0.10.3" } utoipa = { version = "5.5.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } -textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender-models.git", tag = "v0.3.0-v0.3.0-migrate-2be0337da0-111" } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender-models.git", tag = "v0.3.0" } [dev-dependencies] common-multipart-rfc7578 = { version = "0.7.0" } diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f611bc8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,70 @@ +# Stage 1: Build the application +FROM rust:1.95 as builder + +# Set the working directory inside the container +WORKDIR /usr/src/app + +# Install build dependencies if needed (e.g., git for cloning) +RUN apt-get update && apt-get install -y --no-install-recommends \ + pkg-config libssl3 \ + ca-certificates \ + openssh-client git \ + && rm -rf /var/lib/apt/lists/* + +# Create .ssh/ directory for internal dependencies +RUN mkdir -p -m 0700 ~/.ssh && \ + echo "Host git.kundeng.us" >> ~/.ssh/config && \ + echo " User git" >> ~/.ssh/config && \ + chmod 600 ~/.ssh/config + +# << --- ADD HOST KEY HERE --- >> +RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts + +# Copy Cargo manifests +COPY Cargo.toml Cargo.lock ./ + +# Build *only* dependencies to leverage Docker cache +# This dummy build caches dependencies as a separate layer +RUN --mount=type=ssh mkdir src && \ + echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \ + cargo build --release --quiet && \ + rm -rf src target/release/deps/textsender_api* # Clean up dummy build artifacts (replace textsender_api) + +# Copy the actual source code +COPY src ./src +# If you have other directories like `templates` or `static`, copy them too +COPY .env ./.env +COPY migrations ./migrations + +# << --- SSH MOUNT ADDED HERE --- >> +# Build *only* dependencies to leverage Docker cache +# This dummy build caches dependencies as a separate layer +# Mount the SSH agent socket for this command +RUN --mount=type=ssh \ + cargo build --release --quiet + +# Stage 2: Create the final, smaller runtime image +# Use a minimal base image like debian-slim or even distroless for security/size +FROM debian:trixie-slim + +# Install runtime dependencies if needed (e.g., SSL certificates) +RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /usr/local/bin + +# Copy the compiled binary from the builder stage +# Replace 'textsender_api' with the actual name of your binary (usually the crate name) +COPY --from=builder /usr/src/app/target/release/textsender_api . + +# Copy other necessary files like .env (if used for runtime config) or static assets +# It's generally better to configure via environment variables in Docker though +COPY --from=builder /usr/src/app/.env . +COPY --from=builder /usr/src/app/migrations ./migrations + +# Expose the port your Axum app listens on (e.g., 3000 or 8000) +EXPOSE 9081 + +# Set the command to run your application +# Ensure this matches the binary name copied above +CMD ["./textsender_api"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0f0221a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,129 @@ +version: '3.8' # Use a recent version + +services: + # --- Web API --- + api: + build: # Tells docker-compose to build the Dockerfile in the current directory + context: . + ssh: ["default"] # Uses host's SSH agent + container_name: textsender_api # Optional: Give the container a specific name + ports: + # Map host port 8000 to container port 3000 (adjust as needed) + - "9081:9081" + env_file: + - .env + depends_on: + main_db: + condition: service_healthy + networks: + - textsender_api-network + restart: unless-stopped # Optional: Restart policy + + # --- Web API auth --- + auth_api: + build: + # Might want to change the naming convention at some point for the repo names. textsender-models, textsender-auth, textesender-api, etc + context: ../textsender-auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile) + ssh: ["default"] # Uses host's SSH agent + dockerfile: Dockerfile # Optional: Specify if your Dockerfile has a non-standard name + container_name: auth_api + restart: unless-stopped + ports: + - "9080:9080" + # environment: + # Environment variables your API needs, e.g., database connection + # Add other necessary environment variables + env_file: + - ../textsender-auth/.env + depends_on: + auth_db: + condition: service_healthy + networks: + - textsender_api-network + # Optional: Mount local code for development (live reload) + # volumes: + # - ./path/to/your/web-api-repo:/app + + # --- songparser service --- + catapult: + build: + context: ../catapult + ssh: ["default"] + dockerfile: Dockerfile + container_name: catapult + restart: unless-stopped + env_file: + - ../catapult/.env + depends_on: + - api + - main_db + - auth_api + - auth_db + networks: + - textsender_api-network + + + # PostgreSQL Database Service + # --- textsender_api web api db --- + main_db: + image: postgres:18.3-alpine # Use an official Postgres image (Alpine variant is smaller) + container_name: textsender_api_db # Optional: Give the container a specific name + environment: + # These MUST match the user, password, and database name in the DATABASE_URL above + POSTGRES_USER: ${POSTGRES_MAIN_USER:-textsender_api} + POSTGRES_PASSWORD: ${POSTGRES_MAIN_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_MAIN_DB:-textsender_api_db} + volumes: + # Persist database data using a named volume + - postgres_data:/var/lib/postgresql + ports: + # Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging) + - "5432:5432" + healthcheck: + # Checks if Postgres is ready to accept connections + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: always # Optional: Restart policy + networks: + - textsender_api-network + + # --- textsender_api web auth api db --- + auth_db: + image: postgres:18.4-alpine # Use an official Postgres image (Alpine variant is smaller) + container_name: textsender_api_auth_db # Optional: Give the container a specific name + environment: + # These MUST match the user, password, and database name in the DATABASE_URL above + POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender_api_op} + POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password} + POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_api_auth_db} + volumes: + # Persist database data using a named volume + - postgres_data_auth:/var/lib/postgresql + ports: + # Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging) + - "5433:5432" + healthcheck: + # Checks if Postgres is ready to accept connections + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: always # Optional: Restart policy + networks: + - textsender_api-network + +# Define the named volume for data persistence +volumes: + postgres_data: + driver: local # Use the default local driver + postgres_data_auth: + driver: local # Use the default local driver + +# Define the network (optional, but good practice) +networks: + textsender_api-network: + driver: bridge diff --git a/src/main.rs b/src/main.rs index 94ff489..47a4039 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,19 +1,5 @@ - #[tokio::main] async fn main() { // initialize tracing tracing_subscriber::fmt::init(); - - /* - match tokio::net::TcpListener::bind(config::host::get_full()).await { - Ok(listener) => { - // build our application with routes - let app = config::init::app().await; - axum::serve(listener, app).await.unwrap(); - } - Err(err) => { - eprintln!("Error: {err:?}") - } - } - */ } From 7107a50d58ff17b9edeb8f96677313addb25b718 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 13 Jun 2026 14:04:50 -0400 Subject: [PATCH 4/5] Update name (#7) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/7 --- .dockerignore.yaml | 4 ++-- .gitea/workflows/workflow.yml | 3 ++- Cargo.lock | 4 ++-- Cargo.toml | 4 ++-- docker-compose.yml | 6 +++--- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.dockerignore.yaml b/.dockerignore.yaml index 1c47491..95f2e30 100644 --- a/.dockerignore.yaml +++ b/.dockerignore.yaml @@ -1,5 +1,3 @@ -vendor/ - # Ignore git directory .git/ @@ -7,3 +5,5 @@ vendor/ # Ignore environment files (configure via docker-compose instead) .env* + +target/ diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index f8883a7..ba4c8ca 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -43,6 +43,7 @@ jobs: POSTGRES_USER: ${{ secrets.DB_TEST_USER || 'testuser' }} POSTGRES_PASSWORD: ${{ secrets.DB_TEST_PASSWORD || 'testpassword' }} POSTGRES_DB: ${{ secrets.DB_TEST_NAME || 'testdb' }} + POSTGRES_PORT: ${{ secrets.DB_PORT || 5432 }} # Options to wait until the database is ready options: >- --health-cmd pg_isready @@ -72,7 +73,7 @@ jobs: - name: Run tests env: # Define DATABASE_URL for tests to use - DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:5432/${{ secrets.DB_TEST_NAME || 'testdb' }} + DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:${{ secrets.DB_PORT || 5432 }}/${{ secrets.DB_TEST_NAME || 'testdb' }} RUST_LOG: info # Optional: configure test log level SECRET_KEY: ${{ secrets.SECRET_KEY }} # Make SSH agent available if tests fetch private dependencies diff --git a/Cargo.lock b/Cargo.lock index 649fe00..64b2264 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2295,7 +2295,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.5.0" +version = "0.1.3" dependencies = [ "axum", "axum-extra", @@ -2326,7 +2326,7 @@ dependencies = [ [[package]] name = "textsender_models" version = "0.3.0" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender-models.git?tag=v0.3.0#d504108745b7b97a02eac24e16763cdb5e731f2b" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.3.0#d504108745b7b97a02eac24e16763cdb5e731f2b" dependencies = [ "const_format", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 4d327e3..eb85782 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.5.0" +version = "0.1.3" edition = "2024" rust-version = "1.95" @@ -25,7 +25,7 @@ jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } josekit = { version = "0.10.3" } utoipa = { version = "5.5.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } -textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender-models.git", tag = "v0.3.0" } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.3.0" } [dev-dependencies] common-multipart-rfc7578 = { version = "0.7.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 0f0221a..56a3f79 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,8 +22,8 @@ services: # --- Web API auth --- auth_api: build: - # Might want to change the naming convention at some point for the repo names. textsender-models, textsender-auth, textesender-api, etc - context: ../textsender-auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile) + # Might want to change the naming convention at some point for the repo names. textsender-models, textsender_auth, textesender-api, etc + context: ../textsender_auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile) ssh: ["default"] # Uses host's SSH agent dockerfile: Dockerfile # Optional: Specify if your Dockerfile has a non-standard name container_name: auth_api @@ -34,7 +34,7 @@ services: # Environment variables your API needs, e.g., database connection # Add other necessary environment variables env_file: - - ../textsender-auth/.env + - ../textsender_auth/.env depends_on: auth_db: condition: service_healthy From e940d6b1595a69c3c60fff338bf7dd603bd3eb47 Mon Sep 17 00:00:00 2001 From: phoenix Date: Sat, 13 Jun 2026 19:49:14 -0400 Subject: [PATCH 5/5] First one (#8) Reviewed-on: http://git.kundeng.us/phoenix/textsender_api/pulls/8 --- .env.docker.sample | 13 +- .env.local.sample | 13 +- .gitea/workflows/workflow.yml | 2 +- Cargo.lock | 6 +- Cargo.toml | 4 +- docker-compose.yml | 48 +++--- migrations/20260613211440_init.sql | 12 ++ migrations/schema.sql | 98 ++++++------ src/auth/mod.rs | 61 ++++++++ src/caller/mod.rs | 110 ++++++++++++++ src/config/mod.rs | 118 +++++++++++++++ src/db/mod.rs | 26 ++++ src/lib.rs | 5 + src/main.rs | 11 ++ src/repo/mod.rs | 29 ++++ tests/test.rs | 230 +++++++++++++++++++++++++++++ 16 files changed, 695 insertions(+), 91 deletions(-) create mode 100644 migrations/20260613211440_init.sql create mode 100644 src/auth/mod.rs create mode 100644 src/caller/mod.rs create mode 100644 src/config/mod.rs create mode 100644 src/db/mod.rs create mode 100644 src/lib.rs create mode 100644 src/repo/mod.rs create mode 100644 tests/test.rs diff --git a/.env.docker.sample b/.env.docker.sample index 4bcc074..7600a38 100644 --- a/.env.docker.sample +++ b/.env.docker.sample @@ -1,10 +1,11 @@ JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW! -DB_NAME=textsender_db -DB_USER=textsender -DB_PASSWORD=password -DB_HOST=main_db -DB_PORT=5432 -DB_SSLMODE=disable +DB_MAIN_NAME=textsender_db +DB_MAIN_USER=textsender +DB_MAIN_PASSWORD=password +DB_MAIN_HOST=main_db +DB_MAIN_PORT=5432 +DB_MAIN_SSLMODE=disable +DATABASE_URL=postgres://${DB_MAIN_USER}:${DB_MAIN_PASSWORD}@${DB_MAIN_HOST}:${DB_MAIN_PORT}/${DB_MAIN_NAME} TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" diff --git a/.env.local.sample b/.env.local.sample index e47c1f5..166eb33 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -1,10 +1,11 @@ JWT_SECRET=NULqYIzgt28bTiyziCd7IOO7b6LnWDW! -DB_NAME=textsender_db -DB_USER=textsender -DB_PASSWORD=password -DB_HOST=localhost -DB_PORT=5432 -DB_SSLMODE=disable +DB_MAIN_NAME=textsender_db +DB_MAIN_USER=textsender +DB_MAIN_PASSWORD=password +DB_MAIN_HOST=localhost +DB_MAIN_PORT=5432 +DB_MAIN_SSLMODE=disable +DATABASE_URL=postgres://${DB_MAIN_USER}:${DB_MAIN_PASSWORD}@${DB_MAIN_HOST}:${DB_MAIN_PORT}/${DB_MAIN_NAME} TWILIO_AUTH_SID=9M438C93R943U4329MCU43C34U TWILIO_SERVICE_SID=9M4J3X8439U398NUVT3342MC349C348T TWILIO_AUTH_TOKEN="f4a1f2b0b79ea3735078c2d8ee9684e1" diff --git a/.gitea/workflows/workflow.yml b/.gitea/workflows/workflow.yml index ba4c8ca..ea96dad 100644 --- a/.gitea/workflows/workflow.yml +++ b/.gitea/workflows/workflow.yml @@ -75,7 +75,7 @@ jobs: # Define DATABASE_URL for tests to use DATABASE_URL: postgresql://${{ secrets.DB_TEST_USER || 'testuser' }}:${{ secrets.DB_TEST_PASSWORD || 'testpassword' }}@postgres:${{ secrets.DB_PORT || 5432 }}/${{ secrets.DB_TEST_NAME || 'testdb' }} RUST_LOG: info # Optional: configure test log level - SECRET_KEY: ${{ secrets.SECRET_KEY }} + SECRET_MAIN_KEY: ${{ secrets.SECRET_KEY }} # Make SSH agent available if tests fetch private dependencies SSH_AUTH_SOCK: ${{ env.SSH_AUTH_SOCK }} ENABLE_REGISTRATION: 'TRUE' diff --git a/Cargo.lock b/Cargo.lock index 64b2264..2ec2379 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2295,7 +2295,7 @@ dependencies = [ [[package]] name = "textsender_api" -version = "0.1.3" +version = "0.1.4" dependencies = [ "axum", "axum-extra", @@ -2325,8 +2325,8 @@ dependencies = [ [[package]] name = "textsender_models" -version = "0.3.0" -source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.3.0#d504108745b7b97a02eac24e16763cdb5e731f2b" +version = "0.3.3" +source = "git+ssh://git@git.kundeng.us/phoenix/textsender_models.git?tag=v0.3.3#c09fa36b15601b37f65b8baac0d479c30d2c1e44" dependencies = [ "const_format", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index eb85782..9462e9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "textsender_api" -version = "0.1.3" +version = "0.1.4" edition = "2024" rust-version = "1.95" @@ -25,7 +25,7 @@ jsonwebtoken = { version = "10.3.0", features = ["rust_crypto"] } josekit = { version = "0.10.3" } utoipa = { version = "5.5.0", features = ["axum_extras"] } utoipa-swagger-ui = { version = "9.0.2", features = ["axum"] } -textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.3.0" } +textsender_models = { git = "ssh://git@git.kundeng.us/phoenix/textsender_models.git", tag = "v0.3.3" } [dev-dependencies] common-multipart-rfc7578 = { version = "0.7.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 56a3f79..7fe4794 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: - textsender_api-network restart: unless-stopped # Optional: Restart policy - # --- Web API auth --- + # --- Auth API --- auth_api: build: # Might want to change the naming convention at some point for the repo names. textsender-models, textsender_auth, textesender-api, etc @@ -44,23 +44,23 @@ services: # volumes: # - ./path/to/your/web-api-repo:/app - # --- songparser service --- - catapult: - build: - context: ../catapult - ssh: ["default"] - dockerfile: Dockerfile - container_name: catapult - restart: unless-stopped - env_file: - - ../catapult/.env - depends_on: - - api - - main_db - - auth_api - - auth_db - networks: - - textsender_api-network + # --- catapult service --- +# catapult: +# build: +# context: ../catapult +# ssh: ["default"] +# dockerfile: Dockerfile +# container_name: catapult +# restart: unless-stopped +# env_file: +# - ../catapult/.env +# depends_on: +# - api +# - main_db +# - auth_api +# - auth_db +# networks: +# - textsender_api-network # PostgreSQL Database Service @@ -70,9 +70,9 @@ services: container_name: textsender_api_db # Optional: Give the container a specific name environment: # These MUST match the user, password, and database name in the DATABASE_URL above - POSTGRES_USER: ${POSTGRES_MAIN_USER:-textsender_api} - POSTGRES_PASSWORD: ${POSTGRES_MAIN_PASSWORD:-password} - POSTGRES_DB: ${POSTGRES_MAIN_DB:-textsender_api_db} + POSTGRES_USER: ${DB_MAIN_USER:-textsender_api} + POSTGRES_PASSWORD: ${DB_MAIN_PASSWORD:-password} + POSTGRES_DB: ${DB_MAIN_NAME:-textsender_api_db} volumes: # Persist database data using a named volume - postgres_data:/var/lib/postgresql @@ -96,9 +96,9 @@ services: container_name: textsender_api_auth_db # Optional: Give the container a specific name environment: # These MUST match the user, password, and database name in the DATABASE_URL above - POSTGRES_USER: ${POSTGRES_AUTH_USER:-textsender_api_op} - POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password} - POSTGRES_DB: ${POSTGRES_AUTH_DB:-textsender_api_auth_db} + POSTGRES_USER: ${DB_AUTH_USER:-textsender_auth} + POSTGRES_PASSWORD: ${DB_AUTH_PASSWORD:-password} + POSTGRES_DB: ${DB_AUTH_NAME:-textsender_auth_db} volumes: # Persist database data using a named volume - postgres_data_auth:/var/lib/postgresql diff --git a/migrations/20260613211440_init.sql b/migrations/20260613211440_init.sql new file mode 100644 index 0000000..ef5cc2b --- /dev/null +++ b/migrations/20260613211440_init.sql @@ -0,0 +1,12 @@ +-- Add migration script here +CREATE EXTENSION IF NOT EXISTS pgcrypto; +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE IF NOT EXISTS "contacts" ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + phone_number TEXT NOT NULL, + user_id UUID NOT NULL, + firstname TEXT NULL, + lastname TEXT NULL, + nickname TEXT NULL +); diff --git a/migrations/schema.sql b/migrations/schema.sql index 23587c6..9ca3c39 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -1,49 +1,49 @@ -CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; - -DROP TABLE IF EXISTS contacts CASCADE; -DROP TABLE IF EXISTS messages CASCADE; -DROP TABLE IF EXISTS scheduled_messages CASCADE; -DROP TABLE IF EXISTS scheduled_message_events CASCADE; -DROP TABLE IF EXISTS message_event_responses CASCADE; - -CREATE TABLE IF NOT EXISTS contacts ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - phone_number TEXT NOT NULL, - user_id UUID NOT NULL, - first_name TEXT NULL, - last_name TEXT NULL, - nickname TEXT NULL -); - -CREATE TABLE IF NOT EXISTS messages ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - content TEXT NOT NULL, - user_id UUID NOT NULL -); - -CREATE TABLE IF NOT EXISTS scheduled_messages ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - scheduled timestamptz NOT NULL, - created timestamptz DEFAULT now(), - status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')), - user_id UUID NOT NULL -); - -CREATE TABLE IF NOT EXISTS scheduled_message_events ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - contact_id UUID NOT NULL, - message_id UUID NOT NULL, - scheduled_message_id UUID NOT NULL, - created timestamptz DEFAULT now() -); - -CREATE TABLE IF NOT EXISTS message_event_responses ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - scheduled_message_event_id UUID NULL, - response JSONB NOT NULL, - user_id UUID NOT NULL, - sent timestamptz NOT NULL, - contact_id UUID NULL, - message_id UUID NULL, - status TEXT CHECK (status IN ('INSTANT', 'SCHEDULED')) -); +-- CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +-- +-- DROP TABLE IF EXISTS contacts CASCADE; +-- DROP TABLE IF EXISTS messages CASCADE; +-- DROP TABLE IF EXISTS scheduled_messages CASCADE; +-- DROP TABLE IF EXISTS scheduled_message_events CASCADE; +-- DROP TABLE IF EXISTS message_event_responses CASCADE; +-- +-- CREATE TABLE IF NOT EXISTS contacts ( +-- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +-- phone_number TEXT NOT NULL, +-- user_id UUID NOT NULL, +-- first_name TEXT NULL, +-- last_name TEXT NULL, +-- nickname TEXT NULL +-- ); +-- +-- CREATE TABLE IF NOT EXISTS messages ( +-- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +-- content TEXT NOT NULL, +-- user_id UUID NOT NULL +-- ); +-- +-- CREATE TABLE IF NOT EXISTS scheduled_messages ( +-- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +-- scheduled timestamptz NOT NULL, +-- created timestamptz DEFAULT now(), +-- status TEXT CHECK (status IN ('PENDING', 'READY', 'PROCESSING', 'DONE')), +-- user_id UUID NOT NULL +-- ); +-- +-- CREATE TABLE IF NOT EXISTS scheduled_message_events ( +-- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +-- contact_id UUID NOT NULL, +-- message_id UUID NOT NULL, +-- scheduled_message_id UUID NOT NULL, +-- created timestamptz DEFAULT now() +-- ); +-- +-- CREATE TABLE IF NOT EXISTS message_event_responses ( +-- id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), +-- scheduled_message_event_id UUID NULL, +-- response JSONB NOT NULL, +-- user_id UUID NOT NULL, +-- sent timestamptz NOT NULL, +-- contact_id UUID NULL, +-- message_id UUID NULL, +-- status TEXT CHECK (status IN ('INSTANT', 'SCHEDULED')) +-- ); diff --git a/src/auth/mod.rs b/src/auth/mod.rs new file mode 100644 index 0000000..84b764e --- /dev/null +++ b/src/auth/mod.rs @@ -0,0 +1,61 @@ +use axum::{ + Json, + http::{Request, StatusCode}, + middleware::Next, + response::IntoResponse, +}; +use axum_extra::extract::cookie::CookieJar; +use jsonwebtoken::{DecodingKey, Validation, decode}; + +#[derive(Debug, serde::Serialize)] +pub struct ErrorResponse { + pub status: &'static str, + pub message: String, +} + +pub async fn auth( + cookie_jar: CookieJar, + req: Request, + next: Next, +) -> Result)> { + let token = cookie_jar + .get("token") + .map(|cookie| cookie.value().to_string()) + .or_else(|| { + req.headers() + .get(axum::http::header::AUTHORIZATION) + .and_then(|auth_header| auth_header.to_str().ok()) + .and_then(|auth_value| auth_value.strip_prefix("Bearer ").map(String::from)) + }); + + let token = token.ok_or_else(|| { + let json_error = ErrorResponse { + status: "fail", + message: "You are not logged in, please provide token".to_string(), + }; + (StatusCode::UNAUTHORIZED, Json(json_error)) + })?; + + let secret_key = textsender_models::envy::environment::get_secret_main_key() + .await + .value; + + let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256); + validation.set_audience(&["textsender"]); // Must match exactly what's in the token + let _claims = decode::( + &token, + &DecodingKey::from_secret(secret_key.as_ref()), + &validation, + ) + .map_err(|err| { + eprintln!("Error: {err:?}"); + let json_error = ErrorResponse { + status: "fail", + message: "Invalid token - Error decoding claims".to_string(), + }; + (StatusCode::UNAUTHORIZED, Json(json_error)) + })? + .claims; + + Ok(next.run(req).await) +} diff --git a/src/caller/mod.rs b/src/caller/mod.rs new file mode 100644 index 0000000..9839b1a --- /dev/null +++ b/src/caller/mod.rs @@ -0,0 +1,110 @@ +pub mod endpoints { + pub const ROOT: &str = "/"; + + /// Constant for adding Contact endpoint + pub const ADD_CONTACT: &str = "/api/v1/contact/new"; +} + +pub mod response { + pub const SUCCESSFUL: &str = "SUCCESSFUL"; +} + +/// Basic handler that responds with a static string +pub async fn root() -> &'static str { + "Hello, World!" +} + +pub mod contact { + pub mod request { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Deserialize, Serialize, ToSchema)] + pub struct AddContactRequest { + pub phone_number: String, + pub firstname: Option, + pub lastname: Option, + pub nickname: Option, + pub user_id: uuid::Uuid, + } + + impl AddContactRequest { + pub fn is_valid(&self) -> bool { + !self.phone_number.is_empty() || !self.user_id.is_nil() + } + } + } + + pub mod response { + use serde::{Deserialize, Serialize}; + use utoipa::ToSchema; + + #[derive(Debug, Default, Deserialize, Serialize, ToSchema)] + pub struct AddContactResponse { + pub message: String, + pub data: Vec, + } + } + + pub mod endpoint { + // use axum::{Json, response::IntoResponse}; + + use crate::repo::contact as contact_repo; + + /// Endpoint to create Contact + #[utoipa::path( + post, + path = super::super::endpoints::ADD_CONTACT, + request_body( + content = super::request::AddContactRequest, + description = "Data needed to create a Contact", + content_type = "application/json" + ), + responses( + (status = 201, description = "Contact created", body = super::response::AddContactResponse), + (status = 400, description = "Error", body = super::response::AddContactResponse), + (status = 500, description = "Error creating Contact", body = super::response::AddContactResponse) + ) + )] + pub async fn create_contact( + axum::Extension(pool): axum::Extension, + axum::Json(payload): axum::Json, + ) -> ( + axum::http::StatusCode, + axum::Json, + ) { + let mut response = super::response::AddContactResponse::default(); + + if payload.is_valid() { + let mut contact = textsender_models::contact::Contact { + firstname: payload.firstname, + lastname: payload.lastname, + phone_number: payload.phone_number, + user_id: Some(payload.user_id), + ..Default::default() + }; + match contact_repo::insert(&pool, &contact).await { + Ok(id) => { + contact.id = Some(id); + response.message = String::from(super::super::response::SUCCESSFUL); + response.data.push(contact); + + (axum::http::StatusCode::CREATED, axum::Json(response)) + } + Err(err) => { + eprintln!("Error: {err:?}"); + response.message = String::from("Contact not created"); + + ( + axum::http::StatusCode::INTERNAL_SERVER_ERROR, + axum::Json(response), + ) + } + } + } else { + response.message = String::from("Request body is not valid"); + (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)) + } + } + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..1bde5a1 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,118 @@ +pub mod host { + pub const PORT: i64 = 9081; + + pub fn get_full() -> String { + let address: &str = "0.0.0.0"; + format!("{}:{}", address, PORT) + } +} + +pub mod init { + use std::time::Duration; + + use axum::routing::post; + use tower_http::timeout::TimeoutLayer; + use utoipa::OpenApi; + + use crate::caller::contact as contact_caller; + use contact_caller::endpoint as contact_endpoints; + // use contact_caller::response as contact_responses; + + mod cors { + pub async fn configure_cors() -> tower_http::cors::CorsLayer { + let cors = tower_http::cors::CorsLayer::new() + .allow_methods([ + axum::http::Method::GET, + axum::http::Method::POST, + axum::http::Method::PUT, + axum::http::Method::DELETE, + ]) // Specify allowed methods:cite[2] + .allow_headers([ + axum::http::header::CONTENT_TYPE, + axum::http::header::AUTHORIZATION, + ]) // Specify allowed headers:cite[2] + .allow_credentials(true) // If you need to send cookies or authentication headers:cite[2] + .max_age(std::time::Duration::from_secs(3600)); // Cache the preflight response for 1 hour:cite[2] + + // Dynamically set the allowed origin based on the environment + match std::env::var(textsender_models::envy::keys::APP_ENV).as_deref() { + Ok("production") => { + // In production, allow only your specific, trusted origins + let allowed_origins_env = + textsender_models::envy::environment::get_allowed_origins().await; + match textsender_models::envy::utility::delimitize(&allowed_origins_env) { + Ok(alwd) => { + let allowed_origins: Vec = alwd + .into_iter() + .map(|a| a.parse::().unwrap()) + .collect(); + cors.allow_origin(allowed_origins) + } + Err(err) => { + eprintln!("Error getting allowed origins: Error: {err:?}"); + std::process::exit(-1); + } + } + } + _ => { + // Development (default): Allow localhost origins + cors.allow_origin(vec![ + format!("http://localhost:{}", super::super::host::PORT) + .parse() + .unwrap(), + format!("http://127.0.0.1:{}", super::super::host::PORT) + .parse() + .unwrap(), + "http://localhost:4200".parse().unwrap(), + "http://127.0.0.1:4200".parse().unwrap(), + ]) + } + } + } + } + + #[derive(utoipa::OpenApi)] + #[openapi( + paths(contact_endpoints::create_contact,), + components(schemas(contact_caller::response::AddContactResponse)), + tags( + (name = "textsender API", description = "Web API to manage texting") + ) + )] + struct ApiDoc; + + pub async fn routes() -> axum::Router { + axum::Router::new() + .route( + crate::caller::endpoints::ADD_CONTACT, + post(contact_endpoints::create_contact).route_layer(axum::middleware::from_fn( + crate::auth::auth::, + )), + ) + .layer(cors::configure_cors().await) + } + + pub async fn app() -> axum::Router { + let pool = crate::db::create_pool() + .await + .expect("Failed to create pool"); + + crate::db::migrations(&pool).await; + + let cors = cors::configure_cors().await; + + routes() + .await + .merge( + utoipa_swagger_ui::SwaggerUi::new("/swagger-ui") + .url("/api-docs/openapi.json", ApiDoc::openapi()), + ) + .layer(axum::Extension(pool)) + .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024)) + .layer(TimeoutLayer::with_status_code( + axum::http::StatusCode::OK, + Duration::from_secs(300), + )) + .layer(cors) + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..d6fe47d --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,26 @@ +use sqlx::postgres::PgPoolOptions; + +pub mod connection_settings { + pub const MAXCONN: u32 = 10; +} + +pub async fn create_pool() -> Result { + let database_url = textsender_models::envy::environment::get_db_url() + .await + .value; + println!("Database url: {database_url}"); + + PgPoolOptions::new() + .max_connections(connection_settings::MAXCONN) + .connect(&database_url) + .await +} + +pub async fn migrations(pool: &sqlx::PgPool) { + // Run migrations using the sqlx::migrate! macro + // Assumes your migrations are in a ./migrations folder relative to Cargo.toml + sqlx::migrate!("./migrations") + .run(pool) + .await + .expect("Failed to run migrations"); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..09f6e1f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod auth; +pub mod caller; +pub mod config; +pub mod db; +pub mod repo; diff --git a/src/main.rs b/src/main.rs index 47a4039..a6e5f5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,4 +2,15 @@ async fn main() { // initialize tracing tracing_subscriber::fmt::init(); + + match tokio::net::TcpListener::bind(textsender_api::config::host::get_full()).await { + Ok(listener) => { + // build our application with routes + let app = textsender_api::config::init::app().await; + axum::serve(listener, app).await.unwrap(); + } + Err(err) => { + eprintln!("Error: {err:?}") + } + } } diff --git a/src/repo/mod.rs b/src/repo/mod.rs new file mode 100644 index 0000000..7434ddb --- /dev/null +++ b/src/repo/mod.rs @@ -0,0 +1,29 @@ +pub mod contact { + use sqlx::Row; + + pub async fn insert( + pool: &sqlx::PgPool, + contact: &textsender_models::contact::Contact, + ) -> Result { + match sqlx::query( + r#" + INSERT INTO "contacts" (firstname, lastname, nickname, phone_number, user_id) + VALUES($1, $2, $3, $4, $5) RETURNING id; + "#, + ) + .bind(&contact.firstname) + .bind(&contact.lastname) + .bind(&contact.nickname) + .bind(&contact.phone_number) + .bind(contact.user_id) + .fetch_one(pool) + .await + { + Ok(row) => { + let id: uuid::Uuid = row.try_get("id")?; + Ok(id) + } + Err(_) => Err(sqlx::Error::RowNotFound), + } + } +} diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..257fe59 --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,230 @@ +// use std::io::Write; + +// use common_multipart_rfc7578::client::multipart::{Body as MultipartBody, Form as MultipartForm}; +// use tower::ServiceExt; + +use textsender_api::db; + +mod db_mgr { + use std::str::FromStr; + + pub const LIMIT: usize = 6; + + pub async fn get_pool() -> Result { + let tm_db_url = textsender_models::envy::environment::get_db_url() + .await + .value; + let tm_options = sqlx::postgres::PgConnectOptions::from_str(&tm_db_url).unwrap(); + sqlx::PgPool::connect_with(tm_options).await + } + + pub async fn generate_db_name() -> String { + let db_name = + get_database_name().await.unwrap() + &"_" + &uuid::Uuid::new_v4().to_string()[..LIMIT]; + db_name + } + + pub async fn connect_to_db(db_name: &str) -> Result { + let db_url = textsender_models::envy::environment::get_db_url() + .await + .value; + let options = sqlx::postgres::PgConnectOptions::from_str(&db_url)?.database(db_name); + sqlx::PgPool::connect_with(options).await + } + + pub async fn create_database( + template_pool: &sqlx::PgPool, + db_name: &str, + ) -> Result<(), sqlx::Error> { + let create_query = format!("CREATE DATABASE {}", db_name); + match sqlx::query(&create_query).execute(template_pool).await { + Ok(_) => Ok(()), + Err(e) => Err(e), + } + } + + // Function to drop a database + pub async fn drop_database( + template_pool: &sqlx::PgPool, + db_name: &str, + ) -> Result<(), sqlx::Error> { + let drop_query = format!("DROP DATABASE IF EXISTS {} WITH (FORCE)", db_name); + sqlx::query(&drop_query).execute(template_pool).await?; + Ok(()) + } + + pub async fn get_database_name() -> Result> { + let database_url = textsender_models::envy::environment::get_db_url() + .await + .value; + let parsed_url = url::Url::parse(&database_url)?; + + if parsed_url.scheme() == "postgres" || parsed_url.scheme() == "postgresql" { + match parsed_url + .path_segments() + .and_then(|segments| segments.last().map(|s| s.to_string())) + { + Some(sss) => Ok(sss), + None => Err("Error parsing".into()), + } + } else { + // Handle other database types if needed + Err("Error parsing".into()) + } + } + + /* + pub async fn migrations(pool: &sqlx::PgPool) { + // Run migrations using the sqlx::migrate! macro + // Assumes your test migrations are in a ./test_migrations folder relative to Cargo.toml + sqlx::migrate!("./migrations") + .run(pool) + .await + .expect("Failed to run migrations"); + } + */ +} + +mod init { + pub async fn app(pool: sqlx::PgPool) -> axum::Router { + textsender_api::config::init::routes() + .await + .layer(axum::Extension(pool)) + .layer(axum::extract::DefaultBodyLimit::max(1024 * 1024 * 1024)) + .layer(tower_http::timeout::TimeoutLayer::with_status_code( + axum::http::StatusCode::OK, + std::time::Duration::from_secs(300), + )) + } +} + +mod util { + pub async fn resp_to_bytes( + response: axum::response::Response, + ) -> Result { + axum::body::to_bytes(response.into_body(), std::usize::MAX).await + } + + pub async fn get_resp_data(response: axum::response::Response) -> Data + where + Data: for<'a> serde::Deserialize<'a>, + { + let body = resp_to_bytes(response).await.unwrap(); + serde_json::from_slice(&body).unwrap() + } + + /* + pub async fn format_url_with_value(endpoint: &str, value: &uuid::Uuid) -> String { + let last = endpoint.len() - 5; + format!("{}/{value}", &endpoint[0..last]) + } + */ +} + +pub fn token_fields() -> (String, String, String) { + ( + String::from("What a twist!"), + String::from("textsender_test"), + String::from("textsender"), + ) +} + +pub const TEST_USER_ID: uuid::Uuid = uuid::uuid!("cc938368-615a-4694-b2ca-6e122fa31c52"); + +pub async fn test_token() -> Result { + let key: String = textsender_models::envy::environment::get_secret_main_key() + .await + .value; + let (message, issuer, audience) = token_fields(); + + let token_resource = textsender_models::token::TokenResource { + message: message, + issuer: issuer, + audiences: vec![audience], + user_id: TEST_USER_ID, + }; + + match textsender_models::token::create_token(&key, &token_resource, time::Duration::hours(1)) { + Ok((access_token, _some_time)) => Ok(access_token), + Err(err) => Err(err), + } +} + +pub async fn bearer_auth() -> String { + let token = match test_token().await { + Ok(access_token) => access_token, + Err(err) => { + assert!(false, "Error: {err:?}"); + String::new() + } + }; + + format!("Bearer {token}") +} + +mod request { + // use common_multipart_rfc7578::client::multipart::{ + // Body as MultipartBody, Form as MultipartForm, + // }; + use tower::ServiceExt; + + pub async fn create_contact( + app: &axum::Router, + ) -> Result { + let payload = serde_json::json!({ + "phone_number": super::TEST_CONTACT_PHONE_NUMBER, + "user_id": super::TEST_USER_ID, + }); + + let req = axum::http::Request::builder() + .method(axum::http::Method::POST) + .uri(textsender_api::caller::endpoints::ADD_CONTACT) + .header(axum::http::header::CONTENT_TYPE, "application/json") + .header( + axum::http::header::AUTHORIZATION, + super::bearer_auth().await, + ) + .body(axum::body::Body::from(payload.to_string())) + .unwrap(); + app.clone().oneshot(req).await + } +} + +/// Test contact phone number +pub const TEST_CONTACT_PHONE_NUMBER: &str = "+10123456789"; + +#[tokio::test] +async fn test_create_contact() { + let tm_pool = db_mgr::get_pool().await.unwrap(); + let db_name = db_mgr::generate_db_name().await; + + match db_mgr::create_database(&tm_pool, &db_name).await { + Ok(_) => { + println!("Success"); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + } + + let pool = db_mgr::connect_to_db(&db_name).await.unwrap(); + db::migrations(&pool).await; + + let app = init::app(pool).await; + + // Send request + match request::create_contact(&app).await { + Ok(response) => { + let resp = util::get_resp_data::< + textsender_api::caller::contact::response::AddContactResponse, + >(response) + .await; + assert_eq!(false, resp.data.is_empty(), "Should not be empty"); + } + Err(err) => { + assert!(false, "Error: {:?}", err); + } + }; + + let _ = db_mgr::drop_database(&tm_pool, &db_name).await; +}