diff --git a/.gitea/workflows/workflow.yaml b/.gitea/workflows/workflow.yaml index 13984c1..1685be2 100644 --- a/.gitea/workflows/workflow.yaml +++ b/.gitea/workflows/workflow.yaml @@ -107,20 +107,22 @@ jobs: echo "DB_PORT=$DB_PORT" >> .env echo "DB_SSLMODE=$DB_SSLMODE" >> .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 ${{ vars.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 }} + EOF + go test -v ./... - -# - name: Run gofmt (optional) - # Uncomment to check code formatting with gofmt -# run: | -# if [ -n "$(gofmt -l.)" ]; then -# echo "Go code is not formatted. Please run 'gofmt -w.' to fix it." -# exit 1 -# fi - -# - name: Run golint (optional) - # Uncomment to check code style with golint -# run: | -# if [ -n "$(golint./...)" ]; then -# echo "Go code has style issues. Please run 'golint./...' to see them." -# exit 1 -# fi diff --git a/cmd/api/main.go b/cmd/api/main.go index 07917a9..ef81b04 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -21,6 +21,7 @@ import ( "git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint" mdleware "git.kundeng.us/phoenix/textsender-auth/internal/middleware" "git.kundeng.us/phoenix/textsender-auth/internal/model" + "git.kundeng.us/phoenix/textsender-auth/internal/store" ) // @title textsender-auth @@ -73,8 +74,11 @@ func main() { // Services userStore := model.NewUserStore(db.Pool) + serviceStore := store.NewServiceStore(db.Pool) + userHandler := handler.NewUserHandler(userStore) loginHandler := handler.NewLoginHandler(userStore) + serviceHandler := handler.NewServiceHandler(serviceStore) router := chi.NewRouter() @@ -85,6 +89,7 @@ func main() { router.Method("Post", endpoint.Register, http.HandlerFunc(userHandler.Register)) router.Method("Post", endpoint.Login, http.HandlerFunc(loginHandler.Login)) + router.Method("Post", endpoint.CreateServiceUser, http.HandlerFunc(serviceHandler.Register)) router.Method("GET", "/swagger/*", httpSwagger.Handler( httpSwagger.URL(fmt.Sprintf("http://localhost:%s/swagger/doc.json", config.Port)), diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go index 1a70e67..8ec80e2 100644 --- a/cmd/api/main_test.go +++ b/cmd/api/main_test.go @@ -16,6 +16,7 @@ import ( "git.kundeng.us/phoenix/textsender-auth/internal/handler" "git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint" "git.kundeng.us/phoenix/textsender-auth/internal/model" + "git.kundeng.us/phoenix/textsender-auth/internal/store" ) var testRouter *mux.Router @@ -23,27 +24,30 @@ var testRouter *mux.Router func TestMain(m *testing.M) { cfg := load() - db, err := db.NewDatabase(cfg.GetDBConnString()) + database, err := db.NewDatabase(cfg.GetDBConnString()) if err != nil { fmt.Println(err.Error()) panic("Failed to initialize database") } - defer db.Close() + defer database.Close() ctx := context.Background() - err = db.ResetDatabase(ctx) + err = database.ResetDatabase(ctx) if err != nil { fmt.Println(err.Error()) panic("Failed to initialize database") } - userStore := model.NewUserStore(db.Pool) + userStore := model.NewUserStore(database.Pool) + serviceStore := store.NewServiceStore(database.Pool) userHandler := handler.NewUserHandler(userStore) loginHandler := handler.NewLoginHandler(userStore) + serviceHandler := handler.NewServiceHandler(serviceStore) testRouter = mux.NewRouter() testRouter.HandleFunc(endpoint.Register, userHandler.Register).Methods("POST") testRouter.HandleFunc(endpoint.Login, loginHandler.Login).Methods("POST") + testRouter.HandleFunc(endpoint.CreateServiceUser, serviceHandler.Register).Methods("POST") code := m.Run() os.Exit(code) diff --git a/docs/docs.go b/docs/docs.go index 6a7e528..3ffc603 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -116,6 +116,57 @@ const docTemplate = `{ } } } + }, + "/service/register": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a service user that can send texts (requires JWT)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "service users" + ], + "summary": "Register service user", + "parameters": [ + { + "description": "Data to add user", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ServiceCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ServiceCreationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ServiceCreationResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ServiceCreationResponse" + } + } + } + } } }, "definitions": { @@ -186,6 +237,31 @@ const docTemplate = `{ } } }, + "handler.ServiceCreationRequest": { + "type": "object", + "properties": { + "passphrase": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.ServiceCreationResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/user.ServiceUser" + } + }, + "message": { + "type": "string" + } + } + }, "token.Login": { "type": "object", "properties": { @@ -199,6 +275,23 @@ const docTemplate = `{ "type": "string" } } + }, + "user.ServiceUser": { + "type": "object", + "properties": { + "date_created": { + "type": "string" + }, + "id": { + "type": "string" + }, + "passphrase": { + "type": "string" + }, + "username": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.json b/docs/swagger.json index d06ced4..922c622 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -110,6 +110,57 @@ } } } + }, + "/service/register": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "Create a service user that can send texts (requires JWT)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "service users" + ], + "summary": "Register service user", + "parameters": [ + { + "description": "Data to add user", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ServiceCreationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/handler.ServiceCreationResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/handler.ServiceCreationResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/handler.ServiceCreationResponse" + } + } + } + } } }, "definitions": { @@ -180,6 +231,31 @@ } } }, + "handler.ServiceCreationRequest": { + "type": "object", + "properties": { + "passphrase": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.ServiceCreationResponse": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/user.ServiceUser" + } + }, + "message": { + "type": "string" + } + } + }, "token.Login": { "type": "object", "properties": { @@ -193,6 +269,23 @@ "type": "string" } } + }, + "user.ServiceUser": { + "type": "object", + "properties": { + "date_created": { + "type": "string" + }, + "id": { + "type": "string" + }, + "passphrase": { + "type": "string" + }, + "username": { + "type": "string" + } + } } }, "securityDefinitions": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 774e7f3..1ef23cb 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -43,6 +43,22 @@ definitions: username: type: string type: object + handler.ServiceCreationRequest: + properties: + passphrase: + type: string + username: + type: string + type: object + handler.ServiceCreationResponse: + properties: + data: + items: + $ref: '#/definitions/user.ServiceUser' + type: array + message: + type: string + type: object token.Login: properties: access_token: @@ -52,6 +68,17 @@ definitions: token_type: type: string type: object + user.ServiceUser: + properties: + date_created: + type: string + id: + type: string + passphrase: + type: string + username: + type: string + type: object host: localhost:9080 info: contact: {} @@ -123,6 +150,38 @@ paths: summary: Register user tags: - users + /service/register: + post: + consumes: + - application/json + description: Create a service user that can send texts (requires JWT) + parameters: + - description: Data to add user + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ServiceCreationRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/handler.ServiceCreationResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/handler.ServiceCreationResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/handler.ServiceCreationResponse' + security: + - BearerAuth: [] + summary: Register service user + tags: + - service users securityDefinitions: BearerAuth: description: JWT Bearer Token diff --git a/go.mod b/go.mod index 3a91d7d..b3bd713 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.kundeng.us/phoenix/textsender-auth go 1.25.3 require ( - git.kundeng.us/phoenix/textsender-models v0.0.6-main-28b29802b9-556 + git.kundeng.us/phoenix/textsender-models v0.0.8-main-3ab81c2b6d-556 github.com/go-chi/chi/v5 v5.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 github.com/google/uuid v1.6.0 diff --git a/go.sum b/go.sum index 2f1f7ba..83064da 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.kundeng.us/phoenix/textsender-models v0.0.6-main-28b29802b9-556 h1:2NI8cXaxt2rsXrhy/rDhgoOzvjLQwbhwr4nv2g35Ah8= -git.kundeng.us/phoenix/textsender-models v0.0.6-main-28b29802b9-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no= +git.kundeng.us/phoenix/textsender-models v0.0.8-main-3ab81c2b6d-556 h1:iVE8Dg3aYugoZseneN9tQtdV2cymukspShF7dfExSVw= +git.kundeng.us/phoenix/textsender-models v0.0.8-main-3ab81c2b6d-556/go.mod h1:lx5MCnOgGgsdpwzrfi9uph5xmkeb6H8AuexUNGss2no= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= diff --git a/internal/handler/endpoint/endpoint.go b/internal/handler/endpoint/endpoint.go index 88b9582..2cb8b3b 100644 --- a/internal/handler/endpoint/endpoint.go +++ b/internal/handler/endpoint/endpoint.go @@ -3,3 +3,4 @@ package endpoint // Endpoint for registering a user const Register = "/api/v1/register" const Login = "/api/v1/login" +const CreateServiceUser = "/api/v1/service/register" diff --git a/internal/handler/login_test.go b/internal/handler/login_test.go index 491763b..fd000cf 100644 --- a/internal/handler/login_test.go +++ b/internal/handler/login_test.go @@ -12,11 +12,12 @@ import ( "github.com/stretchr/testify/assert" "git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint" + "git.kundeng.us/phoenix/textsender-auth/internal/store/mock" "git.kundeng.us/phoenix/textsender-auth/internal/utility" ) func TestLogin(t *testing.T) { - mockstore := NewMockUserStore() + mockstore := mock.NewMockUserStore() handler := NewLoginHandler(mockstore) testUser := GetTestUser() diff --git a/internal/handler/register_test.go b/internal/handler/register_test.go index 24d2a07..fef906b 100644 --- a/internal/handler/register_test.go +++ b/internal/handler/register_test.go @@ -10,10 +10,11 @@ import ( "github.com/stretchr/testify/assert" "git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint" + "git.kundeng.us/phoenix/textsender-auth/internal/store/mock" ) func TestCreateUserWithMock(t *testing.T) { - mockstore := NewMockUserStore() + mockstore := mock.NewMockUserStore() handler := NewUserHandler(mockstore) testUser := GetTestUser() diff --git a/internal/handler/service.go b/internal/handler/service.go new file mode 100644 index 0000000..8745be6 --- /dev/null +++ b/internal/handler/service.go @@ -0,0 +1,82 @@ +package handler + +import ( + "net/http" + + "git.kundeng.us/phoenix/textsender-models/pkg/user" + + "git.kundeng.us/phoenix/textsender-auth/internal/store" + "git.kundeng.us/phoenix/textsender-auth/internal/utility" +) + +type ServiceCreationRequest struct { + Username string `json:"username"` + Passphrase string `json:"passphrase"` +} + +type ServiceCreationResponse struct { + Message string `json:"message"` + Data []*user.ServiceUser `json:"data"` +} + +type ServiceHandler struct { + ServiceStore store.ServiceStore +} + +func NewServiceHandler(serviceStore store.ServiceStore) *ServiceHandler { + return &ServiceHandler{ServiceStore: serviceStore} +} + +// Register godoc +// @Summary Register service user +// @Description Create a service user that can send texts (requires JWT) +// @Tags service users +// @Accept json +// @Produce json +// @Security BearerAuth +// @Param request body ServiceCreationRequest true "Data to add user" +// @Success 200 {object} ServiceCreationResponse +// @Failure 400 {object} ServiceCreationResponse +// @Failure 500 {object} ServiceCreationResponse +// @Router /service/register [post] +func (s *ServiceHandler) Register(w http.ResponseWriter, r *http.Request) { + var req ServiceCreationRequest + if err := ExtractFromRequest(r, &req); err != nil { + http.Error(w, "Invalid JSON: "+err.Error(), http.StatusBadRequest) + return + } + + defer r.Body.Close() + + var statusCode int + var resp ServiceCreationResponse + + ctx := r.Context() + if exists, err := s.ServiceStore.CheckWithUsername(ctx, req.Username); err != nil { + statusCode = http.StatusInternalServerError + resp.Message = err.Error() + } else { + if exists { + statusCode = http.StatusBadRequest + resp.Message = "Service user already exists" + } else { + hashing := utility.HashMash{Password: req.Passphrase} + if hashedPassword, err := hashing.HashPassword(); err != nil { + statusCode = http.StatusInternalServerError + resp.Message = err.Error() + } else { + serviceUser := user.ServiceUser{Username: req.Username, Passphrase: hashedPassword} + if err := s.ServiceStore.Create(ctx, &serviceUser); err != nil { + statusCode = http.StatusInternalServerError + resp.Message = err.Error() + } else { + statusCode = http.StatusCreated + resp.Message = "Successful" + resp.Data = append(resp.Data, &serviceUser) + } + } + } + } + + RespondWithJson(w, statusCode, &resp) +} diff --git a/internal/handler/service_test.go b/internal/handler/service_test.go new file mode 100644 index 0000000..5fe737b --- /dev/null +++ b/internal/handler/service_test.go @@ -0,0 +1,33 @@ +package handler + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "git.kundeng.us/phoenix/textsender-auth/internal/handler/endpoint" + "git.kundeng.us/phoenix/textsender-auth/internal/store/mock" +) + +func TestCreateServiceUserWithMock(t *testing.T) { + mockStore := mock.NewMockServiceUserStore() + handler := NewServiceHandler(mockStore) + + testService := ServiceCreationRequest{Username: "swoon", Passphrase: "ewrewr329n12y3x2!2"} + jsonValue, err := json.Marshal(testService) + assert.NoError(t, err, "Error marshaling request") + + req, _ := http.NewRequest("POST", endpoint.CreateServiceUser, strings.NewReader(string(jsonValue))) + rr := httptest.NewRecorder() + + handler.Register(rr, req) + assert.Equal(t, http.StatusCreated, rr.Code) + + var response ServiceCreationResponse + err = json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) +} diff --git a/internal/store/mock/service_store.go b/internal/store/mock/service_store.go new file mode 100644 index 0000000..fb7eb70 --- /dev/null +++ b/internal/store/mock/service_store.go @@ -0,0 +1,74 @@ +package mock + +import ( + "context" + "errors" + "sync" + + "github.com/google/uuid" + + "git.kundeng.us/phoenix/textsender-models/pkg/user" +) + +type MockServiceUserStore struct { + ServiceUsers map[uuid.UUID]*user.ServiceUser + ServiceUsersByUsername map[string]*user.ServiceUser + mu sync.RWMutex + Error error // Optional: simulate errors +} + +func NewMockServiceUserStore() *MockServiceUserStore { + return &MockServiceUserStore{ + ServiceUsers: make(map[uuid.UUID]*user.ServiceUser), + ServiceUsersByUsername: make(map[string]*user.ServiceUser), + } +} + +func (m *MockServiceUserStore) Create(ctx context.Context, user *user.ServiceUser) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return m.Error + } + + if user.Id == uuid.Nil { + user.Id = uuid.New() + } + + if _, exists := m.ServiceUsersByUsername[user.Username]; exists { + return errors.New("service User with username already exists") + } + + m.ServiceUsers[user.Id] = user + m.ServiceUsersByUsername[user.Username] = user + return nil +} + +func (m *MockServiceUserStore) CheckWithUsername(ctx context.Context, username string) (bool, error) { + m.mu.Lock() + defer m.mu.Unlock() + + if m.Error != nil { + return false, m.Error + } + + if m.Error != nil { + return false, m.Error + } + + var exists bool + + for _, serviceUser := range m.ServiceUsers { + if serviceUser.Username == username { + exists = true + break + } + } + + if !exists { + return exists, nil + } else { + return exists, nil + } +} diff --git a/internal/handler/mock_store.go b/internal/store/mock/store.go similarity index 99% rename from internal/handler/mock_store.go rename to internal/store/mock/store.go index 8f21de3..aca8169 100644 --- a/internal/handler/mock_store.go +++ b/internal/store/mock/store.go @@ -1,4 +1,4 @@ -package handler +package mock import ( "context" diff --git a/internal/store/service.go b/internal/store/service.go new file mode 100644 index 0000000..8a08ba2 --- /dev/null +++ b/internal/store/service.go @@ -0,0 +1,50 @@ +package store + +import ( + "context" + "fmt" + + "git.kundeng.us/phoenix/textsender-models/pkg/user" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type ServiceStore interface { + CheckWithUsername(ctx context.Context, username string) (bool, error) + Create(ctx context.Context, serviceUser *user.ServiceUser) error +} + +type PGServiceStore struct { + db *pgxpool.Pool +} + +func NewServiceStore(db *pgxpool.Pool) *PGServiceStore { + return &PGServiceStore{db: db} +} + +func (s *PGServiceStore) CheckWithUsername(ctx context.Context, username string) (bool, error) { + var exists bool + query := `SELECT EXISTS(SELECT 1 FROM service_users WHERE Username = $1)` + + if err := s.db.QueryRow(ctx, query, username).Scan(&exists); err != nil { + if err == pgx.ErrNoRows { + return exists, nil + } else { + return exists, fmt.Errorf("Error querying row: %v", err) + } + } else { + return exists, nil + } +} + +func (s *PGServiceStore) Create(ctx context.Context, serviceUser *user.ServiceUser) error { + query := ` + INSERT INTO service_users (username, passphrase) + VALUES ($1, $2) + RETURNING id, created + ` + + return s.db.QueryRow(ctx, query, serviceUser.Username, serviceUser.Passphrase).Scan( + &serviceUser.Id, &serviceUser.Created, + ) +} diff --git a/migrations/schema.sql b/migrations/schema.sql index fdb9caf..acfd1c1 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -1,6 +1,7 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; DROP TABLE IF EXISTS users CASCADE; +DROP TABLE IF EXISTS service_users CASCADE; CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -9,3 +10,9 @@ CREATE TABLE users ( password TEXT NOT NULL ); +CREATE TABLE service_users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + username TEXT NOT NULL, + passphrase TEXT NOT NULL, + created timestamptz DEFAULT now() +);