From 7c2c993dc3c3ca8bccafd18d75a56fe178b96301 Mon Sep 17 00:00:00 2001 From: Marco Realacci Date: Fri, 18 Nov 2022 17:18:46 +0100 Subject: [PATCH 1/2] Add update username method --- service/api/api-context-wrapper.go | 1 - service/api/api-handler.go | 3 +++ service/api/authorization/auth-bearer.go | 17 ++++++++++++++--- service/api/authorization/auth-manager.go | 20 ++++++++++++++++++++ service/api/post-session.go | 2 +- service/api/put-updateusername.go | 23 ++++++++++++++++++++++- service/api/reqcontext/request-context.go | 10 +++++++++- service/database/database.go | 2 +- service/database/db-users.go | 8 +++++++- service/structures/api-structures.go | 5 +++++ 10 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 service/structures/api-structures.go diff --git a/service/api/api-context-wrapper.go b/service/api/api-context-wrapper.go index 5c732be..fb81fe0 100644 --- a/service/api/api-context-wrapper.go +++ b/service/api/api-context-wrapper.go @@ -28,7 +28,6 @@ func (rt *_router) wrap(fn httpRouterHandler) func(http.ResponseWriter, *http.Re if err != nil { rt.baseLogger.WithError(err).Info("User not authorized") - w.WriteHeader(http.StatusUnauthorized) return } diff --git a/service/api/api-handler.go b/service/api/api-handler.go index 49ad354..06c5972 100644 --- a/service/api/api-handler.go +++ b/service/api/api-handler.go @@ -8,6 +8,9 @@ import ( func (rt *_router) Handler() http.Handler { // Register routes rt.router.POST("/session", rt.wrap(rt.PostSession)) + + rt.router.PUT("/users/:user_id/username", rt.wrap(rt.UpdateUsername)) + rt.router.GET("/", rt.getHelloWorld) rt.router.GET("/context", rt.wrap(rt.getContextReply)) diff --git a/service/api/authorization/auth-bearer.go b/service/api/authorization/auth-bearer.go index eeb7f2a..037f131 100644 --- a/service/api/authorization/auth-bearer.go +++ b/service/api/authorization/auth-bearer.go @@ -4,6 +4,7 @@ import ( "errors" "strings" + "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" "github.com/notherealmarco/WASAPhoto/service/database" ) @@ -42,9 +43,19 @@ func (b *BearerAuth) Authorized(db database.AppDatabase) (bool, error) { return state, nil } -func (b *BearerAuth) UserAuthorized(db database.AppDatabase, uid string) (bool, error) { +func (b *BearerAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error) { if b.token == uid { - return b.Authorized(db) + auth, err := b.Authorized(db) + + if err != nil { + return -1, err + } + + if auth { + return reqcontext.AUTHORIZED, nil + } else { + return reqcontext.UNAUTHORIZED, nil + } } - return false, nil + return reqcontext.FORBIDDEN, nil } diff --git a/service/api/authorization/auth-manager.go b/service/api/authorization/auth-manager.go index 9be6f9a..f6c4f2d 100644 --- a/service/api/authorization/auth-manager.go +++ b/service/api/authorization/auth-manager.go @@ -2,8 +2,10 @@ package authorization import ( "errors" + "net/http" "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" + "github.com/notherealmarco/WASAPhoto/service/database" ) func BuildAuth(header string) (reqcontext.Authorization, error) { @@ -16,3 +18,21 @@ func BuildAuth(header string) (reqcontext.Authorization, error) { } return auth, nil } + +func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error), uid string, db database.AppDatabase, w http.ResponseWriter) bool { + auth, err := f(db, uid) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + // todo: log error and write it to the response + return false + } + if auth == reqcontext.UNAUTHORIZED { + w.WriteHeader(http.StatusUnauthorized) + return false + } + if auth == reqcontext.FORBIDDEN { + w.WriteHeader(http.StatusForbidden) + return false + } + return true +} diff --git a/service/api/post-session.go b/service/api/post-session.go index 87e812c..3d2761b 100644 --- a/service/api/post-session.go +++ b/service/api/post-session.go @@ -14,7 +14,7 @@ type _reqbody struct { } type _respbody struct { - UID string `json:"uid"` + UID string `json:"user_id"` } // getContextReply is an example of HTTP endpoint that returns "Hello World!" as a plain text. The signature of this diff --git a/service/api/put-updateusername.go b/service/api/put-updateusername.go index dafb16f..dbc002a 100644 --- a/service/api/put-updateusername.go +++ b/service/api/put-updateusername.go @@ -1,14 +1,35 @@ package api import ( + "encoding/json" "net/http" "github.com/julienschmidt/httprouter" + "github.com/notherealmarco/WASAPhoto/service/api/authorization" "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" + "github.com/notherealmarco/WASAPhoto/service/structures" ) func (rt *_router) UpdateUsername(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { - auth, err := ctx.Auth.UserAuthorized(rt.db, r.URL.Path // todo: prendere il coso giusto dal path) + uid := ps.ByName("user_id") + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w) { + return + } + var req structures.UserDetails + err := json.NewDecoder(r.Body).Decode(&req) //todo: capire se serve close + if err != nil { + w.WriteHeader(http.StatusBadRequest) // todo: move to DecodeOrBadRequest helper + return + } + + err = rt.db.UpdateUsername(uid, req.Name) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) // todo: is not ok, maybe let's use a helper + return + } + + w.WriteHeader(http.StatusNoContent) // todo: change to 204 also in API spec } diff --git a/service/api/reqcontext/request-context.go b/service/api/reqcontext/request-context.go index df1c7e7..b0be6c8 100644 --- a/service/api/reqcontext/request-context.go +++ b/service/api/reqcontext/request-context.go @@ -12,6 +12,14 @@ import ( "github.com/sirupsen/logrus" ) +type AuthStatus int + +const ( + AUTHORIZED = 0 + UNAUTHORIZED = 1 + FORBIDDEN = 2 +) + // RequestContext is the context of the request, for request-dependent parameters type RequestContext struct { // ReqUUID is the request unique ID @@ -26,5 +34,5 @@ type RequestContext struct { type Authorization interface { GetType() string Authorized(db database.AppDatabase) (bool, error) - UserAuthorized(db database.AppDatabase, uid string) (bool, error) + UserAuthorized(db database.AppDatabase, uid string) (AuthStatus, error) } diff --git a/service/database/database.go b/service/database/database.go index 93f69f5..c8f53e7 100644 --- a/service/database/database.go +++ b/service/database/database.go @@ -40,7 +40,7 @@ import ( type AppDatabase interface { UserExists(uid string) (bool, error) GetUserID(name string) (string, error) - SetName(name string) error + UpdateUsername(uid, name string) error CreateUser(name string) (string, error) FollowUser(uid string, follow string) error UnfollowUser(uid string, unfollow string) error diff --git a/service/database/db-users.go b/service/database/db-users.go index 4ac22a2..472619b 100644 --- a/service/database/db-users.go +++ b/service/database/db-users.go @@ -11,7 +11,7 @@ import ( // Check if user exists func (db *appdbimpl) UserExists(uid string) (bool, error) { var name string - err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, name).Scan(&name) + err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name) if db_errors.EmptySet(err) { return false, nil @@ -38,6 +38,12 @@ func (db *appdbimpl) CreateUser(name string) (string, error) { return uid.String(), err } +// Update username +func (db *appdbimpl) UpdateUsername(uid string, name string) error { + _, err := db.c.Exec(`UPDATE "users" SET "name" = ? WHERE "uid" = ?`, name, uid) + return err +} + // Follow a user func (db *appdbimpl) FollowUser(uid string, follow string) error { _, err := db.c.Exec(`INSERT INTO "follows" ("follower", "followed") VALUES (?, ?)`, uid, follow) diff --git a/service/structures/api-structures.go b/service/structures/api-structures.go new file mode 100644 index 0000000..c5020df --- /dev/null +++ b/service/structures/api-structures.go @@ -0,0 +1,5 @@ +package structures + +type UserDetails struct { + Name string `json:"name"` +} From e8047c77a0dd0b0137de796bb1fdae06c4d0eed6 Mon Sep 17 00:00:00 2001 From: Marco Realacci Date: Fri, 18 Nov 2022 18:58:12 +0100 Subject: [PATCH 2/2] Add get followers --- service/api/api-handler.go | 2 + service/api/authorization/auth-bearer.go | 27 +++++++++---- service/api/authorization/auth-manager.go | 5 +++ service/api/followers.go | 49 +++++++++++++++++++++++ service/api/helpers/api-helpers.go | 42 +++++++++++++++++++ service/api/put-updateusername.go | 16 +++++--- service/api/reqcontext/request-context.go | 7 ++-- service/database/database.go | 3 ++ service/database/db-users.go | 23 +++++++++++ service/structures/api-structures.go | 5 +++ 10 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 service/api/followers.go create mode 100644 service/api/helpers/api-helpers.go diff --git a/service/api/api-handler.go b/service/api/api-handler.go index 06c5972..7c181f8 100644 --- a/service/api/api-handler.go +++ b/service/api/api-handler.go @@ -11,6 +11,8 @@ func (rt *_router) Handler() http.Handler { rt.router.PUT("/users/:user_id/username", rt.wrap(rt.UpdateUsername)) + rt.router.GET("/users/:user_id/followers", rt.wrap(rt.GetFollowers)) + rt.router.GET("/", rt.getHelloWorld) rt.router.GET("/context", rt.wrap(rt.getContextReply)) diff --git a/service/api/authorization/auth-bearer.go b/service/api/authorization/auth-bearer.go index 037f131..6e0d03f 100644 --- a/service/api/authorization/auth-bearer.go +++ b/service/api/authorization/auth-bearer.go @@ -33,17 +33,32 @@ func (b *BearerAuth) GetToken() string { return b.token } -func (b *BearerAuth) Authorized(db database.AppDatabase) (bool, error) { +func (b *BearerAuth) Authorized(db database.AppDatabase) (reqcontext.AuthStatus, error) { // this is the way we manage authorization, the bearer token is the user id state, err := db.UserExists(b.token) if err != nil { - return false, err + return reqcontext.UNAUTHORIZED, err } - return state, nil + + if state { + return reqcontext.AUTHORIZED, nil + } + return reqcontext.UNAUTHORIZED, nil } func (b *BearerAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error) { + + // If uid is not a valid user, return USER_NOT_FOUND + user_exists, err := db.UserExists(uid) + + if err != nil { + return reqcontext.UNAUTHORIZED, err + } + if !user_exists { + return reqcontext.USER_NOT_FOUND, nil + } + if b.token == uid { auth, err := b.Authorized(db) @@ -51,11 +66,7 @@ func (b *BearerAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcon return -1, err } - if auth { - return reqcontext.AUTHORIZED, nil - } else { - return reqcontext.UNAUTHORIZED, nil - } + return auth, nil } return reqcontext.FORBIDDEN, nil } diff --git a/service/api/authorization/auth-manager.go b/service/api/authorization/auth-manager.go index f6c4f2d..97cc803 100644 --- a/service/api/authorization/auth-manager.go +++ b/service/api/authorization/auth-manager.go @@ -34,5 +34,10 @@ func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcont w.WriteHeader(http.StatusForbidden) return false } + // requested user is not found -> 404 as the resource is not found + if auth == reqcontext.USER_NOT_FOUND { + w.WriteHeader(http.StatusNotFound) + return false + } return true } diff --git a/service/api/followers.go b/service/api/followers.go new file mode 100644 index 0000000..7a9e1a3 --- /dev/null +++ b/service/api/followers.go @@ -0,0 +1,49 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/notherealmarco/WASAPhoto/service/api/helpers" + "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" +) + +func (rt *_router) GetFollowers(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + + if !helpers.VerifyUserOrNotFound(rt.db, uid, w) { + return + } + + followers, err := rt.db.GetUserFollowers(uid) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) // todo: is not ok, maybe let's use a helper + return + } + + w.Header().Set("content-type", "application/json") + err = json.NewEncoder(w).Encode(&followers) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) // todo: is not ok + return + } +} + +func (rt *_router) PutFollow(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + followed := ps.ByName("follower_uid") + + err := rt.db.FollowUser(uid, followed) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) // todo: is not ok, maybe let's use a helper + return + } + + w.WriteHeader(http.StatusNoContent) // todo: change to 204 also in API spec +} diff --git a/service/api/helpers/api-helpers.go b/service/api/helpers/api-helpers.go new file mode 100644 index 0000000..5302cc6 --- /dev/null +++ b/service/api/helpers/api-helpers.go @@ -0,0 +1,42 @@ +package helpers + +import ( + "encoding/json" + "io" + "net/http" + + "github.com/notherealmarco/WASAPhoto/service/database" + "github.com/sirupsen/logrus" +) + +func DecodeJsonOrBadRequest(r io.Reader, w http.ResponseWriter, v interface{}, l logrus.FieldLogger) bool { + + err := json.NewDecoder(r).Decode(v) + if err != nil { + SendInternalError(err, "Error decoding json", w, l) + return false + } + return true +} + +func VerifyUserOrNotFound(db database.AppDatabase, uid string, w http.ResponseWriter) bool { + + user_exists, err := db.UserExists(uid) + + if err != nil { + SendInternalError(err, "Error verifying user existence", w, nil) + return false + } + + if !user_exists { + w.WriteHeader(http.StatusNotFound) + return false + } + return true +} + +func SendInternalError(err error, description string, w http.ResponseWriter, l logrus.FieldLogger) { + w.WriteHeader(http.StatusInternalServerError) + l.WithError(err).Error(description) + w.Write([]byte(description)) // todo: maybe in json. But it's not important to send the full error to the client +} diff --git a/service/api/put-updateusername.go b/service/api/put-updateusername.go index dbc002a..860f5f2 100644 --- a/service/api/put-updateusername.go +++ b/service/api/put-updateusername.go @@ -1,11 +1,11 @@ package api import ( - "encoding/json" "net/http" "github.com/julienschmidt/httprouter" "github.com/notherealmarco/WASAPhoto/service/api/authorization" + "github.com/notherealmarco/WASAPhoto/service/api/helpers" "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" "github.com/notherealmarco/WASAPhoto/service/structures" ) @@ -17,14 +17,18 @@ func (rt *_router) UpdateUsername(w http.ResponseWriter, r *http.Request, ps htt return } var req structures.UserDetails - err := json.NewDecoder(r.Body).Decode(&req) //todo: capire se serve close - - if err != nil { - w.WriteHeader(http.StatusBadRequest) // todo: move to DecodeOrBadRequest helper + if !helpers.DecodeJsonOrBadRequest(r.Body, w, &req, rt.baseLogger) { return } - err = rt.db.UpdateUsername(uid, req.Name) + //err := json.NewDecoder(r.Body).Decode(&req) //todo: capire se serve close + + //if err != nil { + // w.WriteHeader(http.StatusBadRequest) // todo: move to DecodeOrBadRequest helper + // return + //} + + err := rt.db.UpdateUsername(uid, req.Name) if err != nil { w.WriteHeader(http.StatusInternalServerError) // todo: is not ok, maybe let's use a helper diff --git a/service/api/reqcontext/request-context.go b/service/api/reqcontext/request-context.go index b0be6c8..0513367 100644 --- a/service/api/reqcontext/request-context.go +++ b/service/api/reqcontext/request-context.go @@ -15,9 +15,10 @@ import ( type AuthStatus int const ( - AUTHORIZED = 0 - UNAUTHORIZED = 1 - FORBIDDEN = 2 + AUTHORIZED = 0 + UNAUTHORIZED = 1 + FORBIDDEN = 2 + USER_NOT_FOUND = 3 ) // RequestContext is the context of the request, for request-dependent parameters diff --git a/service/database/database.go b/service/database/database.go index c8f53e7..75850ad 100644 --- a/service/database/database.go +++ b/service/database/database.go @@ -34,6 +34,8 @@ import ( "database/sql" "errors" "fmt" + + "github.com/notherealmarco/WASAPhoto/service/structures" ) // AppDatabase is the high level interface for the DB @@ -42,6 +44,7 @@ type AppDatabase interface { GetUserID(name string) (string, error) UpdateUsername(uid, name string) error CreateUser(name string) (string, error) + GetUserFollowers(uid string) ([]structures.UIDName, error) FollowUser(uid string, follow string) error UnfollowUser(uid string, unfollow string) error BanUser(uid string, ban string) error diff --git a/service/database/db-users.go b/service/database/db-users.go index 472619b..82120e3 100644 --- a/service/database/db-users.go +++ b/service/database/db-users.go @@ -3,6 +3,7 @@ package database import ( "github.com/gofrs/uuid" "github.com/notherealmarco/WASAPhoto/service/database/db_errors" + "github.com/notherealmarco/WASAPhoto/service/structures" ) //Check if user exists and if exists return the user id by username @@ -44,6 +45,28 @@ func (db *appdbimpl) UpdateUsername(uid string, name string) error { return err } +// Get user followers +func (db *appdbimpl) GetUserFollowers(uid string) ([]structures.UIDName, error) { + rows, err := db.c.Query(`SELECT "follower", "user.name" FROM "follows", "users" + WHERE "follows.follower" = "users.uid" + AND "followed" = ?`, uid) + if err != nil { + return nil, err + } + + var followers []structures.UIDName = make([]structures.UIDName, 0) + for rows.Next() { + var uid string + var name string + err = rows.Scan(&uid, &name) + if err != nil { + return nil, err + } + followers = append(followers, structures.UIDName{UID: uid, Name: name}) + } + return followers, nil +} + // Follow a user func (db *appdbimpl) FollowUser(uid string, follow string) error { _, err := db.c.Exec(`INSERT INTO "follows" ("follower", "followed") VALUES (?, ?)`, uid, follow) diff --git a/service/structures/api-structures.go b/service/structures/api-structures.go index c5020df..7b0f2b8 100644 --- a/service/structures/api-structures.go +++ b/service/structures/api-structures.go @@ -3,3 +3,8 @@ package structures type UserDetails struct { Name string `json:"name"` } + +type UIDName struct { + UID string `json:"user_id"` + Name string `json:"name"` +}