diff --git a/.sqlc/migrations/20241108045258_create_users_table.sql b/.sqlc/migrations/20241108045258_create_users_table.sql new file mode 100644 index 0000000..c16fa28 --- /dev/null +++ b/.sqlc/migrations/20241108045258_create_users_table.sql @@ -0,0 +1,21 @@ +-- +goose Up +-- +goose StatementBegin +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + first_name VARCHAR(100), + last_name VARCHAR(100), + role VARCHAR(50) NOT NULL, + wallet_address VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +DROP TABLE users; +-- +goose StatementEnd \ No newline at end of file diff --git a/.sqlc/queries/users.sql b/.sqlc/queries/users.sql new file mode 100644 index 0000000..c22175f --- /dev/null +++ b/.sqlc/queries/users.sql @@ -0,0 +1,18 @@ +-- name: CreateUser :one +INSERT INTO users ( + email, + password_hash, + first_name, + last_name, + role +) VALUES ( + $1, $2, $3, $4, $5 +) RETURNING *; + +-- name: GetUserByEmail :one +SELECT * FROM users +WHERE email = $1 LIMIT 1; + +-- name: GetUserByID :one +SELECT * FROM users +WHERE id = $1 LIMIT 1; \ No newline at end of file diff --git a/db/models.go b/db/models.go index 972fa6f..4de993f 100644 --- a/db/models.go +++ b/db/models.go @@ -18,6 +18,18 @@ type Company struct { UpdatedAt pgtype.Timestamp } +type User struct { + ID pgtype.UUID + Email string + PasswordHash string + FirstName pgtype.Text + LastName pgtype.Text + Role string + WalletAddress pgtype.Text + CreatedAt pgtype.Timestamp + UpdatedAt pgtype.Timestamp +} + type ResourceRequest struct { ID pgtype.UUID CompanyID pgtype.UUID @@ -26,4 +38,4 @@ type ResourceRequest struct { Status string CreatedAt pgtype.Timestamp UpdatedAt pgtype.Timestamp -} +} \ No newline at end of file diff --git a/db/users.sql.go b/db/users.sql.go new file mode 100644 index 0000000..6ca964f --- /dev/null +++ b/db/users.sql.go @@ -0,0 +1,99 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: users.sql + +package db + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const createUser = `-- name: CreateUser :one +INSERT INTO users ( + email, + password_hash, + first_name, + last_name, + role +) VALUES ( + $1, $2, $3, $4, $5 +) RETURNING id, email, password_hash, first_name, last_name, role, wallet_address, created_at, updated_at +` + +type CreateUserParams struct { + Email string + PasswordHash string + FirstName pgtype.Text + LastName pgtype.Text + Role string +} + +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { + row := q.db.QueryRow(ctx, createUser, + arg.Email, + arg.PasswordHash, + arg.FirstName, + arg.LastName, + arg.Role, + ) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.PasswordHash, + &i.FirstName, + &i.LastName, + &i.Role, + &i.WalletAddress, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getUserByEmail = `-- name: GetUserByEmail :one +SELECT id, email, password_hash, first_name, last_name, role, wallet_address, created_at, updated_at FROM users +WHERE email = $1 LIMIT 1 +` + +func (q *Queries) GetUserByEmail(ctx context.Context, email string) (User, error) { + row := q.db.QueryRow(ctx, getUserByEmail, email) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.PasswordHash, + &i.FirstName, + &i.LastName, + &i.Role, + &i.WalletAddress, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const getUserByID = `-- name: GetUserByID :one +SELECT id, email, password_hash, first_name, last_name, role, wallet_address, created_at, updated_at FROM users +WHERE id = $1 LIMIT 1 +` + +func (q *Queries) GetUserByID(ctx context.Context, id pgtype.UUID) (User, error) { + row := q.db.QueryRow(ctx, getUserByID, id) + var i User + err := row.Scan( + &i.ID, + &i.Email, + &i.PasswordHash, + &i.FirstName, + &i.LastName, + &i.Role, + &i.WalletAddress, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} diff --git a/go.mod b/go.mod index 1603441..a63a46e 100644 --- a/go.mod +++ b/go.mod @@ -3,31 +3,39 @@ module github.com/KonferCA/NoKap go 1.23.2 require ( + github.com/go-playground/validator/v10 v10.22.1 + github.com/golang-jwt/jwt v3.2.2+incompatible github.com/jackc/pgx/v5 v5.7.1 github.com/joho/godotenv v1.5.1 github.com/labstack/echo/v4 v4.12.0 + github.com/rs/zerolog v1.33.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/crypto v0.27.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/pgtalk v1.5.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.22.1 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect + github.com/google/uuid v1.6.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/kr/text v0.2.0 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/rs/zerolog v1.33.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.27.0 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sync v0.8.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index d6b4a3f..3c06550 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,14 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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/emicklei/pgtalk v1.5.0 h1:qD2YUvB3VFZYxRmLKymlUrsVBgfXPec1SWkPFD58SQI= +github.com/emicklei/pgtalk v1.5.0/go.mod h1:Gl6hNbBV10II4aLKBxf4lQub6b2hxKD6NJ98P/PNML0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -13,6 +18,8 @@ github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaC github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +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= @@ -23,6 +30,10 @@ 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/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= @@ -38,6 +49,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D 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.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= @@ -66,6 +79,8 @@ golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 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/server/auth.go b/internal/server/auth.go new file mode 100644 index 0000000..b6dd3d6 --- /dev/null +++ b/internal/server/auth.go @@ -0,0 +1,116 @@ +package server + +import ( + "context" + "net/http" + + "github.com/KonferCA/NoKap/db" + "github.com/emicklei/pgtalk/convert" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v4" + "golang.org/x/crypto/bcrypt" +) + +func (s *Server) setupAuthRoutes() { + auth := s.apiV1.Group("/auth") + auth.POST("/signup", s.handleSignup) + auth.POST("/signin", s.handleSignin) +} + +func (s *Server) handleSignup(c echo.Context) error { + var req SignupRequest + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request body") + } + + if err := c.Validate(&req); err != nil { + return err + } + + ctx := context.Background() + existingUser, err := s.queries.GetUserByEmail(ctx, req.Email) + if err == nil && existingUser.ID.Valid { + return echo.NewHTTPError(http.StatusConflict, "email already registered") + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to hash password") + } + + user, err := s.queries.CreateUser(ctx, db.CreateUserParams{ + Email: req.Email, + PasswordHash: string(hashedPassword), + FirstName: pgtype.Text{String: req.FirstName, Valid: true}, + LastName: pgtype.Text{String: req.LastName, Valid: true}, + Role: req.Role, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to create user") + } + + userID := convert.UUIDToString(user.ID) + token, err := generateJWT(userID, user.Role) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate token") + } + + return c.JSON(http.StatusCreated, AuthResponse{ + Token: token, + User: User{ + ID: userID, + Email: user.Email, + FirstName: user.FirstName.String, + LastName: user.LastName.String, + Role: user.Role, + WalletAddress: getStringPtr(user.WalletAddress), + }, + }) +} + +func (s *Server) handleSignin(c echo.Context) error { + var req SigninRequest + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid request body") + } + + if err := c.Validate(&req); err != nil { + return err + } + + ctx := context.Background() + user, err := s.queries.GetUserByEmail(ctx, req.Email) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") + } + + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "invalid credentials") + } + + userID := convert.UUIDToString(user.ID) + token, err := generateJWT(userID, user.Role) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate token") + } + + return c.JSON(http.StatusOK, AuthResponse{ + Token: token, + User: User{ + ID: userID, + Email: user.Email, + FirstName: user.FirstName.String, + LastName: user.LastName.String, + Role: user.Role, + WalletAddress: getStringPtr(user.WalletAddress), + }, + }) +} + +// helper function to convert pgtype.Text to *string +func getStringPtr(t pgtype.Text) *string { + if !t.Valid { + return nil + } + return &t.String +} diff --git a/internal/server/auth_test.go b/internal/server/auth_test.go new file mode 100644 index 0000000..edf3b5b --- /dev/null +++ b/internal/server/auth_test.go @@ -0,0 +1,122 @@ +package server + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "testing" + + "context" + + "github.com/stretchr/testify/assert" +) + +func TestAuth(t *testing.T) { + // setup test environment + os.Setenv("JWT_SECRET", "test-secret") + os.Setenv("DB_HOST", "localhost") + os.Setenv("DB_PORT", "5432") + os.Setenv("DB_USER", "postgres") + os.Setenv("DB_PASSWORD", "postgres") + os.Setenv("DB_NAME", "postgres") + os.Setenv("DB_SSLMODE", "disable") + + // create server + s, err := New() + if err != nil { + t.Fatalf("failed to create server: %v", err) + } + defer s.DBPool.Close() + + // clean up database before tests + ctx := context.Background() + _, err = s.DBPool.Exec(ctx, "DELETE FROM users WHERE email = $1", "test@example.com") + if err != nil { + t.Fatalf("failed to clean up database: %v", err) + } + + // test signup + t.Run("signup", func(t *testing.T) { + payload := SignupRequest{ + Email: "test@example.com", + Password: "password123", + FirstName: "Test", + LastName: "User", + Role: "startup_owner", + } + body, _ := json.Marshal(payload) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signup", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + s.echoInstance.ServeHTTP(rec, req) + + assert.Equal(t, http.StatusCreated, rec.Code) + + var response AuthResponse + err := json.NewDecoder(rec.Body).Decode(&response) + assert.NoError(t, err) + assert.NotEmpty(t, response.Token) + assert.Equal(t, payload.Email, response.User.Email) + }) + + // test duplicate email + t.Run("duplicate email", func(t *testing.T) { + payload := SignupRequest{ + Email: "test@example.com", + Password: "password123", + FirstName: "Test", + LastName: "User", + Role: "startup_owner", + } + body, _ := json.Marshal(payload) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signup", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + s.echoInstance.ServeHTTP(rec, req) + assert.Equal(t, http.StatusConflict, rec.Code) + }) + + // test signin + t.Run("signin", func(t *testing.T) { + payload := SigninRequest{ + Email: "test@example.com", + Password: "password123", + } + body, _ := json.Marshal(payload) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signin", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + s.echoInstance.ServeHTTP(rec, req) + assert.Equal(t, http.StatusOK, rec.Code) + + var response AuthResponse + err := json.Unmarshal(rec.Body.Bytes(), &response) + assert.NoError(t, err) + assert.NotEmpty(t, response.Token) + assert.Equal(t, payload.Email, response.User.Email) + }) + + // test invalid credentials + t.Run("invalid credentials", func(t *testing.T) { + payload := SigninRequest{ + Email: "test@example.com", + Password: "wrongpassword", + } + body, _ := json.Marshal(payload) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/signin", bytes.NewReader(body)) + req.Header.Set("Content-Type", "application/json") + rec := httptest.NewRecorder() + + s.echoInstance.ServeHTTP(rec, req) + assert.Equal(t, http.StatusUnauthorized, rec.Code) + }) +} diff --git a/internal/server/index.go b/internal/server/index.go index ce10642..f4f84a6 100644 --- a/internal/server/index.go +++ b/internal/server/index.go @@ -14,6 +14,7 @@ import ( type Server struct { DBPool *pgxpool.Pool + queries *db.Queries echoInstance *echo.Echo apiV1 *echo.Group } @@ -35,6 +36,9 @@ func New() (*Server, error) { return nil, err } + // Initialize queries + queries := db.New(pool) + e := echo.New() e.Use(middleware.Logger()) @@ -46,11 +50,13 @@ func New() (*Server, error) { server := &Server{ DBPool: pool, + queries: queries, echoInstance: e, } // setup api routes server.setupV1Routes() + server.setupAuthRoutes() server.setupCompanyRoutes() server.setupResourceRequestRoutes() server.setupHealthRoutes() diff --git a/internal/server/jwt.go b/internal/server/jwt.go new file mode 100644 index 0000000..1d82ffa --- /dev/null +++ b/internal/server/jwt.go @@ -0,0 +1,28 @@ +package server + +import ( + "os" + "time" + + "github.com/golang-jwt/jwt" +) + +type JWTClaims struct { + UserID string `json:"user_id"` + Role string `json:"role"` + jwt.StandardClaims +} + +func generateJWT(userID string, role string) (string, error) { + claims := JWTClaims{ + UserID: userID, + Role: role, + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(24 * time.Hour).Unix(), + IssuedAt: time.Now().Unix(), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + return token.SignedString([]byte(os.Getenv("JWT_SECRET"))) +} diff --git a/internal/server/startup.go b/internal/server/startup.go new file mode 100644 index 0000000..1d88579 --- /dev/null +++ b/internal/server/startup.go @@ -0,0 +1,51 @@ +package server + +import ( + "context" + "net/http" + + "github.com/KonferCA/NoKap/db" + "github.com/jackc/pgx/v5/pgtype" + "github.com/labstack/echo/v4" +) + +func (s *Server) handleCreateStartup(c echo.Context) error { + var req CreateCompanyRequest + if err := c.Bind(&req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid request body :(") + } + + if err := c.Validate(req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + + var ownerUUID pgtype.UUID + if err := ownerUUID.Scan(req.OwnerUserID); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "invalid owner ID format") + } + + queries := db.New(s.DBPool) + params := db.CreateCompanyParams{ + OwnerUserID: ownerUUID, + Name: req.Name, + Description: pgtype.Text{String: req.Description, Valid: true}, + } + + company, err := queries.CreateCompany(context.Background(), params) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to create company :(") + } + + return c.JSON(http.StatusCreated, company) +} + +func (s *Server) handleGetStartup(c echo.Context) error { + queries := db.New(s.DBPool) + + companies, err := queries.ListCompanies(context.Background()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch companies :(") + } + + return c.JSON(http.StatusOK, companies) +} diff --git a/internal/server/types.go b/internal/server/types.go index 952f601..06c11a1 100644 --- a/internal/server/types.go +++ b/internal/server/types.go @@ -61,3 +61,30 @@ func (cv *CustomValidator) Validate(i interface{}) error { return nil } + +type SignupRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required,min=8"` + FirstName string `json:"first_name" validate:"required"` + LastName string `json:"last_name" validate:"required"` + Role string `json:"role" validate:"required,oneof=startup_owner admin investor"` +} + +type SigninRequest struct { + Email string `json:"email" validate:"required,email"` + Password string `json:"password" validate:"required"` +} + +type AuthResponse struct { + Token string `json:"token"` + User User `json:"user"` +} + +type User struct { + ID string `json:"id"` + Email string `json:"email"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Role string `json:"role"` + WalletAddress *string `json:"wallet_address,omitempty"` +}