From 5862378dc192b30924664d8274c9d3471137610c Mon Sep 17 00:00:00 2001 From: Ada Conway Date: Sun, 9 Nov 2025 21:20:36 -0700 Subject: [PATCH] feat: user CRUD --- .gitignore | 5 +- .../middleware/authentication_middleware.go | 5 +- .../middleware/authorization_middleware.go | 39 +++++++ .../middleware/verification_middleware.go | 70 ++++++++++++ api/controllers/orderController.go | 1 + api/controllers/testController.go | 16 +++ api/controllers/userController.go | 103 ++++++++++++++++++ api/dto/refresh_token_request_body.go | 8 ++ api/dto/refresh_token_response.go | 10 ++ api/dto/user_profile_response.go | 11 ++ api/go.mod | 9 +- api/go.sum | 28 +++++ api/main.go | 74 +++++++++++++ api/queries/UserQueries.go | 21 ++++ db/schema initialization.sql | 4 +- 15 files changed, 399 insertions(+), 5 deletions(-) create mode 100644 api/auth/middleware/authorization_middleware.go create mode 100644 api/auth/middleware/verification_middleware.go create mode 100644 api/controllers/orderController.go create mode 100644 api/controllers/testController.go create mode 100644 api/controllers/userController.go create mode 100644 api/dto/refresh_token_request_body.go create mode 100644 api/dto/refresh_token_response.go create mode 100644 api/dto/user_profile_response.go create mode 100644 api/main.go create mode 100644 api/queries/UserQueries.go diff --git a/.gitignore b/.gitignore index 019f8e6..e7e990e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ # Log files and temporary data *.log -*.tmp \ No newline at end of file +*.tmp + +# dotenv +.env \ No newline at end of file diff --git a/api/auth/middleware/authentication_middleware.go b/api/auth/middleware/authentication_middleware.go index 31bc729..b00d99e 100644 --- a/api/auth/middleware/authentication_middleware.go +++ b/api/auth/middleware/authentication_middleware.go @@ -10,8 +10,8 @@ import ( "os" "time" - "git.conway.engineer/ada/ordr.git/auth" - "git.conway.engineer/ada/ordr.git/dto" + "ordr-api/auth" + "ordr-api/dto" "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" @@ -157,6 +157,7 @@ func IsAuthenticated(auth *auth.Authenticator) gin.HandlerFunc { } else { if !HandleRefreshToken(session) { context.String(http.StatusUnauthorized, "Failed to refresh access token") + return } else { context.Next() } diff --git a/api/auth/middleware/authorization_middleware.go b/api/auth/middleware/authorization_middleware.go new file mode 100644 index 0000000..ac50402 --- /dev/null +++ b/api/auth/middleware/authorization_middleware.go @@ -0,0 +1,39 @@ +package middleware + +import ( + "context" + "log" + "net/http" + "ordr-api/dto" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" +) + +func IsAdmin(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + + if conn_err != nil { + log.Println(conn_err) + ctx.AbortWithStatus(http.StatusInternalServerError) + } + + user_profile, _ := ctx.Get("user_profile") + + var is_admin bool + + query_err := conn.QueryRow(context.Background(), "SELECT is_admin FROM ordr_user WHERE sub_id = $1", user_profile.(dto.UserProfileResponse).Sub).Scan(&is_admin) + + if query_err != nil { + log.Println(query_err) + ctx.AbortWithStatus(http.StatusInternalServerError) + } + + if is_admin != true { + ctx.AbortWithStatus(http.StatusUnauthorized) + } + + ctx.Next() + } +} diff --git a/api/auth/middleware/verification_middleware.go b/api/auth/middleware/verification_middleware.go new file mode 100644 index 0000000..7e6b47f --- /dev/null +++ b/api/auth/middleware/verification_middleware.go @@ -0,0 +1,70 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" + + "context" + "log" + "net/http" + "ordr-api/dto" + "ordr-api/queries" +) + +func UserInDatabase(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("UserInDatabase(): ERROR: Failed to establish connection... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + } + defer conn.Release() + + user_profile, _ := ctx.Get("user_profile") + + sub_id := user_profile.(dto.UserProfileResponse).Sub + nickname := user_profile.(dto.UserProfileResponse).Nickname + + var count int + query_err := conn.QueryRow(context.Background(), "SELECT COUNT(id) FROM ordr_user WHERE sub_id = $1", sub_id).Scan(&count) + if query_err != nil { + log.Println("UserInDatabase(): ERROR Failed to query for user count") + ctx.AbortWithStatus(http.StatusInternalServerError) + } + + if count == 0 { + _, exec_err := conn.Exec(context.Background(), queries.USER_CREATE_QUERY, sub_id, nickname) + + if exec_err != nil { + log.Printf("UserInDatabase(): ERROR Failed to create user... %s", exec_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + } + } + } +} + +func UserIsActive(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("UserIsActive(): ERROR: Failed to establish connection... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + } + defer conn.Release() + + user_profile, _ := ctx.Get("user_profile") + + sub_id := user_profile.(dto.UserProfileResponse).Sub + + var active bool + query_err := conn.QueryRow(context.Background(), "SELECT active FROM ordr_user WHERE sub_id=$1", sub_id).Scan(&active) + if query_err != nil { + log.Printf("UserIsActive: ERROR: Failed to query user... %s", query_err.Error()) + } + + if !active { + ctx.AbortWithStatus(http.StatusUnauthorized) + } + ctx.Next() + } +} diff --git a/api/controllers/orderController.go b/api/controllers/orderController.go new file mode 100644 index 0000000..2d32936 --- /dev/null +++ b/api/controllers/orderController.go @@ -0,0 +1 @@ +package controllers diff --git a/api/controllers/testController.go b/api/controllers/testController.go new file mode 100644 index 0000000..6f85dcb --- /dev/null +++ b/api/controllers/testController.go @@ -0,0 +1,16 @@ +package controllers + +import ( + "net/http" + + "github.com/gin-gonic/gin" +) + +func BaseFunction(context *gin.Context) { + user_profile, _ := context.Get("user_profile") + context.IndentedJSON(http.StatusOK, user_profile) +} + +func PublicEndpoint(context *gin.Context) { + context.String(http.StatusOK, "Public endpoint for you to land at") +} diff --git a/api/controllers/userController.go b/api/controllers/userController.go new file mode 100644 index 0000000..42e16cf --- /dev/null +++ b/api/controllers/userController.go @@ -0,0 +1,103 @@ +package controllers + +import ( + "context" + "log" + "net/http" + "ordr-api/dto" + "ordr-api/queries" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" +) + +func CreateUser(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, err := pool.Acquire(ctx) + defer conn.Release() + if err != nil { + ctx.String(http.StatusInternalServerError, err.Error()) + } + user_profile, _ := ctx.Get("user_profile") + + user_name := ctx.Query("user_name") + + if user_name == "" { + log.Println("CreateUser(): ERROR: user name not supplied") + return + } + + updateCommandTag, update_err := conn.Exec(context.Background(), queries.USER_UPDATE_QUERY, user_name, user_profile.(dto.UserProfileResponse).Sub) + if update_err != nil { + log.Printf("%s", update_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + } + + if updateCommandTag.RowsAffected() == 0 { + _, query_err := conn.Exec(context.Background(), queries.USER_CREATE_QUERY, user_profile.(dto.UserProfileResponse).Sub, user_name) + if query_err != nil { + log.Printf("%s", query_err.Error()) + ctx.String(http.StatusInternalServerError, query_err.Error()) + } + } + } +} + +func PromoteUser(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + defer conn.Release() + if conn_err != nil { + ctx.String(http.StatusInternalServerError, conn_err.Error()) + } + + user_id := ctx.Query("user_id") + if user_id == "" { + ctx.String(http.StatusBadRequest, "PromoteUser(): ERROR Missing user id") + return + } + + _, update_err := conn.Exec(context.Background(), queries.USER_SET_IS_ADMIN_QUERY, user_id) + if update_err != nil { + ctx.String(http.StatusInternalServerError, update_err.Error()) + } + } +} + +func DemoteUser(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + ctx.String(http.StatusInternalServerError, conn_err.Error()) + return + } + defer conn.Release() + user_id := ctx.Query("user_id") + if user_id == "" { + ctx.String(http.StatusBadRequest, "ERROR: User Id Not Supplied") + return + } + _, exec_err := conn.Exec(context.Background(), queries.USER_REVOKE_ADMIN_QUERY, user_id) + if exec_err != nil { + ctx.String(http.StatusInternalServerError, exec_err.Error()) + } + } +} + +func DeactivateUser(pool *pgxpool.Pool) gin.HandlerFunc { + return func(ctx *gin.Context) { + conn, conn_err := pool.Acquire(ctx) + if conn_err != nil { + log.Printf("DeactivateUser(): ERROR: Failed to connect... %s", conn_err.Error()) + ctx.AbortWithStatus(http.StatusInternalServerError) + } + defer conn.Release() + + user_id := ctx.Query("user_id") + if user_id == "" { + ctx.String(http.StatusBadRequest, "DeactivateUser(): User id not supplied") + } + + conn.Exec(context.Background(), queries.USER_SET_INACTIVE_QUERY, user_id) + } +} diff --git a/api/dto/refresh_token_request_body.go b/api/dto/refresh_token_request_body.go new file mode 100644 index 0000000..25c36e2 --- /dev/null +++ b/api/dto/refresh_token_request_body.go @@ -0,0 +1,8 @@ +package dto + +type RefreshTokenRequest struct { + GrantType string `url:"grant_type"` + ClientId string `url:"client_id"` + ClientSecret string `url:"client_secret"` + RefreshToken string `url:"refresh_token"` +} diff --git a/api/dto/refresh_token_response.go b/api/dto/refresh_token_response.go new file mode 100644 index 0000000..335c9a6 --- /dev/null +++ b/api/dto/refresh_token_response.go @@ -0,0 +1,10 @@ +package dto + +type RefreshTokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` + Scope string `json:"scope"` + ExpiresIn int64 `json:"expires_in"` + TokenType string `json:"token_type"` +} diff --git a/api/dto/user_profile_response.go b/api/dto/user_profile_response.go new file mode 100644 index 0000000..73148f9 --- /dev/null +++ b/api/dto/user_profile_response.go @@ -0,0 +1,11 @@ +package dto + +type UserProfileResponse struct { + Sub string `json:"sub"` + Nickname string `json:"nickname"` + Name string `json:"name"` + PictureUrl string `json:"picture"` + Updated_at string `json:"updated_at"` + Email string `json:"email"` + Verified bool `json:"email_verified"` +} diff --git a/api/go.mod b/api/go.mod index 15c4988..2d37999 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,4 +1,4 @@ -module git.conway.engineer/ada/ordr.git/api +module ordr-api go 1.25.3 @@ -6,6 +6,10 @@ require ( github.com/coreos/go-oidc/v3 v3.16.0 github.com/gin-contrib/sessions v1.0.4 github.com/gin-gonic/gin v1.11.0 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/google/go-querystring v1.1.0 + github.com/jackc/pgx/v5 v5.7.6 + github.com/joho/godotenv v1.5.1 golang.org/x/oauth2 v0.33.0 ) @@ -24,6 +28,9 @@ require ( github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.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/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/api/go.sum b/api/go.sum index b96f182..72d891e 100644 --- a/api/go.sum +++ b/api/go.sum @@ -7,6 +7,7 @@ github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gE github.com/coreos/go-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow= github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= 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/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= @@ -18,6 +19,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +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= @@ -28,13 +31,32 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +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/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +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/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +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.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -50,6 +72,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= @@ -59,9 +82,12 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +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/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= @@ -87,8 +113,10 @@ golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/api/main.go b/api/main.go new file mode 100644 index 0000000..f44976f --- /dev/null +++ b/api/main.go @@ -0,0 +1,74 @@ +package main + +import ( + "context" + "encoding/gob" + "log" + "os" + + "ordr-api/auth" + "ordr-api/auth/middleware" + "ordr-api/controllers" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/joho/godotenv" +) + +func init_db_pool(databaseUrl string) (*pgxpool.Pool, error) { + config, err := pgxpool.ParseConfig(databaseUrl) + if err != nil { + log.Fatalf("Failed to load pgx pool config %s....", err) + } + + pool, err := pgxpool.NewWithConfig(context.Background(), config) + + err = pool.Ping(context.Background()) + if err != nil { + log.Fatalf("Unable to ping database: %v\n", err) + } + + return pool, err +} + +func main() { + if err := godotenv.Load(); err != nil { + log.Fatalf("Failed to load the env vars: %v", err) + } + + authenticator, auth_err := auth.New() + if auth_err != nil { + log.Fatal("ERROR: Failed to initialize Authenticator") + return + } + + pool, pool_err := init_db_pool(os.Getenv("CONNECTION_STRING")) + log.Printf("%s", pool) + + if pool_err != nil { + log.Fatal("ERROR: Failed to initialize DB pooL") + } + + router := gin.Default() + + store := cookie.NewStore([]byte(os.Getenv("COOKIE_SECRET"))) + router.Use(sessions.Sessions("auth-session", store)) + + gob.Register(map[string]interface{}{}) + router.GET("/", middleware.IsAuthenticated(authenticator), middleware.GetUserProfile, controllers.BaseFunction) + router.GET("/auth/login", auth.LoginHandler(authenticator)) + router.GET("/auth/logout", auth.LogoutHandler) + router.GET("/auth/logout_callback", auth.LogoutCallbackHandler(store)) + router.GET("/callback", auth.AuthenticationCallbackHandler(authenticator)) + router.GET("/public", controllers.PublicEndpoint) + + router.POST("/user/create", middleware.IsAuthenticated(authenticator), middleware.GetUserProfile, middleware.UserInDatabase(pool), middleware.UserIsActive(pool), controllers.CreateUser(pool)) + + router.PUT("/user/promote", middleware.IsAuthenticated(authenticator), middleware.GetUserProfile, middleware.UserInDatabase(pool), middleware.UserIsActive(pool), middleware.IsAdmin(pool), controllers.PromoteUser(pool)) + router.PUT("/user/demote", middleware.IsAuthenticated(authenticator), middleware.GetUserProfile, middleware.UserInDatabase(pool), middleware.UserIsActive(pool), middleware.IsAdmin(pool), controllers.DemoteUser(pool)) + router.DELETE("/user/deactivate", middleware.IsAuthenticated(authenticator), middleware.GetUserProfile, middleware.UserInDatabase(pool), middleware.UserIsActive(pool), middleware.IsAdmin(pool), controllers.DeactivateUser(pool)) + + router.Run("localhost:8080") +} diff --git a/api/queries/UserQueries.go b/api/queries/UserQueries.go new file mode 100644 index 0000000..376d890 --- /dev/null +++ b/api/queries/UserQueries.go @@ -0,0 +1,21 @@ +package queries + +const USER_CREATE_QUERY string = ` +INSERT INTO ordr_user(sub_id, user_name) VALUES ($1, $2); +` + +const USER_UPDATE_QUERY string = ` +UPDATE ordr_user SET user_name = $1 WHERE sub_id = $2; +` + +const USER_SET_IS_ADMIN_QUERY string = ` +UPDATE ordr_user SET is_admin = TRUE WHERE id = $1; +` + +const USER_REVOKE_ADMIN_QUERY string = ` +UPDATE ordr_user SET is_admin = FALSE WHERE id = $1; +` + +const USER_SET_INACTIVE_QUERY string = ` +UPDATE ordr_user SET active = FALSE WHERE id = $1; +` diff --git a/db/schema initialization.sql b/db/schema initialization.sql index 4b50223..27935b7 100644 --- a/db/schema initialization.sql +++ b/db/schema initialization.sql @@ -1,6 +1,8 @@ CREATE TABLE IF NOT EXISTS ordr_user ( id SERIAL NOT NULL PRIMARY KEY, - active BOOLEAN NOT NULL DEFAULT FALSE, + sub_id TEXT UNIQUE NOT NULL, + user_name TEXT NOT NULL, + active BOOLEAN NOT NULL DEFAULT TRUE, is_admin BOOLEAN NOT NULL DEFAULT FALSE );