diff --git a/.gitignore b/.gitignore index 2fb241b..e7dc997 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules tmp .env +api diff --git a/README.md b/README.md index 05e87fa..33f020c 100644 --- a/README.md +++ b/README.md @@ -14,13 +14,14 @@ Check the [API documentation](http://localhost:4000) for more information (for n - Air - docker compose - migrate +- Just ### Onboarding script ```bash docker compose up -d -migrate -path=migrations -database "postgresql://postgres:postgres@0.0.0.0:5432/gists?sslmode=disable" -verbose up -air +just migrate +just dev ``` ## Installation @@ -52,17 +53,21 @@ MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" SMTP_HOST="" +APP_KEY="" ``` -4. Run the server +4. Run the server in development mode ```bash +just dev +# or air ``` ## Configuration -All the configuration is done through env variables : +All the configuration is done through env variables : + - `PORT` : the port on which your web server runs - `PG_USER` : the postgres user - `PG_PASSWORD` : the postgres password @@ -75,13 +80,20 @@ All the configuration is done through env variables : - `GOOGLE_SECRET` : your google client secret for OAUTH2 - `GITHUB_KEY` : your github client key for OAUTH2 - `GITHUB_SECRET` : your github client secret for OAUTH2 +- `MAIL_SMTP` : your smtp server +- `MAIL_PASSWORD` : your smtp password +- `SMTP_PORT` : your smtp port +- `SMTP_HOST` : your smtp host +- `APP_KEY` : your app key, which is a random string that is used to encrypt access tokens ## Tests To run tests, execute: ```bash -go test . +just test +# or to run all tests +just test-all ``` ## Migrations @@ -96,15 +108,8 @@ migrate create -ext=sql -dir=migrations -seq init To run the existing migrations locally : -### With bash - ```bash +just migrate +# or migrate -path=migrations -database "postgresql://postgres:postgres@0.0.0.0:5432/gists?sslmode=disable" -verbose up ``` - -### With go - -```bash -go build main.go -./main migrate -``` diff --git a/docs/openapi.json b/docs/openapi.json index 2ca6b45..8a8a7c7 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.1","info":{"title":"Gists","description":"","version":"1.0.0"},"tags":[],"paths":{"/gists":{"post":{"summary":"Create a gist","deprecated":false,"description":"Create a gist and link it to an organization as an option","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"name"},"content":{"type":"string"},"org_id":{"type":"string"}},"required":["name","content"]},"example":{"name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat."}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gist"},"examples":{"1":{"summary":"Success","value":{"id":"10","name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat.","owner_id":"4"}}}}}}},"security":[{"bearer":[]}]},"get":{"summary":"Get all gists","deprecated":false,"description":"Retrieve all your gists","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Gist"}},"examples":{"1":{"summary":"Fetch all gists","value":[{"id":"4","name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat.","owner_id":"4","org_id":"3"},{"id":"9","name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat.","owner_id":"4"}]}}}}}},"security":[{"bearer":[]}]}},"/gists/{id}/name":{"patch":{"summary":"Update gist's name","deprecated":false,"description":"","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"1","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"name"}},"required":["name"]},"example":{"name":"doloremque dolorum nobis"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"string"}}}}},"security":[{"bearer":[]}]}},"/gists/{id}/content":{"patch":{"summary":"Update gist's content","deprecated":false,"description":"","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"1","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"name"}},"required":["name"]},"example":{"content":"ezaeeza"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"string"}}}}},"security":[{"bearer":[]}]}},"/gists/{id}":{"get":{"summary":"Get a gist","deprecated":false,"description":"","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"4","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gist"}}}}},"security":[{"bearer":[]}]},"delete":{"summary":"Delete a gist","deprecated":false,"description":"Delete a gist","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"1","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"*/*":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":"Gist deleted successfully"}}}}}},"security":[{"bearer":[]}]}},"/auth/local/begin":{"post":{"summary":"Authenticate to the application wth a token","deprecated":false,"description":"Request a one time code by email and get registered or authenticated automatically","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}},"example":{"email":"radulescutristan@gmail.com"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"email":"radulescutristan@gmail.com"}}}}}}},"security":[]}},"/auth/local/verify":{"post":{"summary":"Confirm local authentication request","deprecated":false,"description":"Once you received your one time code, get your auth cookie with this route. It will be named `gists.access_token`","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"token":{"type":"string"}},"required":["email","token"]},"example":{"email":"radulescutristan@gmail.com","token":"234244"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"message":"You are now logged in"}}}}}}},"security":[]}},"/auth/google":{"get":{"summary":"Authenticate with google","deprecated":false,"description":"Authenticate with google, and get redirected directly to gists's frontend","tags":[],"parameters":[],"responses":{"302":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}},"security":[]}},"/auth/github":{"get":{"summary":"Authenticate with github","deprecated":false,"description":"Authenticate with github, and get redirected directly to gists's frontend","tags":[],"parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}},"security":[]}},"/orgs":{"post":{"summary":"Create an organization","deprecated":false,"description":"Create an organization by providing its name","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]},"example":{"name":"Test organization 2"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"id":"8","name":"Test organization 2"}}}}}}},"security":[{"bearer":[]}]}},"/orgs/":{"get":{"summary":"get all orgs of user","deprecated":false,"description":"Get all your organizations (not the gists)","tags":[],"parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":[{"id":"3","name":"Test organization 2"},{"id":"8","name":"Test organization 2"}]}}}}}},"security":[{"bearer":[]}]}},"/orgs/3":{"get":{"summary":"Get all gists ids from organization","deprecated":false,"description":"Get all the gists created in your organization.","tags":[],"parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"id":"3","name":"Test organization 2","gists":["4"]}}}}}}},"security":[{"bearer":[]}]}}},"components":{"schemas":{"Gist":{"type":"object","properties":{"id":{"type":"string","description":"ID"},"name":{"type":"string","description":"name"},"content":{"type":"string","description":"content"}},"required":["id","name","content"]}},"securitySchemes":{"bearer":{"type":"http","scheme":"bearer"}}},"servers":[]} \ No newline at end of file +{"openapi":"3.0.1","info":{"title":"Gists","description":"","version":"1.0.0"},"tags":[],"paths":{"/user/me":{"get":{"summary":"Retrieve user informations","deprecated":false,"description":"Retrieve your personnal information such as your email, profile picture...","tags":[],"parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"},"picture":{"type":"string"}},"required":["id","name","email","picture"]},"examples":{"1":{"summary":"Success","value":{"id":"8","name":"","email":"radulescutristan@gmail.com","picture":"https://lh3.googleusercontent.com/a-/ALV-UjVFT4VKCiYyND2v4fPS323_CZN0EE7zwHdQ1jFwdah3Sv5FspPa=s96-c"}}}}}}},"security":[{"bearer":[]}]}},"/gists":{"post":{"summary":"Create a gist","deprecated":false,"description":"Create a gist and link it to an organization as an option","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"name"},"content":{"type":"string"},"org_id":{"type":"string"}},"required":["name","content"]},"example":{"name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat."}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gist"},"examples":{"1":{"summary":"Success","value":{"id":"10","name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat.","owner_id":"4"}}}}}}},"security":[{"bearer":[]}]},"get":{"summary":"Get all gists","deprecated":false,"description":"Retrieve all your gists","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Gist"}},"examples":{"1":{"summary":"Fetch all gists","value":[{"id":"4","name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat.","owner_id":"4","org_id":"3"},{"id":"9","name":"temporibus sit amet","content":"Similique veniam illum laudantium sit. Officiis vitae esse accusantium. Deserunt hic distinctio dolores eos delectus enim reprehenderit sunt. Saepe doloremque iusto accusamus praesentium. Non deserunt aspernatur voluptate dolorem voluptas repellat quo nam modi. Nihil perferendis sapiente officiis quam voluptas ducimus tempora velit quaerat.","owner_id":"4"}]}}}}}},"security":[{"bearer":[]}]}},"/gists/{id}/name":{"patch":{"summary":"Update gist's name","deprecated":false,"description":"","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"1","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"name"}},"required":["name"]},"example":{"name":"doloremque dolorum nobis"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"string"}}}}},"security":[{"bearer":[]}]}},"/gists/{id}/content":{"patch":{"summary":"Update gist's content","deprecated":false,"description":"","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"1","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"name"}},"required":["name"]},"example":{"content":"ezaeeza"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"string"}}}}},"security":[{"bearer":[]}]}},"/gists/{id}":{"get":{"summary":"Get a gist","deprecated":false,"description":"","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"4","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Gist"}}}}},"security":[{"bearer":[]}]},"delete":{"summary":"Delete a gist","deprecated":false,"description":"Delete a gist","tags":[],"parameters":[{"name":"id","in":"path","description":"","required":true,"example":"1","schema":{"type":"string"}}],"responses":{"200":{"description":"Success","content":{"*/*":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":"Gist deleted successfully"}}}}}},"security":[{"bearer":[]}]}},"/auth/local/begin":{"post":{"summary":"Authenticate to the application wth a token","deprecated":false,"description":"Request a one time code by email and get registered or authenticated automatically","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}},"example":{"email":"radulescutristan@gmail.com"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"email":"radulescutristan@gmail.com"}}}}}}},"security":[]}},"/auth/local/verify":{"post":{"summary":"Confirm local authentication request","deprecated":false,"description":"Once you received your one time code, get your auth cookie with this route. It will be named `gists.access_token`","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string"},"token":{"type":"string"}},"required":["email","token"]},"example":{"email":"radulescutristan@gmail.com","token":"234244"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"message":"You are now logged in"}}}}}}},"security":[]}},"/auth/google":{"get":{"summary":"Authenticate with google","deprecated":false,"description":"Authenticate with google, and get redirected directly to gists's frontend","tags":[],"parameters":[],"responses":{"302":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}},"security":[]}},"/auth/github":{"get":{"summary":"Authenticate with github","deprecated":false,"description":"Authenticate with github, and get redirected directly to gists's frontend","tags":[],"parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}}}}}},"security":[]}},"/orgs":{"post":{"summary":"Create an organization","deprecated":false,"description":"Create an organization by providing its name","tags":[],"parameters":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]},"example":{"name":"Test organization 2"}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"id":"8","name":"Test organization 2"}}}}}}},"security":[{"bearer":[]}]}},"/orgs/":{"get":{"summary":"get all orgs of user","deprecated":false,"description":"Get all your organizations (not the gists)","tags":[],"parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":[{"id":"3","name":"Test organization 2"},{"id":"8","name":"Test organization 2"}]}}}}}},"security":[{"bearer":[]}]}},"/orgs/3":{"get":{"summary":"Get all gists ids from organization","deprecated":false,"description":"Get all the gists created in your organization.","tags":[],"parameters":[],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{}},"examples":{"1":{"summary":"Success","value":{"id":"3","name":"Test organization 2","gists":["4"]}}}}}}},"security":[{"bearer":[]}]}}},"components":{"schemas":{"Gist":{"type":"object","properties":{"id":{"type":"string","description":"ID"},"name":{"type":"string","description":"name"},"content":{"type":"string","description":"content"}},"required":["id","name","content"]}},"securitySchemes":{"bearer":{"type":"http","scheme":"bearer"}}},"servers":[]} \ No newline at end of file diff --git a/gists/router.go b/gists/router.go index dc4fdd8..fc2a264 100644 --- a/gists/router.go +++ b/gists/router.go @@ -1,7 +1,7 @@ package gists import ( - "github.com/gistapp/api/server" + "github.com/gistapp/api/user" "github.com/gofiber/fiber/v2" ) @@ -10,7 +10,7 @@ type GistRouter struct { } func (r *GistRouter) SubscribeRoutes(app *fiber.Router) { - gists_router := (*app).Group("/gists", server.AuthNeededMiddleware) + gists_router := (*app).Group("/gists", user.AuthNeededMiddleware) gists_router.Post("/", r.Controller.Save()) gists_router.Patch("/:id/name", r.Controller.UpdateName()) diff --git a/justfile b/justfile new file mode 100644 index 0000000..5ea0470 --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +build: + go build -o api -v + +test-all: + go test ./tests/ -v + +test TEST: + go test ./tests/{{TEST}} -v + +migrate: build + ./api migrate + +dev: + air diff --git a/main.go b/main.go index ac59b4c..3bd9fe8 100644 --- a/main.go +++ b/main.go @@ -4,11 +4,11 @@ import ( "fmt" "os" - "github.com/gistapp/api/auth" "github.com/gistapp/api/gists" "github.com/gistapp/api/organizations" "github.com/gistapp/api/server" "github.com/gistapp/api/storage" + "github.com/gistapp/api/user" "github.com/gistapp/api/utils" "github.com/gofiber/fiber/v2/log" ) @@ -37,19 +37,23 @@ func main() { Controller: gists.GistController, } - authRouter := auth.AuthRouter{ - Controller: &auth.AuthControllerImpl{ - AuthService: &auth.AuthService, + authRouter := user.AuthRouter{ + Controller: &user.AuthControllerImpl{ + AuthService: &user.AuthService, }, } + userRouter := user.UserRouter{ + Controller: &user.UserControllerImpl{}, + } + orgRouter := organizations.OrganizationRouter{ Controller: organizations.OrganizationControllerImpl{}, } - auth.AuthService.RegisterProviders() //register goth providers for authentication + user.AuthService.RegisterProviders() //register goth providers for authentication // Start the server - s.Setup(&gistRouter, &authRouter, &orgRouter) + s.Setup(&gistRouter, &authRouter, &orgRouter, &userRouter) s.Ignite() } diff --git a/organizations/router.go b/organizations/router.go index e6a1027..a819096 100644 --- a/organizations/router.go +++ b/organizations/router.go @@ -1,7 +1,7 @@ package organizations import ( - "github.com/gistapp/api/server" + "github.com/gistapp/api/user" "github.com/gofiber/fiber/v2" ) @@ -10,7 +10,7 @@ type OrganizationRouter struct { } func (r *OrganizationRouter) SubscribeRoutes(app *fiber.Router) { - organizations_router := (*app).Group("/orgs", server.AuthNeededMiddleware) + organizations_router := (*app).Group("/orgs", user.AuthNeededMiddleware) organizations_router.Post("/", r.Controller.Save()) organizations_router.Get("/", r.Controller.GetAsMember()) diff --git a/server/middleware.go b/server/middleware.go index c29ccef..906e5a7 100644 --- a/server/middleware.go +++ b/server/middleware.go @@ -1,7 +1,7 @@ package server import ( - "github.com/gistapp/api/auth" + "github.com/gistapp/api/user" "github.com/gofiber/fiber/v2" ) @@ -21,7 +21,7 @@ func AuthNeededMiddleware(ctx *fiber.Ctx) error { }) } raw_token := string(ctx.Request().Header.Peek("Authorization")[7:]) - claims, err := auth.AuthService.IsAuthenticated(raw_token) + claims, err := user.AuthService.IsAuthenticated(raw_token) if err != nil { return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": "Unauthorized", diff --git a/tests/gists_test.go b/tests/gists_test.go index 3d0de98..b9fb569 100644 --- a/tests/gists_test.go +++ b/tests/gists_test.go @@ -6,12 +6,12 @@ import ( "os" "testing" - "github.com/gistapp/api/auth" "github.com/gistapp/api/gists" "github.com/gistapp/api/organizations" "github.com/gistapp/api/server" "github.com/gistapp/api/storage" "github.com/gistapp/api/tests/mock" + "github.com/gistapp/api/user" "github.com/gistapp/api/utils" "github.com/gofiber/fiber/v2" ) @@ -34,7 +34,7 @@ func InitServerGists() *fiber.App { Controller: gists.GistController, } - auth_router := auth.AuthRouter{ + auth_router := user.AuthRouter{ Controller: &mock.MockAuthController{ //needs GetUser to fit the auth interface AuthService: &mock.MockAuthService{}, @@ -55,7 +55,7 @@ func TestCreateGists(t *testing.T) { app := InitServerGists() authToken := GetAuthToken(t, app) - body, req := utils.MakeRequest(t, app, "/gists", map[string]string{ + body, req := utils.MakeRequest("POST", t, app, "/gists", map[string]string{ "name": "Test Gist", "content": "Test content", }, map[string]string{ @@ -73,7 +73,7 @@ func TestCreateGists(t *testing.T) { t.Run("Create a new organization gist", func(t *testing.T) { app := InitServerGists() auth_token := GetAuthToken(t, app) - claims, _ := auth.AuthService.IsAuthenticated(auth_token) + claims, _ := user.AuthService.IsAuthenticated(auth_token) org_mod := organizations.OrganizationSQL{ Name: sql.NullString{ @@ -93,7 +93,7 @@ func TestCreateGists(t *testing.T) { "org_id": org.ID, } - body, req := utils.MakeRequest(t, app, "/gists", payload, map[string]string{ + body, req := utils.MakeRequest("POST", t, app, "/gists", payload, map[string]string{ "Authorization": fmt.Sprintf("Bearer %s", auth_token), }) diff --git a/tests/mock/auth_controller.go b/tests/mock/auth_controller.go index 022af36..b39ff0e 100644 --- a/tests/mock/auth_controller.go +++ b/tests/mock/auth_controller.go @@ -1,12 +1,12 @@ package mock import ( - "github.com/gistapp/api/auth" + "github.com/gistapp/api/user" "github.com/gofiber/fiber/v2" ) -type MockAuthController struct{ - AuthService auth.IAuthService +type MockAuthController struct { + AuthService user.IAuthService } func (a *MockAuthController) Callback() fiber.Handler { @@ -23,7 +23,7 @@ func (a *MockAuthController) Authenticate() fiber.Handler { func (a *MockAuthController) LocalAuth() fiber.Handler { return func(c *fiber.Ctx) error { - e := new(auth.AuthLocalValidator) + e := new(user.AuthLocalValidator) if err := c.BodyParser(e); err != nil { return c.Status(400).SendString("Request must be valid JSON with field email as text") } @@ -39,7 +39,7 @@ func (a *MockAuthController) LocalAuth() fiber.Handler { func (a *MockAuthController) VerifyAuthToken() fiber.Handler { return func(c *fiber.Ctx) error { - e := new(auth.AuthLocalVerificationValidator) + e := new(user.AuthLocalVerificationValidator) if err := c.BodyParser(e); err != nil { return c.Status(400).SendString("Request must be valid JSON with fields token and email as text") @@ -61,4 +61,4 @@ func (a *MockAuthController) VerifyAuthToken() fiber.Handler { c.Cookie(token_cookie) return c.Status(200).JSON(fiber.Map{"message": "You are now logged in"}) } -} \ No newline at end of file +} diff --git a/tests/mock/auth_service.go b/tests/mock/auth_service.go index 14923cc..60cdb9a 100644 --- a/tests/mock/auth_service.go +++ b/tests/mock/auth_service.go @@ -6,7 +6,6 @@ import ( "errors" "strings" - "github.com/gistapp/api/auth" "github.com/gistapp/api/user" "github.com/gistapp/api/utils" "github.com/gofiber/fiber/v2" @@ -20,12 +19,12 @@ func (m *MockAuthService) Authenticate(c *fiber.Ctx) error { return nil } -func (m *MockAuthService) LocalAuth(email string) (auth.TokenSQL, error) { +func (m *MockAuthService) LocalAuth(email string) (user.TokenSQL, error) { token_val := utils.GenToken(6) - token_model := auth.TokenSQL{ + token_model := user.TokenSQL{ Keyword: sql.NullString{String: email, Valid: true}, Value: sql.NullString{String: token_val, Valid: true}, - Type: sql.NullString{String: string(auth.LocalAuth), Valid: true}, + Type: sql.NullString{String: string(user.LocalAuth), Valid: true}, } _, err := token_model.Save() @@ -35,10 +34,10 @@ func (m *MockAuthService) LocalAuth(email string) (auth.TokenSQL, error) { } func (m *MockAuthService) VerifyLocalAuthToken(token string, email string) (string, error) { - token_model := auth.TokenSQL{ + token_model := user.TokenSQL{ Value: sql.NullString{String: token, Valid: true}, Keyword: sql.NullString{String: email, Valid: true}, - Type: sql.NullString{String: string(auth.LocalAuth), Valid: true}, + Type: sql.NullString{String: string(user.LocalAuth), Valid: true}, } token_data, err := token_model.Get() if err != nil { @@ -80,8 +79,8 @@ func (m *MockAuthService) Callback(c *fiber.Ctx) (string, error) { return "", nil } -func (a *MockAuthService) GetUser(auth_user goth.User) (*user.User, *auth.AuthIdentity, error) { - return auth.AuthService.GetUser(auth_user) +func (a *MockAuthService) GetUser(auth_user goth.User) (*user.User, *user.AuthIdentity, error) { + return user.AuthService.GetUser(auth_user) } func (m *MockAuthService) Register(auth_user goth.User) (*user.User, error) { @@ -103,7 +102,7 @@ func (m *MockAuthService) Register(auth_user goth.User) (*user.User, error) { return nil, err } - auth_identity_model := auth.AuthIdentitySQL{ + auth_identity_model := user.AuthIdentitySQL{ Data: sql.NullString{String: string(data), Valid: true}, Type: sql.NullString{String: auth_user.Provider, Valid: true}, OwnerID: sql.NullString{String: user_data.ID, Valid: true}, @@ -114,14 +113,14 @@ func (m *MockAuthService) Register(auth_user goth.User) (*user.User, error) { return user_data, err } -func (a *MockAuthService) IsAuthenticated(token string) (*auth.JWTClaim, error) { +func (a *MockAuthService) IsAuthenticated(token string) (*user.JWTClaim, error) { claims, err := utils.VerifyJWT(token) if err != nil { return nil, err } - jwtClaim := new(auth.JWTClaim) + jwtClaim := new(user.JWTClaim) jwtClaim.Pub = claims["pub"].(string) jwtClaim.Email = claims["email"].(string) diff --git a/tests/organization_test.go b/tests/organization_test.go index f565611..70370dc 100644 --- a/tests/organization_test.go +++ b/tests/organization_test.go @@ -7,18 +7,16 @@ import ( "strconv" "testing" - "github.com/gistapp/api/auth" "github.com/gistapp/api/gists" "github.com/gistapp/api/organizations" "github.com/gistapp/api/server" "github.com/gistapp/api/storage" "github.com/gistapp/api/tests/mock" + "github.com/gistapp/api/user" "github.com/gistapp/api/utils" "github.com/gofiber/fiber/v2" ) -var endpoint = "http://localhost:4000" - func InitServerOrgs() *fiber.App { // Check for command-line arguments if len(os.Args) > 1 && os.Args[1] == "migrate" { @@ -37,7 +35,7 @@ func InitServerOrgs() *fiber.App { Controller: gists.GistController, } - auth_router := auth.AuthRouter{ + auth_router := user.AuthRouter{ Controller: &mock.MockAuthController{ AuthService: &mock.MockAuthService{}, }, @@ -70,7 +68,7 @@ func TestCreateOrganization(t *testing.T) { } fmt.Println(org_payload) // - body, _ := utils.MakeRequest(t, app, "/orgs", org_payload, map[string]string{ + body, _ := utils.MakeRequest("POST", t, app, "/orgs", org_payload, map[string]string{ "Authorization": "Bearer " + auth_token, }) // diff --git a/tests/users_test.go b/tests/users_test.go new file mode 100644 index 0000000..187996d --- /dev/null +++ b/tests/users_test.go @@ -0,0 +1,81 @@ +package tests + +import ( + "fmt" + "os" + "testing" + + "github.com/gistapp/api/server" + "github.com/gistapp/api/storage" + "github.com/gistapp/api/tests/mock" + "github.com/gistapp/api/user" + "github.com/gistapp/api/utils" + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +func InitServerUsers() *fiber.App { + if len(os.Args) > 1 && os.Args[1] == "migrate" { + if err := storage.Migrate(); err != nil { + return nil + } + return nil + } + + // Set up the server + port := utils.Get("PORT") + s := server.NewServer(fmt.Sprintf(":%s", port)) + + // Set up routers + + auth_router := user.AuthRouter{ + Controller: &mock.MockAuthController{ + AuthService: &mock.MockAuthService{}, + }, + } + + user_router := user.UserRouter{ + Controller: &user.UserControllerImpl{}, + } + + // Initialize the server with the routers + s.Setup(&auth_router, &user_router) + return s.App +} + +func TestRetreiveUser(t *testing.T) { + t.Run("Retreive user", func(t *testing.T) { + app := InitServerUsers() + if app == nil { + t.Fatal("Failed to initialize the application") + } + + // Begin the sign-up process + //token corresponds to "test@test.com user" + auth_token := GetAuthToken(t, app) + + // Retrieve the user + body, _ := utils.MakeRequest("GET", t, app, "/user/me", nil, map[string]string{ + "Authorization": "Bearer " + auth_token, + }) + + if body["email"] != "test@test.com" { + t.Fatalf("Expected email to be test@test.com") + } + + shouldHave := map[string]bool{ + "email": true, + "name": true, + "picture": true, + "id": true, + } + + for key := range body { + if !shouldHave[key] { + t.Fatalf("Unexpected key %s", key) + } + } + + log.Info(body) + }) +} diff --git a/tests/utils.go b/tests/utils.go index af9c2df..59d00de 100644 --- a/tests/utils.go +++ b/tests/utils.go @@ -4,7 +4,6 @@ import ( "database/sql" "testing" - "github.com/gistapp/api/auth" "github.com/gistapp/api/user" "github.com/gistapp/api/utils" "github.com/gofiber/fiber/v2" @@ -15,7 +14,7 @@ func GetAuthToken(t *testing.T, app *fiber.App) string { beginPayload := map[string]string{ "email": "test@test.com", } - respBody, _ := utils.MakeRequest(t, app, "/auth/local/begin", beginPayload, nil) + respBody, _ := utils.MakeRequest("POST", t, app, "/auth/local/begin", beginPayload, nil) token := respBody["token"] // Verify the sign-up process @@ -23,14 +22,14 @@ func GetAuthToken(t *testing.T, app *fiber.App) string { "email": "test@test.com", "token": token, } - _, resp := utils.MakeRequest(t, app, "/auth/local/verify", verifyPayload, nil) + _, resp := utils.MakeRequest("POST", t, app, "/auth/local/verify", verifyPayload, nil) auth_token := resp.Cookies()[0].Value return auth_token } func DeleteAuthUser(t *testing.T, auth_token string) { - claims, _ := auth.AuthService.IsAuthenticated(auth_token) + claims, _ := user.AuthService.IsAuthenticated(auth_token) user := user.UserSQL{ ID: sql.NullString{ diff --git a/auth/controller.go b/user/auth_controller.go similarity index 97% rename from auth/controller.go rename to user/auth_controller.go index 7bd23bb..371ce59 100644 --- a/auth/controller.go +++ b/user/auth_controller.go @@ -1,4 +1,4 @@ -package auth +package user import ( "github.com/gistapp/api/utils" @@ -12,7 +12,7 @@ type IAuthController interface { VerifyAuthToken() fiber.Handler } -type AuthControllerImpl struct{ +type AuthControllerImpl struct { AuthService IAuthService } diff --git a/auth/auth_identity_model.go b/user/auth_identity_model.go similarity index 97% rename from auth/auth_identity_model.go rename to user/auth_identity_model.go index 6d8dc4e..40b9a4d 100644 --- a/auth/auth_identity_model.go +++ b/user/auth_identity_model.go @@ -1,11 +1,10 @@ -package auth +package user import ( "database/sql" "errors" "github.com/gistapp/api/storage" - "github.com/gistapp/api/user" "github.com/gofiber/fiber/v2/log" ) @@ -32,7 +31,7 @@ type JWTClaim struct { type AuthIdentityAndUser struct { AuthIdentity AuthIdentity - User user.User + User User } type AuthIdentityModel interface { diff --git a/user/auth_middleware.go b/user/auth_middleware.go new file mode 100644 index 0000000..bc00ecf --- /dev/null +++ b/user/auth_middleware.go @@ -0,0 +1,21 @@ +package user + +import "github.com/gofiber/fiber/v2" + +func AuthNeededMiddleware(ctx *fiber.Ctx) error { + if ctx.Get("Authorization") == "" { + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Unauthorized", + }) + } + raw_token := string(ctx.Request().Header.Peek("Authorization")[7:]) + claims, err := AuthService.IsAuthenticated(raw_token) + if err != nil { + return ctx.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ + "error": "Unauthorized", + }) + } + ctx.Locals("pub", claims.Pub) + ctx.Locals("email", claims.Email) + return ctx.Next() +} diff --git a/auth/router.go b/user/auth_router.go similarity index 96% rename from auth/router.go rename to user/auth_router.go index 5647a41..a4f961d 100644 --- a/auth/router.go +++ b/user/auth_router.go @@ -1,4 +1,4 @@ -package auth +package user import "github.com/gofiber/fiber/v2" diff --git a/auth/service.go b/user/auth_service.go similarity index 93% rename from auth/service.go rename to user/auth_service.go index 88438d9..0e586c7 100644 --- a/auth/service.go +++ b/user/auth_service.go @@ -1,4 +1,4 @@ -package auth +package user import ( "database/sql" @@ -6,7 +6,6 @@ import ( "errors" "strings" - "github.com/gistapp/api/user" "github.com/gistapp/api/utils" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/log" @@ -21,8 +20,8 @@ type IAuthService interface { LocalAuth(email string) (TokenSQL, error) VerifyLocalAuthToken(token string, email string) (string, error) Callback(c *fiber.Ctx) (string, error) - GetUser(auth_user goth.User) (*user.User, *AuthIdentity, error) - Register(auth_user goth.User) (*user.User, error) + GetUser(auth_user goth.User) (*User, *AuthIdentity, error) + Register(auth_user goth.User) (*User, error) RegisterProviders() IsAuthenticated(token string) (*JWTClaim, error) } @@ -132,7 +131,7 @@ func (a *AuthServiceImpl) Callback(c *fiber.Ctx) (string, error) { return jwt, nil } -func (a *AuthServiceImpl) GetUser(auth_user goth.User) (*user.User, *AuthIdentity, error) { +func (a *AuthServiceImpl) GetUser(auth_user goth.User) (*User, *AuthIdentity, error) { auth_and_user, err := new(AuthIdentitySQL).GetWithUser(auth_user.UserID) if err != nil { return nil, nil, err @@ -141,13 +140,13 @@ func (a *AuthServiceImpl) GetUser(auth_user goth.User) (*user.User, *AuthIdentit return &auth_and_user.User, &auth_and_user.AuthIdentity, nil } -func (a *AuthServiceImpl) Register(auth_user goth.User) (*user.User, error) { +func (a *AuthServiceImpl) Register(auth_user goth.User) (*User, error) { data, err := json.Marshal(auth_user) if err != nil { return nil, errors.New("couldn't marshal user") } - user_model := user.UserSQL{ + user_model := UserSQL{ ID: sql.NullString{String: auth_user.UserID, Valid: true}, Email: sql.NullString{String: auth_user.Email, Valid: true}, Name: sql.NullString{String: auth_user.Name, Valid: true}, diff --git a/user/service.go b/user/service.go deleted file mode 100644 index a00006b..0000000 --- a/user/service.go +++ /dev/null @@ -1 +0,0 @@ -package user diff --git a/auth/token_model.go b/user/token_model.go similarity index 99% rename from auth/token_model.go rename to user/token_model.go index 29ccc81..f3418d3 100644 --- a/auth/token_model.go +++ b/user/token_model.go @@ -1,4 +1,4 @@ -package auth +package user import ( "database/sql" diff --git a/user/user_controller.go b/user/user_controller.go new file mode 100644 index 0000000..b303825 --- /dev/null +++ b/user/user_controller.go @@ -0,0 +1,19 @@ +package user + +import "github.com/gofiber/fiber/v2" + +type UserControllerImpl struct{} + +func (u *UserControllerImpl) Get() fiber.Handler { + return func(c *fiber.Ctx) error { + owner_id := c.Locals("pub").(string) + + user, err := UserService.GetUserByID(owner_id) + + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.JSON(user) + } +} diff --git a/user/model.go b/user/user_model.go similarity index 79% rename from user/model.go rename to user/user_model.go index d2d9bae..bf9028e 100644 --- a/user/model.go +++ b/user/user_model.go @@ -67,6 +67,25 @@ func (u *UserSQL) GetByEmail() (*User, error) { return &user, nil } +func (u *UserSQL) GetByID() (*User, error) { + query := "SELECT user_id, email, name, picture FROM users WHERE user_id = $1" + row, err := storage.Database.Query(query, u.ID.String) + + if err != nil { + log.Error(err) + return nil, errors.New("couldn't find user") + } + + var user User + row.Next() + err = row.Scan(&user.ID, &user.Email, &user.Name, &user.Picture) + if err != nil { + log.Error(err) + return nil, errors.New("couldn't find user") + } + return &user, nil +} + func (u *UserSQL) Delete() error { _, err := storage.Database.Exec("DELETE FROM users WHERE user_id = $1", u.ID.String) if err != nil { diff --git a/user/user_router.go b/user/user_router.go new file mode 100644 index 0000000..7dc15b1 --- /dev/null +++ b/user/user_router.go @@ -0,0 +1,15 @@ +package user + +import ( + "github.com/gofiber/fiber/v2" +) + +type UserRouter struct { + Controller *UserControllerImpl +} + +func (r *UserRouter) SubscribeRoutes(app *fiber.Router) { + user_router := (*app).Group("/user", AuthNeededMiddleware) + + user_router.Get("/me", r.Controller.Get()) +} diff --git a/user/user_service.go b/user/user_service.go new file mode 100644 index 0000000..9602d01 --- /dev/null +++ b/user/user_service.go @@ -0,0 +1,17 @@ +package user + +import "database/sql" + +type UserServiceImpl struct{} + +func (u *UserServiceImpl) GetUserByID(id string) (*User, error) { + user := UserSQL{ + ID: sql.NullString{ + String: id, + Valid: true, + }, + } + return user.GetByID() +} + +var UserService = UserServiceImpl{} diff --git a/utils/http_testing.go b/utils/http_testing.go index 38b89fb..dc03c40 100644 --- a/utils/http_testing.go +++ b/utils/http_testing.go @@ -9,25 +9,28 @@ import ( "github.com/gofiber/fiber/v2" ) -func MakeRequest(t *testing.T, app *fiber.App, url string, payload interface{}, headers map[string]string) (map[string]string, *http.Response) { +func MakeRequest(method string, t *testing.T, app *fiber.App, url string, payload interface{}, headers map[string]string) (map[string]string, *http.Response) { // Marshal payload to JSON jsonPayload, err := json.Marshal(payload) if err != nil { t.Fatalf("Failed to marshal JSON: %v", err) } + var req *http.Request // Create a new HTTP request - req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonPayload))) + if method == "GET" { + req, err = http.NewRequest("GET", url, nil) + } else { + req, err = http.NewRequest("POST", url, strings.NewReader(string(jsonPayload))) + req.Header.Add("Content-Type", "application/json") + + } if err != nil { t.Fatalf("Failed to create request: %v", err) } - req.Header.Add("Content-Type", "application/json") - - if headers != nil { - for key, value := range headers { - req.Header.Add(key, value) - } + for key, value := range headers { + req.Header.Add(key, value) } // Test the request using Fiber's testing framework