diff --git a/.github/workflows/mr-test.yml b/.github/workflows/mr-test.yml new file mode 100644 index 0000000..3110ccf --- /dev/null +++ b/.github/workflows/mr-test.yml @@ -0,0 +1,34 @@ +name: Continuous testing +on: + pull_request: + branches: + - main + +jobs: + test: + name: Integration tests + runs-on: ubuntu-latest + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: gists + POSTGRES_USER: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: + - uses: actions/checkout@v4 + - name: Install Go toolchain + uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2 + with: + go-version: 1.23.0 + - name: Run migrations + run: go build -o main && chmod +x ./main && PORT="4000" PG_USER="postgres" PG_PASSWORD="postgres" PG_PORT="5432" PG_HOST="0.0.0.0" PG_DATABASE="gists" GOOGLE_KEY="" GOOGLE_SECRET="" GITHUB_KEY="" GITHUB_SECRET="" PUBLIC_URL="http://localhost:4000" APP_KEY="DUMP_APP_KEY_FOR_TEST" SMTP_HOST="" MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" FRONTEND_URL="http://localhost:3000" ./main migrate + - name: Run tests + run: PORT="4000" PG_USER="postgres" PG_PASSWORD="postgres" PG_PORT="5432" PG_HOST="0.0.0.0" PG_DATABASE="gists" GOOGLE_KEY="" GOOGLE_SECRET="" GITHUB_KEY="" GITHUB_SECRET="" PUBLIC_URL="http://localhost:4000" APP_KEY="DUMP_APP_KEY_FOR_TEST" SMTP_HOST="" MAIL_SMTP="" MAIL_PASSWORD="" SMTP_PORT="" FRONTEND_URL="http://localhost:3000" go test ./tests/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5d71af8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "ansible.python.interpreterPath": "/bin/python3" +} \ No newline at end of file diff --git a/auth/controller.go b/auth/controller.go index a0be051..7bd23bb 100644 --- a/auth/controller.go +++ b/auth/controller.go @@ -5,7 +5,16 @@ import ( "github.com/gofiber/fiber/v2" ) -type AuthControllerImpl struct{} +type IAuthController interface { + Callback() fiber.Handler + Authenticate() fiber.Handler + LocalAuth() fiber.Handler + VerifyAuthToken() fiber.Handler +} + +type AuthControllerImpl struct{ + AuthService IAuthService +} type AuthLocalValidator struct { Email string `json:"email"` @@ -18,7 +27,7 @@ type AuthLocalVerificationValidator struct { func (a *AuthControllerImpl) Callback() fiber.Handler { return func(c *fiber.Ctx) error { - token, err := AuthService.Callback(c) + token, err := a.AuthService.Callback(c) if err != nil { return c.Status(400).SendString(err.Error()) } @@ -33,7 +42,7 @@ func (a *AuthControllerImpl) Callback() fiber.Handler { func (a *AuthControllerImpl) Authenticate() fiber.Handler { return func(c *fiber.Ctx) error { - return AuthService.Authenticate(c) + return a.AuthService.Authenticate(c) } } @@ -44,7 +53,7 @@ func (a *AuthControllerImpl) LocalAuth() fiber.Handler { return c.Status(400).SendString("Request must be valid JSON with field email as text") } - if err := AuthService.LocalAuth(e.Email); err != nil { + if _, err := a.AuthService.LocalAuth(e.Email); err != nil { return c.Status(400).SendString(err.Error()) } @@ -63,7 +72,7 @@ func (a *AuthControllerImpl) VerifyAuthToken() fiber.Handler { token := e.Token email := e.Email - jwt_token, err := AuthService.VerifyLocalAuthToken(token, email) + jwt_token, err := a.AuthService.VerifyLocalAuthToken(token, email) if err != nil { return c.Status(400).SendString(err.Error()) diff --git a/auth/router.go b/auth/router.go index 3d7af2c..5647a41 100644 --- a/auth/router.go +++ b/auth/router.go @@ -3,7 +3,7 @@ package auth import "github.com/gofiber/fiber/v2" type AuthRouter struct { - Controller AuthControllerImpl + Controller IAuthController } func (r *AuthRouter) SubscribeRoutes(app *fiber.Router) { diff --git a/auth/service.go b/auth/service.go index 66231aa..88438d9 100644 --- a/auth/service.go +++ b/auth/service.go @@ -16,6 +16,17 @@ import ( "github.com/shareed2k/goth_fiber" ) +type IAuthService interface { + Authenticate(c *fiber.Ctx) error + 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) + RegisterProviders() + IsAuthenticated(token string) (*JWTClaim, error) +} + type AuthServiceImpl struct{} func (a *AuthServiceImpl) Authenticate(c *fiber.Ctx) error { @@ -28,7 +39,7 @@ func (a *AuthServiceImpl) Authenticate(c *fiber.Ctx) error { } // generates a token and sends it to the user by email -func (a *AuthServiceImpl) LocalAuth(email string) error { +func (a *AuthServiceImpl) LocalAuth(email string) (TokenSQL, error) { token_val := utils.GenToken(6) token_model := TokenSQL{ Keyword: sql.NullString{String: email, Valid: true}, @@ -39,12 +50,12 @@ func (a *AuthServiceImpl) LocalAuth(email string) error { _, err := token_model.Save() if err != nil { - return err + return token_model, err } err = utils.SendEmail("Gistapp: Local Auth", "Your token is: "+token_val, email) - return err + return token_model, err } // verifies the token and finishes the registration diff --git a/docs/openapi.json b/docs/openapi.json index baee89f..2ca6b45 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1,332 +1 @@ -{ - "openapi": "3.0.1", - "info": { "title": "Gists", "description": "", "version": "1.0.0" }, - "tags": [], - "paths": { - "/gists": { - "post": { - "summary": "Create a gist", - "deprecated": false, - "description": "", - "tags": [], - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { "type": "string", "description": "name" }, - "content": { "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" } - } - } - } - }, - "security": [{ "bearer": [] }] - }, - "get": { - "summary": "Get all gists", - "deprecated": false, - "description": "", - "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" } - } - } - } - } - }, - "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": "2", - "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": "", - "tags": [], - "parameters": [ - { - "name": "id", - "in": "path", - "description": "", - "required": true, - "example": "1", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { "type": "object", "properties": {} } - } - } - } - }, - "security": [{ "bearer": [] }] - } - }, - "/auth/local/begin": { - "post": { - "summary": "Authenticate to the application wth a token", - "deprecated": false, - "description": "", - "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": {} } - } - } - } - }, - "security": [] - } - }, - "/auth/local/verify": { - "post": { - "summary": "Confirm local authentication request", - "deprecated": false, - "description": "", - "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": "163224" - } - } - } - }, - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { "type": "object", "properties": {} } - } - } - } - }, - "security": [] - } - }, - "/auth/google": { - "get": { - "summary": "Authenticate with google", - "deprecated": false, - "description": "", - "tags": [], - "parameters": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { "type": "object", "properties": {} } - } - } - } - }, - "security": [] - } - }, - "/auth/github": { - "get": { - "summary": "Authenticate with github", - "deprecated": false, - "description": "", - "tags": [], - "parameters": [], - "responses": { - "200": { - "description": "Success", - "content": { - "application/json": { - "schema": { "type": "object", "properties": {} } - } - } - } - }, - "security": [] - } - } - }, - "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": [] -} +{"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 diff --git a/gists/controller.go b/gists/controller.go index b9c0b2f..3c3d75f 100644 --- a/gists/controller.go +++ b/gists/controller.go @@ -7,6 +7,7 @@ type GistControllerImpl struct{} type GistSaveValidator struct { Name string `json:"name"` Content string `json:"content"` + OrgID string `json:"org_id,omitempty"` } func (g *GistControllerImpl) Save() fiber.Handler { @@ -17,7 +18,7 @@ func (g *GistControllerImpl) Save() fiber.Handler { if err := c.BodyParser(g); err != nil { return c.Status(400).SendString("Request must be valid JSON with fields name and content as text") } - gist, err := GistService.Save(g.Name, g.Content, owner_id) + gist, err := GistService.Save(g.Name, g.Content, owner_id, g.OrgID) if err != nil { return c.Status(500).SendString(err.Error()) } @@ -51,6 +52,7 @@ func (g *GistControllerImpl) FindAll() fiber.Handler { if err != nil { return c.Status(500).SendString(err.Error()) } + return c.JSON(gists) } } diff --git a/gists/model.go b/gists/model.go index 475602a..6802344 100644 --- a/gists/model.go +++ b/gists/model.go @@ -14,13 +14,15 @@ type GistSQL struct { Name sql.NullString Content sql.NullString OwnerID sql.NullString + OrgID sql.NullInt32 } type Gist struct { - ID string `json:"id"` - Name string `json:"name"` - Content string `json:"content"` - OwnerID string `json:"owner_id"` + ID string `json:"id"` + Name string `json:"name"` + Content string `json:"content"` + OwnerID string `json:"owner_id"` + OrgID *string `json:"org_id,omitempty"` } type GistModel interface { @@ -28,7 +30,14 @@ type GistModel interface { } func (g *GistSQL) Save() (*Gist, error) { - row, err := storage.Database.Query("INSERT INTO gists(name, content, owner) VALUES ($1, $2, $3) RETURNING gist_id, name, content, owner", g.Name.String, g.Content.String, g.OwnerID.String) + var row *sql.Rows + var err error + + if g.OrgID.Valid { + row, err = storage.Database.Query("INSERT INTO gists(name, content, owner, org_id) VALUES ($1, $2, $3, $4) RETURNING gist_id, name, content, owner, org_id", g.Name.String, g.Content.String, g.OwnerID.String, g.OrgID.Int32) + } else { + row, err = storage.Database.Query("INSERT INTO gists(name, content, owner) VALUES ($1, $2, $3) RETURNING gist_id, name, content, owner", g.Name.String, g.Content.String, g.OwnerID.String) + } if err != nil { log.Error(err) @@ -38,13 +47,17 @@ func (g *GistSQL) Save() (*Gist, error) { var gist Gist row.Next() - err = row.Scan(&gist.ID, &gist.Name, &gist.Content, &gist.OwnerID) + if g.OrgID.Valid { + err = row.Scan(&gist.ID, &gist.Name, &gist.Content, &gist.OwnerID, &gist.OrgID) + } else { + err = row.Scan(&gist.ID, &gist.Name, &gist.Content, &gist.OwnerID) + gist.OrgID = nil + } if err != nil { log.Error(err) return nil, errors.New("couldn't find gist") } return &gist, nil - } func (g *GistSQL) UpdateName(id string) error { @@ -75,14 +88,14 @@ func (g *GistSQL) Delete(id string) error { } func (g *GistSQL) FindByID(id string) (*Gist, error) { - row, err := storage.Database.Query("SELECT gist_id, name, content, owner FROM gists WHERE gist_id = $1 AND owner = $2", id, g.OwnerID.String) + row, err := storage.Database.Query("SELECT gist_id, name, content, owner, org_id FROM gists WHERE gist_id = $1 AND owner = $2", id, g.OwnerID.String) if err != nil { log.Error(err) return nil, errors.New("couldn't find gist") } row.Next() var gist Gist - err = row.Scan(&gist.ID, &gist.Name, &gist.Content, &gist.OwnerID) + err = row.Scan(&gist.ID, &gist.Name, &gist.Content, &gist.OwnerID, &gist.OrgID) if err != nil { log.Error(err) return nil, errors.New("couldn't find gist") @@ -91,7 +104,7 @@ func (g *GistSQL) FindByID(id string) (*Gist, error) { } func (g *GistSQL) FindAll() ([]Gist, error) { - rows, err := storage.Database.Query("SELECT gist_id, name, content, owner FROM gists WHERE owner = $1", g.OwnerID.String) + rows, err := storage.Database.Query("SELECT gist_id, name, content, owner, org_id FROM gists WHERE owner = $1", g.OwnerID.String) if err != nil { log.Error(err) return nil, errors.New("couldn't find gists") @@ -99,7 +112,7 @@ func (g *GistSQL) FindAll() ([]Gist, error) { var gists []Gist for rows.Next() { var gist GistSQL - err = rows.Scan(&gist.ID, &gist.Name, &gist.Content, &gist.OwnerID) + err = rows.Scan(&gist.ID, &gist.Name, &gist.Content, &gist.OwnerID, &gist.OrgID) if err != nil { log.Error(err) return nil, errors.New("couldn't find gists") @@ -109,6 +122,13 @@ func (g *GistSQL) FindAll() ([]Gist, error) { Name: gist.Name.String, Content: gist.Content.String, OwnerID: gist.OwnerID.String, + OrgID: func() *string { + if gist.OrgID.Valid { + orgID := strconv.Itoa(int(gist.OrgID.Int32)) + return &orgID + } + return nil + }(), }) } return gists, nil diff --git a/gists/service.go b/gists/service.go index 3da7b7f..3143d57 100644 --- a/gists/service.go +++ b/gists/service.go @@ -3,28 +3,61 @@ package gists import ( "database/sql" "errors" + "strconv" ) type GistServiceImpl struct{} -func (g *GistServiceImpl) Save(name string, content string, owner_id string) (*Gist, error) { - m := GistSQL{ - ID: sql.NullInt32{ - Valid: false, - Int32: 0, - }, - Name: sql.NullString{ - String: name, - Valid: true, - }, - Content: sql.NullString{ - String: content, - Valid: true, - }, - OwnerID: sql.NullString{ - String: owner_id, - Valid: true, - }, +func (g *GistServiceImpl) Save(name string, content string, owner_id string, org_id string) (*Gist, error) { + var m GistSQL + + if org_id != "" { + org_id_int, err := strconv.Atoi(org_id) + + if err != nil { + return nil, errors.New("org_id must be an integer") + } + m = GistSQL{ + ID: sql.NullInt32{ + Valid: false, + Int32: 0, + }, + Name: sql.NullString{ + String: name, + Valid: true, + }, + Content: sql.NullString{ + String: content, + Valid: true, + }, + OwnerID: sql.NullString{ + String: owner_id, + Valid: true, + }, + OrgID: sql.NullInt32{ + Int32: int32(org_id_int), + Valid: true, + }, + } + } else { + m = GistSQL{ + ID: sql.NullInt32{ + Valid: false, + Int32: 0, + }, + Name: sql.NullString{ + String: name, + Valid: true, + }, + Content: sql.NullString{ + String: content, + Valid: true, + }, + OwnerID: sql.NullString{ + String: owner_id, + Valid: true, + }, + } } gist, err := m.Save() diff --git a/go.mod b/go.mod index b2ad414..44849b4 100644 --- a/go.mod +++ b/go.mod @@ -2,16 +2,23 @@ module github.com/gistapp/api go 1.22.5 -require github.com/gofiber/fiber/v2 v2.52.5 +require ( + github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 + github.com/gofiber/fiber/v2 v2.52.5 + github.com/golang-jwt/jwt/v5 v5.2.1 + github.com/golang-migrate/migrate/v4 v4.17.1 + github.com/lib/pq v1.10.9 + github.com/markbates/goth v1.80.0 + github.com/shareed2k/goth_fiber v0.3.0 + github.com/spf13/viper v1.19.0 + gopkg.in/mail.v2 v2.3.1 +) require ( cloud.google.com/go/compute v1.24.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect - github.com/golang-migrate/migrate/v4 v4.17.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.5.0 // indirect github.com/gorilla/context v1.1.1 // indirect @@ -22,9 +29,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/klauspost/compress v1.17.2 // indirect - github.com/lib/pq v1.10.9 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/markbates/goth v1.80.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -33,12 +38,10 @@ require ( github.com/rivo/uniseg v0.2.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/shareed2k/goth_fiber v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.19.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.51.0 // indirect @@ -53,6 +56,5 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3225f63..4d616b5 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,37 @@ -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06 h1:W4Yar1SUsPmmA51qoIRb174uDO/Xt3C48MB1YX9Y3vM= github.com/MarceloPetrucio/go-scalar-api-reference v0.0.0-20240521013641-ce5d2efe0e06/go.mod h1:/wotfjM8I3m8NuIHPz3S8k+CCYH80EqDT8ZeNLqMQm0= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg= +github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0= +github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo= github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4= @@ -22,12 +41,12 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= @@ -41,10 +60,12 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -60,11 +81,25 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= @@ -89,6 +124,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV 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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= @@ -108,9 +144,13 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -122,8 +162,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -137,6 +175,8 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= @@ -148,6 +188,8 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= diff --git a/main.go b/main.go index 99c6659..ac59b4c 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,7 @@ import ( "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/utils" @@ -13,7 +14,6 @@ import ( ) func main() { - if len(os.Args) > 1 { args := os.Args[1] @@ -38,11 +38,18 @@ func main() { } authRouter := auth.AuthRouter{ - Controller: auth.AuthController, + Controller: &auth.AuthControllerImpl{ + AuthService: &auth.AuthService, + }, + } + + orgRouter := organizations.OrganizationRouter{ + Controller: organizations.OrganizationControllerImpl{}, } auth.AuthService.RegisterProviders() //register goth providers for authentication // Start the server - s.Ignite(&gistRouter, &authRouter) + s.Setup(&gistRouter, &authRouter, &orgRouter) + s.Ignite() } diff --git a/migrations/000003_organizations.down.sql b/migrations/000003_organizations.down.sql new file mode 100644 index 0000000..a89edfd --- /dev/null +++ b/migrations/000003_organizations.down.sql @@ -0,0 +1,14 @@ +-- Drop the function if it exists +DROP FUNCTION IF EXISTS assert_owner_is_member(); + +-- Remove the foreign key constraint from the 'gists' table +ALTER TABLE gists DROP CONSTRAINT IF EXISTS fk_org_id; + +-- Drop the 'org_id' column from the 'gists' table +ALTER TABLE gists DROP COLUMN IF EXISTS org_id; + +-- Drop the 'member' table if it exists +DROP TABLE IF EXISTS member; + +-- Drop the 'organization' table if it exists +DROP TABLE IF EXISTS organization; diff --git a/migrations/000003_organizations.up.sql b/migrations/000003_organizations.up.sql new file mode 100644 index 0000000..6d14c1f --- /dev/null +++ b/migrations/000003_organizations.up.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS organization ( + org_id SERIAL PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS member ( + member_id SERIAL PRIMARY KEY, + org_id INT NOT NULL, + user_id INT NOT NULL, + role TEXT NOT NULL, + CONSTRAINT fk_org_id FOREIGN KEY (org_id) REFERENCES organization(org_id) ON DELETE CASCADE, + CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE +); + +ALTER TABLE gists ADD COLUMN org_id INT NULL; +ALTER TABLE gists ADD CONSTRAINT fk_org_id FOREIGN KEY (org_id) REFERENCES organization(org_id) ON DELETE CASCADE; + +CREATE OR REPLACE FUNCTION assert_owner_is_member() RETURNS TRIGGER AS $$ +BEGIN + IF NEW.org_id IS NULL THEN + RETURN NEW; + END IF; + IF (SELECT COUNT(*) FROM member WHERE org_id = NEW.org_id AND user_id = NEW.owner) = 0 THEN + RAISE EXCEPTION 'Owner is not a member of the organization'; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER assert_owner_is_member +BEFORE INSERT ON gists +FOR EACH ROW +EXECUTE FUNCTION assert_owner_is_member(); diff --git a/organizations/controller.go b/organizations/controller.go new file mode 100644 index 0000000..f5d793d --- /dev/null +++ b/organizations/controller.go @@ -0,0 +1,54 @@ +package organizations + +import ( + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +type OrganizationControllerImpl struct{} + +type OrganizationValidator struct { + Name string `json:"name"` +} + +func (c *OrganizationControllerImpl) Save() fiber.Handler { + return func(c *fiber.Ctx) error { + org_payload := new(OrganizationValidator) + owner_id := c.Locals("pub").(string) + // + if err := c.BodyParser(org_payload); err != nil { + return c.Status(400).SendString("Request must be valid JSON with fields name as text") + } + org, err := OrganizationService.Save(org_payload.Name, owner_id) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + // + return c.Status(201).JSON(org) + } +} + +func (c *OrganizationControllerImpl) GetAsMember() fiber.Handler { + return func(c *fiber.Ctx) error { + user_id := c.Locals("pub").(string) + log.Info("user_id: ", user_id) + + organizations, err := OrganizationService.GetAsMember(user_id) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + return c.JSON(organizations) + } +} + +func (c *OrganizationControllerImpl) GetByID() fiber.Handler { + return func(c *fiber.Ctx) error { + org_id := c.Params("id") + user_id := c.Locals("pub").(string) + org, err := OrganizationService.GetByID(org_id, user_id) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + return c.JSON(org) + } +} diff --git a/organizations/model.go b/organizations/model.go new file mode 100644 index 0000000..25f79d3 --- /dev/null +++ b/organizations/model.go @@ -0,0 +1,131 @@ +package organizations + +import ( + "database/sql" + "errors" + + "github.com/gistapp/api/storage" + "github.com/gofiber/fiber/v2/log" +) + +type Role string + +const ( + Owner Role = "owner" +) + +type OrganizationSQL struct { + ID sql.NullInt32 + Name sql.NullString +} + +type Organization struct { + ID string `json:"id"` + Name string `json:"name"` + Gists []string `json:"gists,omitempty"` +} + +type OrganizationModel interface { + Save() error +} + +func (o *OrganizationSQL) Save(owner_id string) (*Organization, error) { + row, err := storage.Database.Query("INSERT INTO organization(name) VALUES ($1) RETURNING org_id, name", o.Name.String) + + if err != nil { + log.Error(err) + return nil, errors.New("couldn't create organization") + } + + var organization Organization + + row.Next() + err = row.Scan(&organization.ID, &organization.Name) + if err != nil { + log.Error(err) + return nil, errors.New("couldn't find organization") + } + + _, err = storage.Database.Exec("INSERT INTO member(org_id, user_id, role) VALUES ($1, $2, $3)", organization.ID, owner_id, Owner) + + if err != nil { + log.Error(err) + return nil, errors.New("couldn't create member") + } + return &organization, nil +} + +func (o *OrganizationSQL) Delete() error { + _, err := storage.Database.Exec("DELETE FROM organization WHERE org_id = $1", o.ID.Int32) + if err != nil { + log.Error(err) + return errors.New("couldn't delete organization") + } + return nil +} + +func (o *OrganizationSQL) GetByMember(user_id string) ([]Organization, error) { + query := "SELECT o.org_id, o.name FROM organization o JOIN member ON o.org_id = member.org_id WHERE user_id = $1" + rows, err := storage.Database.Query(query, user_id) + if err != nil { + log.Error(err) + return nil, errors.New("couldn't find organizations") + } + var organizations []Organization + for rows.Next() { + var organization Organization + err = rows.Scan(&organization.ID, &organization.Name) + if err != nil { + log.Error(err) + return nil, errors.New("couldn't find organization") + } + organizations = append(organizations, organization) + } + return organizations, nil +} + +func (o *OrganizationSQL) GetByID(user_id string, org_id string) (*Organization, error) { + query := "SELECT o.org_id, o.name, gists.gist_id FROM organization o JOIN gists ON o.org_id = gists.org_id WHERE o.org_id=$1 AND owner=$2" + log.Info("user is ", user_id) + + rows, err := storage.Database.Query(query, org_id, user_id) + if err != nil { + log.Error(err) + return nil, errors.New("couldn't find organization") + } + + type organization_line struct { + ID string + Name string + Gist string + } + + orgs := []organization_line{} + + for rows.Next() { + organization := new(organization_line) + err = rows.Scan(&organization.ID, &organization.Name, &organization.Gist) + if err != nil { + log.Error(err) + return nil, errors.New("couldn't find organization") + } + + orgs = append(orgs, *organization) + } + + if len(orgs) == 0 { + return nil, errors.New("couldn't find organization") + } + + gists_ids := []string{} + + for _, org := range orgs { + gists_ids = append(gists_ids, org.Gist) + } + + return &Organization{ + Name: orgs[0].Name, + ID: orgs[0].ID, + Gists: gists_ids, + }, nil +} diff --git a/organizations/router.go b/organizations/router.go new file mode 100644 index 0000000..e6a1027 --- /dev/null +++ b/organizations/router.go @@ -0,0 +1,18 @@ +package organizations + +import ( + "github.com/gistapp/api/server" + "github.com/gofiber/fiber/v2" +) + +type OrganizationRouter struct { + Controller OrganizationControllerImpl +} + +func (r *OrganizationRouter) SubscribeRoutes(app *fiber.Router) { + organizations_router := (*app).Group("/orgs", server.AuthNeededMiddleware) + + organizations_router.Post("/", r.Controller.Save()) + organizations_router.Get("/", r.Controller.GetAsMember()) + organizations_router.Get("/:id", r.Controller.GetByID()) +} diff --git a/organizations/service.go b/organizations/service.go new file mode 100644 index 0000000..c3784a0 --- /dev/null +++ b/organizations/service.go @@ -0,0 +1,70 @@ +package organizations + +import ( + "database/sql" + "errors" + + "github.com/gofiber/fiber/v2/log" +) + +type OrganizationServiceImpl struct{} + +func (g *OrganizationServiceImpl) Save(name string, owner_id string) (*Organization, error) { + m := OrganizationSQL{ + ID: sql.NullInt32{ + Valid: false, + Int32: 0, + }, // useless ID + Name: sql.NullString{ + String: name, + Valid: true, + }, + } + + log.Info("saving organization") + + organization, err := m.Save(owner_id) + if err != nil { + return nil, errors.New("couldn't insert into database organization") + } + return organization, nil +} + +// returns a list of organizations that the user is a member of +func (g *OrganizationServiceImpl) GetAsMember(user_id string) ([]Organization, error) { + m := OrganizationSQL{ + ID: sql.NullInt32{ + Valid: false, + Int32: 0, + }, + Name: sql.NullString{ + String: "", + Valid: false, + }, + } + organizations, err := m.GetByMember(user_id) + if err != nil { + return nil, errors.New("couldn't find organizations") + } + return organizations, nil +} + +func (g *OrganizationServiceImpl) GetByID(org_id string, user_id string) (*Organization, error) { + m := OrganizationSQL{ + ID: sql.NullInt32{ + Valid: true, + Int32: 0, + }, + Name: sql.NullString{ + String: "", + Valid: false, + }, + } + organization, err := m.GetByID(user_id, org_id) + if err != nil { + return nil, errors.New("couldn't find organization") + } + return organization, nil +} + +var OrganizationService *OrganizationServiceImpl = &OrganizationServiceImpl{} diff --git a/server/server.go b/server/server.go index 897d4ab..ee94ce3 100644 --- a/server/server.go +++ b/server/server.go @@ -11,7 +11,7 @@ import ( type Server struct { listenAddr string - app *fiber.App + App *fiber.App } type DomainRouter interface { @@ -21,13 +21,13 @@ type DomainRouter interface { func NewServer(listenAddr string) *Server { return &Server{ listenAddr: listenAddr, - app: fiber.New(), + App: fiber.New(), } } -func (s *Server) Ignite(routers ...DomainRouter) { +func (s *Server) Setup(routers ...DomainRouter) { - s.app.Get("/", func(c *fiber.Ctx) error { + s.App.Get("/", func(c *fiber.Ctx) error { htmlContent, err := scalar.ApiReferenceHTML(&scalar.Options{ SpecURL: "./docs/openapi.json", Theme: scalar.ThemeKepler, @@ -44,18 +44,21 @@ func (s *Server) Ignite(routers ...DomainRouter) { return c.Format(htmlContent) }) - s.app.Use(cors.New(cors.Config{ + s.App.Use(cors.New(cors.Config{ AllowCredentials: true, AllowOrigins: utils.Get("FRONTEND_URL"), })) - s.app.Use(logger.New()) + s.App.Use(logger.New()) - custom_router := s.app.Group("/") + custom_router := s.App.Group("/") for _, router := range routers { router.SubscribeRoutes(&custom_router) } - log.Fatal(s.app.Listen(s.listenAddr)) +} + +func (s *Server) Ignite() { + log.Fatal(s.App.Listen(s.listenAddr)) } diff --git a/tests/gists_test.go b/tests/gists_test.go new file mode 100644 index 0000000..3d0de98 --- /dev/null +++ b/tests/gists_test.go @@ -0,0 +1,109 @@ +package tests + +import ( + "database/sql" + "fmt" + "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/utils" + "github.com/gofiber/fiber/v2" +) + +func InitServerGists() *fiber.App { + // Check for command-line arguments + 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 + gist_router := gists.GistRouter{ + Controller: gists.GistController, + } + + auth_router := auth.AuthRouter{ + Controller: &mock.MockAuthController{ + //needs GetUser to fit the auth interface + AuthService: &mock.MockAuthService{}, + }, + } + + organization_router := organizations.OrganizationRouter{ + Controller: organizations.OrganizationControllerImpl{}, + } + + // Initialize the server with the routers + s.Setup(&gist_router, &auth_router, &organization_router) + return s.App +} + +func TestCreateGists(t *testing.T) { + t.Run("Create a new personal gist", func(t *testing.T) { + app := InitServerGists() + authToken := GetAuthToken(t, app) + + body, req := utils.MakeRequest(t, app, "/gists", map[string]string{ + "name": "Test Gist", + "content": "Test content", + }, map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", authToken), + }) + + if req.StatusCode != 201 { + t.Fatalf("Expected status code 201, got %d", req.StatusCode) + } + + fmt.Println(body) + + }) + + t.Run("Create a new organization gist", func(t *testing.T) { + app := InitServerGists() + auth_token := GetAuthToken(t, app) + claims, _ := auth.AuthService.IsAuthenticated(auth_token) + + org_mod := organizations.OrganizationSQL{ + Name: sql.NullString{ + String: "Test Org", + Valid: true, + }, + } + + org, err := org_mod.Save(claims.Pub) + if err != nil { + t.Fatalf("Failed to create organization: %v", err) + } + + payload := map[string]string{ + "name": "Test Gist", + "content": "Test content", + "org_id": org.ID, + } + + body, req := utils.MakeRequest(t, app, "/gists", payload, map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", auth_token), + }) + + if req.StatusCode != 201 { + t.Fatalf("Expected status code 201, got %d", req.StatusCode) + } + + fmt.Println(body) + + DeleteAuthUser(t, auth_token) + + }) +} diff --git a/tests/mock/auth_controller.go b/tests/mock/auth_controller.go new file mode 100644 index 0000000..022af36 --- /dev/null +++ b/tests/mock/auth_controller.go @@ -0,0 +1,64 @@ +package mock + +import ( + "github.com/gistapp/api/auth" + "github.com/gofiber/fiber/v2" +) + +type MockAuthController struct{ + AuthService auth.IAuthService +} + +func (a *MockAuthController) Callback() fiber.Handler { + return func(c *fiber.Ctx) error { + return nil + } +} + +func (a *MockAuthController) Authenticate() fiber.Handler { + return func(c *fiber.Ctx) error { + return nil + } +} + +func (a *MockAuthController) LocalAuth() fiber.Handler { + return func(c *fiber.Ctx) error { + e := new(auth.AuthLocalValidator) + if err := c.BodyParser(e); err != nil { + return c.Status(400).SendString("Request must be valid JSON with field email as text") + } + token, err := a.AuthService.LocalAuth(e.Email) + + if err != nil { + return c.Status(400).SendString(err.Error()) + } + + return c.JSON(fiber.Map{"token": token.Value.String}) + } +} + +func (a *MockAuthController) VerifyAuthToken() fiber.Handler { + return func(c *fiber.Ctx) error { + e := new(auth.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") + } + + token := e.Token + email := e.Email + + jwt_token, err := a.AuthService.VerifyLocalAuthToken(token, email) + + if err != nil { + return c.Status(400).SendString(err.Error()) + } + + token_cookie := new(fiber.Cookie) + token_cookie.Name = "gists.access_token" + token_cookie.HTTPOnly = true + token_cookie.Value = jwt_token + 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 new file mode 100644 index 0000000..14923cc --- /dev/null +++ b/tests/mock/auth_service.go @@ -0,0 +1,132 @@ +package mock + +import ( + "database/sql" + "encoding/json" + "errors" + "strings" + + "github.com/gistapp/api/auth" + "github.com/gistapp/api/user" + "github.com/gistapp/api/utils" + "github.com/gofiber/fiber/v2" + "github.com/markbates/goth" +) + +type MockAuthService struct { +} + +func (m *MockAuthService) Authenticate(c *fiber.Ctx) error { + return nil +} + +func (m *MockAuthService) LocalAuth(email string) (auth.TokenSQL, error) { + token_val := utils.GenToken(6) + token_model := auth.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}, + } + + _, err := token_model.Save() + + return token_model, err + +} + +func (m *MockAuthService) VerifyLocalAuthToken(token string, email string) (string, error) { + token_model := auth.TokenSQL{ + Value: sql.NullString{String: token, Valid: true}, + Keyword: sql.NullString{String: email, Valid: true}, + Type: sql.NullString{String: string(auth.LocalAuth), Valid: true}, + } + token_data, err := token_model.Get() + if err != nil { + return "", err + } + err = token_data.Delete() + if err != nil { + return "", errors.New("couldn't invalidate token") + } + + //now we finish users registration + goth_user := goth.User{ + UserID: email, + Name: strings.Split(email, "@")[0], + Email: email, + AvatarURL: "https://vercel.com/api/www/avatar/?u=" + email + "&s=80", + } + + if user, _, err := m.GetUser(goth_user); err == nil { + jwt_token, err := utils.CreateToken(user.Email, user.ID) + if err != nil { + return "", err + } + return jwt_token, nil + } + + user, err := m.Register(goth_user) + + if err != nil { + return "", err + } + + jwt_token, err := utils.CreateToken(user.Email, user.ID) + + return jwt_token, err +} + +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 (m *MockAuthService) Register(auth_user goth.User) (*user.User, error) { + data, err := json.Marshal(auth_user) + if err != nil { + return nil, errors.New("couldn't marshal user") + } + + user_model := user.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}, + Picture: sql.NullString{String: auth_user.AvatarURL, Valid: true}, + } + + user_data, err := user_model.Save() + + if err != nil { + return nil, err + } + + auth_identity_model := auth.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}, + ProviderID: sql.NullString{String: auth_user.UserID, Valid: true}, + } + + _, err = auth_identity_model.Save() + return user_data, err +} + +func (a *MockAuthService) IsAuthenticated(token string) (*auth.JWTClaim, error) { + claims, err := utils.VerifyJWT(token) + + if err != nil { + return nil, err + } + + jwtClaim := new(auth.JWTClaim) + jwtClaim.Pub = claims["pub"].(string) + jwtClaim.Email = claims["email"].(string) + + return jwtClaim, nil +} + +func (a *MockAuthService) RegisterProviders() { +} diff --git a/tests/organization_test.go b/tests/organization_test.go new file mode 100644 index 0000000..f565611 --- /dev/null +++ b/tests/organization_test.go @@ -0,0 +1,99 @@ +package tests + +import ( + "database/sql" + "fmt" + "os" + "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/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" { + 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 + gist_router := gists.GistRouter{ + Controller: gists.GistController, + } + + auth_router := auth.AuthRouter{ + Controller: &mock.MockAuthController{ + AuthService: &mock.MockAuthService{}, + }, + } + + organization_router := organizations.OrganizationRouter{ + Controller: organizations.OrganizationControllerImpl{}, + } + + // Initialize the server with the routers + s.Setup(&gist_router, &auth_router, &organization_router) + return s.App +} + +func TestCreateOrganization(t *testing.T) { + t.Run("Create organization", func(t *testing.T) { + app := InitServerOrgs() + if app == nil { + t.Fatal("Failed to initialize the application") + } + + // Begin the sign-up process + // + auth_token := GetAuthToken(t, app) + fmt.Println(auth_token) + // + // // Create a new organization + org_payload := map[string]string{ + "name": "Test Organization", + } + fmt.Println(org_payload) + // + body, _ := utils.MakeRequest(t, app, "/orgs", org_payload, map[string]string{ + "Authorization": "Bearer " + auth_token, + }) + // + if body["name"] != "Test Organization" { + t.Errorf("Expected organization name to be 'Test Organization', got %s", body["name"]) + } + + // cleanup + id, err := strconv.ParseInt(body["id"], 10, 32) + + if err != nil { + t.Errorf("Failed to parse organization ID: %v", err) + } + + org := organizations.OrganizationSQL{ + ID: sql.NullInt32{ + Int32: int32(id), + Valid: true, + }, + } + if err = org.Delete(); err != nil { + t.Errorf("Failed to delete organization: %v", err) + } + DeleteAuthUser(t, auth_token) + }) +} diff --git a/tests/utils.go b/tests/utils.go new file mode 100644 index 0000000..af9c2df --- /dev/null +++ b/tests/utils.go @@ -0,0 +1,48 @@ +package tests + +import ( + "database/sql" + "testing" + + "github.com/gistapp/api/auth" + "github.com/gistapp/api/user" + "github.com/gistapp/api/utils" + "github.com/gofiber/fiber/v2" +) + +func GetAuthToken(t *testing.T, app *fiber.App) string { + // Begin the sign-up process + beginPayload := map[string]string{ + "email": "test@test.com", + } + respBody, _ := utils.MakeRequest(t, app, "/auth/local/begin", beginPayload, nil) + token := respBody["token"] + + // Verify the sign-up process + verifyPayload := map[string]string{ + "email": "test@test.com", + "token": token, + } + _, resp := utils.MakeRequest(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) + + user := user.UserSQL{ + ID: sql.NullString{ + Valid: true, + String: claims.Pub, + }, + } + + err := user.Delete() + + if err != nil { + t.Fatalf("Failed to delete user: %v", err) + } + +} diff --git a/user/model.go b/user/model.go index d2772d1..d2d9bae 100644 --- a/user/model.go +++ b/user/model.go @@ -66,3 +66,12 @@ func (u *UserSQL) GetByEmail() (*User, error) { 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 { + log.Error(err) + return errors.New("couldn't delete user") + } + return nil +} diff --git a/utils/env.go b/utils/env.go index 46ddbf8..ed50588 100644 --- a/utils/env.go +++ b/utils/env.go @@ -20,7 +20,6 @@ func Get(key string) string { if !ok { msg := fmt.Sprintf("Env variable '%q' not found\n", key) panic(msg) - return "" } return value diff --git a/utils/http_testing.go b/utils/http_testing.go new file mode 100644 index 0000000..38b89fb --- /dev/null +++ b/utils/http_testing.go @@ -0,0 +1,50 @@ +package utils + +import ( + "encoding/json" + "net/http" + "strings" + "testing" + + "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) { + // Marshal payload to JSON + jsonPayload, err := json.Marshal(payload) + if err != nil { + t.Fatalf("Failed to marshal JSON: %v", err) + } + + // Create a new HTTP request + req, err := http.NewRequest("POST", url, strings.NewReader(string(jsonPayload))) + 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) + } + } + + // Test the request using Fiber's testing framework + resp, err := app.Test(req) + if err != nil { + t.Fatalf("Failed to execute request: %v", err) + } + + if resp.StatusCode != 200 && resp.StatusCode != 201 { + t.Errorf("Expected status code 200 or 201, got %d", resp.StatusCode) + } + + // Decode the response body into a map + respBody := make(map[string]string) + if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil { + t.Fatalf("Failed to decode response: %v", err) + } + + return respBody, resp +}