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..7c181f8 100644 --- a/service/api/api-handler.go +++ b/service/api/api-handler.go @@ -8,6 +8,11 @@ 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("/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 eeb7f2a..6e0d03f 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" ) @@ -32,19 +33,40 @@ 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) (bool, error) { - if b.token == uid { - return b.Authorized(db) +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 } - return false, nil + if !user_exists { + return reqcontext.USER_NOT_FOUND, nil + } + + if b.token == uid { + auth, err := b.Authorized(db) + + if err != nil { + return -1, err + } + + return auth, nil + } + return reqcontext.FORBIDDEN, nil } diff --git a/service/api/authorization/auth-manager.go b/service/api/authorization/auth-manager.go index 9be6f9a..97cc803 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,26 @@ 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 + } + // 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/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..860f5f2 100644 --- a/service/api/put-updateusername.go +++ b/service/api/put-updateusername.go @@ -4,11 +4,36 @@ import ( "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" ) 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 + if !helpers.DecodeJsonOrBadRequest(r.Body, w, &req, rt.baseLogger) { + return + } + //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..0513367 100644 --- a/service/api/reqcontext/request-context.go +++ b/service/api/reqcontext/request-context.go @@ -12,6 +12,15 @@ import ( "github.com/sirupsen/logrus" ) +type AuthStatus int + +const ( + AUTHORIZED = 0 + UNAUTHORIZED = 1 + FORBIDDEN = 2 + USER_NOT_FOUND = 3 +) + // RequestContext is the context of the request, for request-dependent parameters type RequestContext struct { // ReqUUID is the request unique ID @@ -26,5 +35,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..75850ad 100644 --- a/service/database/database.go +++ b/service/database/database.go @@ -34,14 +34,17 @@ import ( "database/sql" "errors" "fmt" + + "github.com/notherealmarco/WASAPhoto/service/structures" ) // AppDatabase is the high level interface for the DB 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) + 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 4ac22a2..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 @@ -11,7 +12,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 +39,34 @@ 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 +} + +// 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 new file mode 100644 index 0000000..7b0f2b8 --- /dev/null +++ b/service/structures/api-structures.go @@ -0,0 +1,10 @@ +package structures + +type UserDetails struct { + Name string `json:"name"` +} + +type UIDName struct { + UID string `json:"user_id"` + Name string `json:"name"` +}