diff --git a/cmd/webapi/load-configuration.go b/cmd/webapi/load-configuration.go index a5d13a6..e9d17a7 100644 --- a/cmd/webapi/load-configuration.go +++ b/cmd/webapi/load-configuration.go @@ -3,11 +3,12 @@ package main import ( "errors" "fmt" - "github.com/ardanlabs/conf" - "gopkg.in/yaml.v2" "io" "os" "time" + + "github.com/ardanlabs/conf" + "gopkg.in/yaml.v2" ) // WebAPIConfiguration describes the web API configuration. This structure is automatically parsed by @@ -27,6 +28,9 @@ type WebAPIConfiguration struct { DB struct { Filename string `conf:"default:/tmp/decaf.db"` } + Data struct { + Path string `conf:"default:/tmp/wasaphoto"` + } } // loadConfiguration creates a WebAPIConfiguration starting from flags, environment variables and configuration file. diff --git a/cmd/webapi/main.go b/cmd/webapi/main.go index 853921a..0abe1c9 100644 --- a/cmd/webapi/main.go +++ b/cmd/webapi/main.go @@ -28,17 +28,18 @@ import ( "database/sql" "errors" "fmt" - "github.com/notherealmarco/WASAPhoto/service/api" - "github.com/notherealmarco/WASAPhoto/service/database" - "github.com/notherealmarco/WASAPhoto/service/globaltime" - "github.com/ardanlabs/conf" - _ "github.com/mattn/go-sqlite3" - "github.com/sirupsen/logrus" "math/rand" "net/http" "os" "os/signal" "syscall" + + "github.com/ardanlabs/conf" + _ "github.com/mattn/go-sqlite3" + "github.com/notherealmarco/WASAPhoto/service/api" + "github.com/notherealmarco/WASAPhoto/service/database" + "github.com/notherealmarco/WASAPhoto/service/globaltime" + "github.com/sirupsen/logrus" ) // main is the program entry point. The only purpose of this function is to call run() and set the exit code if there is @@ -113,6 +114,7 @@ func run() error { apirouter, err := api.New(api.Config{ Logger: logger, Database: db, + DataPath: cfg.Data.Path, }) if err != nil { logger.WithError(err).Error("error creating the API server instance") diff --git a/service/api/api-context-wrapper.go b/service/api/api-context-wrapper.go index fb81fe0..6183893 100644 --- a/service/api/api-context-wrapper.go +++ b/service/api/api-context-wrapper.go @@ -27,8 +27,9 @@ func (rt *_router) wrap(fn httpRouterHandler) func(http.ResponseWriter, *http.Re auth, err := authorization.BuildAuth(r.Header.Get("Authorization")) if err != nil { + auth = authorization.BuildAnonymous() rt.baseLogger.WithError(err).Info("User not authorized") - return + // is not an error, just a not logged in user! } var ctx = reqcontext.RequestContext{ diff --git a/service/api/api-handler.go b/service/api/api-handler.go index 7c181f8..697671b 100644 --- a/service/api/api-handler.go +++ b/service/api/api-handler.go @@ -11,7 +11,20 @@ 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("/users/:user_id/followers", rt.wrap(rt.GetFollowersFollowing)) + rt.router.PUT("/users/:user_id/followers/:follower_uid", rt.wrap(rt.PutFollow)) + rt.router.DELETE("/users/:user_id/followers/:follower_uid", rt.wrap(rt.DeleteFollow)) + rt.router.GET("/users/:user_id/following", rt.wrap(rt.GetFollowersFollowing)) + + rt.router.PUT("/users/:user_id/bans/:ban_uid", rt.wrap(rt.PutBan)) + rt.router.DELETE("/users/:user_id/bans/:ban_uid", rt.wrap(rt.DeleteBan)) + + rt.router.POST("/users/:user_id/photos", rt.wrap(rt.PostPhoto)) + rt.router.GET("/users/:user_id/photos/:photo_id", rt.wrap(rt.GetPhoto)) + rt.router.DELETE("/users/:user_id/photos/:photo_id", rt.wrap(rt.DeletePhoto)) + + rt.router.PUT("/users/:user_id/photos/:photo_id/likes/:liker_uid", rt.wrap(rt.PutDeleteLike)) + rt.router.DELETE("/users/:user_id/photos/:photo_id/likes/:liker_uid", rt.wrap(rt.PutDeleteLike)) rt.router.GET("/", rt.getHelloWorld) rt.router.GET("/context", rt.wrap(rt.getContextReply)) diff --git a/service/api/api.go b/service/api/api.go index e7e085e..7b1c39c 100644 --- a/service/api/api.go +++ b/service/api/api.go @@ -38,10 +38,11 @@ package api import ( "errors" - "github.com/notherealmarco/WASAPhoto/service/database" - "github.com/julienschmidt/httprouter" - "github.com/sirupsen/logrus" "net/http" + + "github.com/julienschmidt/httprouter" + "github.com/notherealmarco/WASAPhoto/service/database" + "github.com/sirupsen/logrus" ) // Config is used to provide dependencies and configuration to the New function. @@ -51,6 +52,8 @@ type Config struct { // Database is the instance of database.AppDatabase where data are saved Database database.AppDatabase + + DataPath string } // Router is the package API interface representing an API handler builder @@ -82,6 +85,7 @@ func New(cfg Config) (Router, error) { router: router, baseLogger: cfg.Logger, db: cfg.Database, + dataPath: cfg.DataPath, }, nil } @@ -93,4 +97,6 @@ type _router struct { baseLogger logrus.FieldLogger db database.AppDatabase + + dataPath string } diff --git a/service/api/authorization/auth-anonymous.go b/service/api/authorization/auth-anonymous.go new file mode 100644 index 0000000..9fbeea3 --- /dev/null +++ b/service/api/authorization/auth-anonymous.go @@ -0,0 +1,31 @@ +// This identity provider represents non logged-in users. + +package authorization + +import ( + "github.com/notherealmarco/WASAPhoto/service/api/reqcontext" + "github.com/notherealmarco/WASAPhoto/service/database" +) + +type AnonymousAuth struct { +} + +func BuildAnonymous() *AnonymousAuth { + return &AnonymousAuth{} +} + +func (u *AnonymousAuth) GetType() string { + return "Anonymous" +} + +func (u *AnonymousAuth) Authorized(db database.AppDatabase) (reqcontext.AuthStatus, error) { + return reqcontext.UNAUTHORIZED, nil +} + +func (u *AnonymousAuth) UserAuthorized(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error) { + return reqcontext.UNAUTHORIZED, nil +} + +func (u *AnonymousAuth) GetUserID() string { + return "" +} diff --git a/service/api/authorization/auth-bearer.go b/service/api/authorization/auth-bearer.go index 6e0d03f..633772d 100644 --- a/service/api/authorization/auth-bearer.go +++ b/service/api/authorization/auth-bearer.go @@ -29,7 +29,7 @@ func BuildBearer(header string) (*BearerAuth, error) { return &BearerAuth{token: header[7:]}, nil } -func (b *BearerAuth) GetToken() string { +func (b *BearerAuth) GetUserID() string { return b.token } diff --git a/service/api/authorization/auth-manager.go b/service/api/authorization/auth-manager.go index 97cc803..dbe1d30 100644 --- a/service/api/authorization/auth-manager.go +++ b/service/api/authorization/auth-manager.go @@ -19,7 +19,7 @@ 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 { +func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcontext.AuthStatus, error), uid string, db database.AppDatabase, w http.ResponseWriter, notFoundStatus int) bool { auth, err := f(db, uid) if err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -36,7 +36,7 @@ func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcont } // requested user is not found -> 404 as the resource is not found if auth == reqcontext.USER_NOT_FOUND { - w.WriteHeader(http.StatusNotFound) + w.WriteHeader(notFoundStatus) return false } return true diff --git a/service/api/bans.go b/service/api/bans.go new file mode 100644 index 0000000..95745e2 --- /dev/null +++ b/service/api/bans.go @@ -0,0 +1,70 @@ +package api + +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/database" +) + +func (rt *_router) PutBan(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + banned := ps.ByName("ban_uid") + + // send error if the user has no permission to perform this action + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) { + return + } + + if uid == banned { + helpers.SendBadRequest(w, "You cannot ban yourself", rt.baseLogger) + return + } + + status, err := rt.db.BanUser(uid, banned) + + if err != nil { + helpers.SendInternalError(err, "Database error: BanUser", w, rt.baseLogger) + return + } + + if status == database.ERR_NOT_FOUND { + helpers.SendBadRequest(w, "You are trying to ban a non-existent user", rt.baseLogger) + return + } + + if status == database.ERR_EXISTS { + w.WriteHeader(http.StatusNoContent) + return + } + + helpers.SendStatus(http.StatusCreated, w, "Success", rt.baseLogger) +} + +func (rt *_router) DeleteBan(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + uid := ps.ByName("user_id") + banned := ps.ByName("ban_uid") + + // send error if the user has no permission to perform this action + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) { + return + } + + status, err := rt.db.UnbanUser(uid, banned) + + if err != nil { + helpers.SendInternalError(err, "Database error: UnbanUser", w, rt.baseLogger) + return + } + + if status == database.ERR_NOT_FOUND { + helpers.SendNotFound(w, "User not banned", rt.baseLogger) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/service/api/comments.go b/service/api/comments.go new file mode 100644 index 0000000..5a701d6 --- /dev/null +++ b/service/api/comments.go @@ -0,0 +1,167 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" + + "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/database" +) + +type reqbody struct { + UID string `json:"user_id"` + Comment string `json:"comment"` +} + +func (rt *_router) GetComments(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + // get the user id from the url + uid := ps.ByName("user_id") + photo_id, err := strconv.ParseInt(ps.ByName("photo_id"), 10, 64) + + if err != nil { + helpers.SendBadRequest(w, "Invalid photo id", rt.baseLogger) + return + } + + // send 404 if the user does not exist + if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) { + return + } + + // get the user's comments + success, comments, err := rt.db.GetComments(uid, photo_id) + + if err != nil { + helpers.SendInternalError(err, "Database error: GetComments", w, rt.baseLogger) + return + } + + if success == database.ERR_NOT_FOUND { + helpers.SendNotFound(w, "User or photo not found", rt.baseLogger) + return + } + + // send the response + err = json.NewEncoder(w).Encode(comments) + + if err != nil { + helpers.SendInternalError(err, "Error encoding comments", w, rt.baseLogger) + return + } +} + +func (rt *_router) PostComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + + photo_id, err := strconv.ParseInt(ps.ByName("photo_id"), 10, 64) + if err != nil { + helpers.SendBadRequestError(err, "Bad photo_id", w, rt.baseLogger) + return + } + + // get the comment from the request + var request_body reqbody + if !helpers.DecodeJsonOrBadRequest(r.Body, w, &request_body, rt.baseLogger) { + return + } + + // check if the user is authorized to post a comment + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, request_body.UID, rt.db, w, http.StatusBadRequest) { + return + } + + // add the comment to the database + success, err := rt.db.PostComment(uid, photo_id, request_body.UID, request_body.Comment) + + if err != nil { + helpers.SendInternalError(err, "Database error: PostComment", w, rt.baseLogger) + return + } + + // if user or photo does not exist, send 404 + if success == database.ERR_NOT_FOUND { + helpers.SendNotFound(w, "User or photo not found", rt.baseLogger) + return + } + + // send the response + helpers.SendStatus(http.StatusCreated, w, "Comment created with success", rt.baseLogger) +} + +func (rt *_router) DeleteComment(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + + photo_id, err := strconv.ParseInt(ps.ByName("photo_id"), 10, 64) + if err != nil { + helpers.SendBadRequestError(err, "Bad photo_id", w, rt.baseLogger) + return + } + + comment_id, err := strconv.ParseInt(ps.ByName("comment_id"), 10, 64) + if err != nil { + helpers.SendBadRequestError(err, "Bad comment_id", w, rt.baseLogger) + return + } + + // Check if the user is authorized to delete that comment + // (only the user who posted the comment or the owner of the photo can delete it) + status, comment_owner, err := rt.db.GetCommentOwner(uid, photo_id, comment_id) + + if err != nil { + helpers.SendInternalError(err, "Database error: GetCommentOwner", w, rt.baseLogger) + return + } + + if status == database.ERR_NOT_FOUND { + helpers.SendNotFound(w, "Resource not found", rt.baseLogger) + return + } + + // The authorized user must be either comment_owner or uid + owner_auth, err := ctx.Auth.UserAuthorized(rt.db, comment_owner) + + if err != nil { + helpers.SendInternalError(err, "Error checking authorization", w, rt.baseLogger) + return + } + + // If the status is UNAUTHORIZED, this means that the Authorization header is missing or invalid + // We don't need to check if user is 'uid' and we can send the error + if owner_auth == reqcontext.UNAUTHORIZED { + helpers.SendStatus(http.StatusUnauthorized, w, "Unauthorized", rt.baseLogger) + } + + if owner_auth != reqcontext.AUTHORIZED { + // Authorized user is not the owner of the comment + // let's check if it's the owner of the photo + + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusForbidden) { + // The authorized user is not the owner of the photo, so we sent an error + return + } + // If it is authorized, the user can delete the comment. + // (else the status must be FORBIDDEN. It can't be UNAUTHORIZED because we already checked it + // and it can't be NOT_FOUND because we used it before to get the comment owner) + } + + // Delete the comment + _, err = rt.db.DeleteComment(uid, photo_id, comment_id) + + if err != nil { + helpers.SendInternalError(err, "Database error: DeleteComment", w, rt.baseLogger) + return + } + + // We don't need to check the status because if the comment didn't exist + // we'd have already returned an error when getting the comment owner + // so we know the comment existed and was deleted, and we can safely send 204 + + w.WriteHeader(http.StatusNoContent) +} diff --git a/service/api/followers.go b/service/api/followers.go index 7a9e1a3..d54b40b 100644 --- a/service/api/followers.go +++ b/service/api/followers.go @@ -3,32 +3,56 @@ package api import ( "encoding/json" "net/http" + "strings" "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/database" + "github.com/notherealmarco/WASAPhoto/service/structures" ) -func (rt *_router) GetFollowers(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { +func (rt *_router) GetFollowersFollowing(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { uid := ps.ByName("user_id") - if !helpers.VerifyUserOrNotFound(rt.db, uid, w) { + if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) { return } - followers, err := rt.db.GetUserFollowers(uid) + var users *[]structures.UIDName + var err error + var status database.QueryResult + // Check if client is asking for followers or following + if strings.HasSuffix(r.URL.Path, "/followers") { + // Get the followers from the database + status, users, err = rt.db.GetUserFollowers(uid) + } else { + // Get the following users from the database + status, users, err = rt.db.GetUserFollowing(uid) + } + + // Send a 500 response if there was an error if err != nil { - w.WriteHeader(http.StatusInternalServerError) // todo: is not ok, maybe let's use a helper + helpers.SendInternalError(err, "Database error: GetUserFollowers", w, rt.baseLogger) return } + // Send a 404 response if the user was not found + if status == database.ERR_NOT_FOUND { + helpers.SendNotFound(w, "User not found", rt.baseLogger) + return + } + + // Send the users to the client w.Header().Set("content-type", "application/json") - err = json.NewEncoder(w).Encode(&followers) + err = json.NewEncoder(w).Encode(users) + // Send a 500 response if there was an error if err != nil { - w.WriteHeader(http.StatusInternalServerError) // todo: is not ok + helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger) return } } @@ -38,12 +62,52 @@ func (rt *_router) PutFollow(w http.ResponseWriter, r *http.Request, ps httprout 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 + // send error if the user has no permission to perform this action + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) { return } - w.WriteHeader(http.StatusNoContent) // todo: change to 204 also in API spec + status, err := rt.db.FollowUser(uid, followed) + + if err != nil { + helpers.SendInternalError(err, "Database error: FollowUser", w, rt.baseLogger) + return + } + + if status == database.ERR_EXISTS { + w.WriteHeader(http.StatusNoContent) + return + } + + if status == database.ERR_NOT_FOUND { + helpers.SendBadRequest(w, "You are trying to follow a non-existent user", rt.baseLogger) + return + } + + helpers.SendStatus(http.StatusCreated, w, "Success", rt.baseLogger) +} + +func (rt *_router) DeleteFollow(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + followed := ps.ByName("follower_uid") + + // send error if the user has no permission to perform this action + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) { + return + } + + status, err := rt.db.UnfollowUser(uid, followed) + + if err != nil { + helpers.SendInternalError(err, "Database error: UnfollowUser", w, rt.baseLogger) + return + } + + if status == database.ERR_NOT_FOUND { + helpers.SendNotFound(w, "User not found", rt.baseLogger) + return + } + + w.WriteHeader(http.StatusNoContent) } diff --git a/service/api/helpers/api-helpers.go b/service/api/helpers/api-helpers.go index 5302cc6..37ebb31 100644 --- a/service/api/helpers/api-helpers.go +++ b/service/api/helpers/api-helpers.go @@ -6,6 +6,7 @@ import ( "net/http" "github.com/notherealmarco/WASAPhoto/service/database" + "github.com/notherealmarco/WASAPhoto/service/structures" "github.com/sirupsen/logrus" ) @@ -19,24 +20,72 @@ func DecodeJsonOrBadRequest(r io.Reader, w http.ResponseWriter, v interface{}, l return true } -func VerifyUserOrNotFound(db database.AppDatabase, uid string, w http.ResponseWriter) bool { +func VerifyUserOrNotFound(db database.AppDatabase, uid string, w http.ResponseWriter, l logrus.FieldLogger) bool { user_exists, err := db.UserExists(uid) if err != nil { - SendInternalError(err, "Error verifying user existence", w, nil) + SendInternalError(err, "Error verifying user existence", w, l) return false } if !user_exists { - w.WriteHeader(http.StatusNotFound) + SendNotFound(w, "User not found", l) return false } return true } +func SendStatus(httpStatus int, w http.ResponseWriter, description string, l logrus.FieldLogger) { + w.WriteHeader(httpStatus) + err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) + if err != nil { + l.WithError(err).Error("Error encoding json") + //todo: empty response? + } +} + +func SendNotFound(w http.ResponseWriter, description string, l logrus.FieldLogger) { + w.WriteHeader(http.StatusNotFound) + err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) + if err != nil { + l.WithError(err).Error("Error encoding json") + //todo: empty response? + } +} + +func SendBadRequest(w http.ResponseWriter, description string, l logrus.FieldLogger) { + w.WriteHeader(http.StatusBadRequest) + err := json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) + if err != nil { + l.WithError(err).Error("Error encoding json") + //todo: empty response? + } +} + +func SendBadRequestError(err error, description string, w http.ResponseWriter, l logrus.FieldLogger) { + w.WriteHeader(http.StatusBadRequest) + l.WithError(err).Error(description) + err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) + if err != nil { + l.WithError(err).Error("Error encoding json") + //todo: empty response? + } +} + 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 + err = json.NewEncoder(w).Encode(structures.GenericResponse{Status: description}) + if err != nil { + l.WithError(err).Error("Error encoding json") + //todo: empty response? + } +} + +func RollbackOrLogError(tx database.DBTransaction, l logrus.FieldLogger) { + err := tx.Rollback() + if err != nil { + l.WithError(err).Error("Error rolling back transaction") + } } diff --git a/service/api/likes.go b/service/api/likes.go new file mode 100644 index 0000000..825600c --- /dev/null +++ b/service/api/likes.go @@ -0,0 +1,96 @@ +package api + +import ( + "encoding/json" + "net/http" + "strconv" + + "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/database" +) + +func (rt *_router) GetLikes(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + // get the user id from the url + uid := ps.ByName("user_id") + photo_id, err := strconv.ParseInt(ps.ByName("photo_id"), 10, 64) + + if err != nil { + helpers.SendBadRequest(w, "Invalid photo id", rt.baseLogger) + return + } + + // send 404 if the user does not exist + if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) { + return + } + + // get the user's likes + success, likes, err := rt.db.GetPhotoLikes(uid, photo_id) + + if err != nil { + helpers.SendInternalError(err, "Database error: GetLikes", w, rt.baseLogger) + return + } + + if success == database.ERR_NOT_FOUND { + helpers.SendNotFound(w, "User or photo not found", rt.baseLogger) + return + } + + // send the response + err = json.NewEncoder(w).Encode(likes) + + if err != nil { + helpers.SendInternalError(err, "Error encoding response", w, rt.baseLogger) + return + } +} + +func (rt *_router) PutDeleteLike(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + + photo_id, err := strconv.ParseInt(ps.ByName("photo_id"), 10, 64) + if err != nil { + helpers.SendBadRequestError(err, "Bad photo_id", w, rt.baseLogger) + return + } + + liker_uid := ps.ByName("liker_uid") + + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, liker_uid, rt.db, w, http.StatusBadRequest) { + return + } + + var success database.QueryResult + + // If the request is a PUT, then we like the photo + if r.Method == "PUT" { + success, err = rt.db.LikePhoto(uid, photo_id, liker_uid) + } else { // Request is a DELETE, so we unlike the photo + success, err = rt.db.UnlikePhoto(uid, photo_id, liker_uid) + } + + if err != nil { + helpers.SendInternalError(err, "Database error", w, rt.baseLogger) + return + } + + if success == database.ERR_NOT_FOUND { + helpers.SendBadRequest(w, "User or photo not found", rt.baseLogger) + return + } + + // User already liked the photo + if success == database.ERR_EXISTS { + w.WriteHeader(http.StatusNoContent) + return + } + + // User liked the photo successfully + helpers.SendStatus(http.StatusCreated, w, "Success", rt.baseLogger) +} diff --git a/service/api/photos.go b/service/api/photos.go new file mode 100644 index 0000000..9f7ff9d --- /dev/null +++ b/service/api/photos.go @@ -0,0 +1,131 @@ +package api + +import ( + "io" + "net/http" + "os" + "path/filepath" + "strconv" + + "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" +) + +func (rt *_router) PostPhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + defer r.Body.Close() + + uid := ps.ByName("user_id") + + // send error if the user has no permission to perform this action + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) { + return + } + + transaction, photo_id, err := rt.db.PostPhoto(uid) + + if err != nil { + helpers.SendInternalError(err, "Database error: PostPhoto", w, rt.baseLogger) + return + } + + path := rt.dataPath + "/photos/" + uid + "/" + strconv.FormatInt(photo_id, 10) + ".jpg" + // todo: we should check if the body is a valid jpg image + + if err = os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { // perms = 511 + helpers.SendInternalError(err, "Error creating directory", w, rt.baseLogger) + return + } + + file, err := os.Create(path) + if err != nil { + helpers.SendInternalError(err, "Error creating file", w, rt.baseLogger) + helpers.RollbackOrLogError(transaction, rt.baseLogger) + return + } + + if _, err = io.Copy(file, r.Body); err != nil { + helpers.SendInternalError(err, "Error writing the file", w, rt.baseLogger) + helpers.RollbackOrLogError(transaction, rt.baseLogger) + return + } + + if err = file.Close(); err != nil { + helpers.SendInternalError(err, "Error closing file", w, rt.baseLogger) + helpers.RollbackOrLogError(transaction, rt.baseLogger) + } + + err = transaction.Commit() + + if err != nil { + helpers.SendInternalError(err, "Error committing transaction", w, rt.baseLogger) + //todo: should I roll back? + return + } + + helpers.SendStatus(http.StatusCreated, w, "Photo uploaded", rt.baseLogger) +} + +func (rt *_router) GetPhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + photo_id := ps.ByName("photo_id") + + if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) { + return + } + + path := rt.dataPath + "/photos/" + uid + "/" + photo_id + ".jpg" + + file, err := os.Open(path) + + if err != nil { + helpers.SendNotFound(w, "Photo not found", rt.baseLogger) + return + } + + defer file.Close() + + io.Copy(w, file) +} + +func (rt *_router) DeletePhoto(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { + + uid := ps.ByName("user_id") + photo_id := ps.ByName("photo_id") + + // photo id to int64 + photo_id_int, err := strconv.ParseInt(photo_id, 10, 64) + + if err != nil { + helpers.SendBadRequestError(err, "Bad photo id", w, rt.baseLogger) + return + } + + // send error if the user has no permission to perform this action (only the author can delete a photo) + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) { + return + } + + deleted, err := rt.db.DeletePhoto(uid, photo_id_int) + if err != nil { + helpers.SendInternalError(err, "Error deleting photo from database", w, rt.baseLogger) + return + } //todo: maybe let's use a transaction also here + + if !deleted { + helpers.SendNotFound(w, "Photo not found", rt.baseLogger) + return + } + + path := rt.dataPath + "/photos/" + uid + "/" + photo_id + ".jpg" + + if err := os.Remove(path); err != nil { + helpers.SendInternalError(err, "Error deleting file", w, rt.baseLogger) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/service/api/put-updateusername.go b/service/api/put-updateusername.go index 860f5f2..9a1a644 100644 --- a/service/api/put-updateusername.go +++ b/service/api/put-updateusername.go @@ -13,7 +13,7 @@ import ( func (rt *_router) UpdateUsername(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) { uid := ps.ByName("user_id") - if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w) { + if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) { return } var req structures.UserDetails diff --git a/service/api/reqcontext/request-context.go b/service/api/reqcontext/request-context.go index 0513367..c44ab2a 100644 --- a/service/api/reqcontext/request-context.go +++ b/service/api/reqcontext/request-context.go @@ -19,7 +19,7 @@ const ( UNAUTHORIZED = 1 FORBIDDEN = 2 USER_NOT_FOUND = 3 -) +) // todo: here? // RequestContext is the context of the request, for request-dependent parameters type RequestContext struct { @@ -34,6 +34,7 @@ type RequestContext struct { type Authorization interface { GetType() string - Authorized(db database.AppDatabase) (bool, error) + GetUserID() string + Authorized(db database.AppDatabase) (AuthStatus, error) UserAuthorized(db database.AppDatabase, uid string) (AuthStatus, error) } diff --git a/service/database/database.go b/service/database/database.go index 75850ad..34108d6 100644 --- a/service/database/database.go +++ b/service/database/database.go @@ -40,25 +40,42 @@ import ( // AppDatabase is the high level interface for the DB type AppDatabase interface { + CreateUser(name string) (string, error) UserExists(uid string) (bool, error) 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 - UnbanUser(uid string, unban string) error - PostPhoto(uid string) (int64, error) - LikePhoto(uid string, photo int64) error - UnlikePhoto(uid string, photo int64) error + UpdateUsername(uid, name string) error + + GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) // todo: maybe use a pointer to a slice? + GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error) + FollowUser(uid string, follow string) (QueryResult, error) + UnfollowUser(uid string, unfollow string) (QueryResult, error) + + BanUser(uid string, ban string) (QueryResult, error) + UnbanUser(uid string, unban string) (QueryResult, error) + + PostPhoto(uid string) (DBTransaction, int64, error) + DeletePhoto(uid string, photo int64) (bool, error) + + GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error) + LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) + UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) GetUserProfile(uid string) (*UserProfile, error) + GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) + PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error) + DeleteComment(uid string, photo_id int64, comment_id int64) (QueryResult, error) + GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error) + Ping() error } +type DBTransaction interface { + Commit() error + Rollback() error +} + type appdbimpl struct { c *sql.DB } diff --git a/service/database/db-comments.go b/service/database/db-comments.go new file mode 100644 index 0000000..9e78009 --- /dev/null +++ b/service/database/db-comments.go @@ -0,0 +1,112 @@ +package database + +import ( + "time" + + "github.com/notherealmarco/WASAPhoto/service/database/db_errors" + "github.com/notherealmarco/WASAPhoto/service/structures" +) + +func (db *appdbimpl) PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error) { + + // Check if the photo exists, as API specification requires + // photos to be identified also by the user who posted them. + // But our DB implementation only requires the photo id. + exists, err := db.photoExists(uid, photo_id) + if err != nil || !exists { + return ERR_NOT_FOUND, err + } + + _, err = db.c.Exec(`PRAGMA foreign_keys = ON; + INSERT INTO "comments" ("user", "photo", "comment", "date") VALUES (?, ?, ?, ?)`, comment_user, photo_id, comment, time.Now().Format(time.RFC3339)) + + // todo: we don't actually need it, it's already done before + if db_errors.ForeignKeyViolation(err) { + return ERR_NOT_FOUND, nil + } + + if err != nil { + return ERR_INTERNAL, err + } + return SUCCESS, nil +} + +func (db *appdbimpl) GetCommentOwner(uid string, photo_id int64, comment_id int64) (QueryResult, string, error) { + + // Check if the photo exists, as it exist but have no comments + exists, err := db.photoExists(uid, photo_id) + if err != nil || !exists { + return ERR_NOT_FOUND, "", err + } + + var comment_user string + err = db.c.QueryRow(`SELECT "user" FROM "comments" WHERE "photo" = ? AND "id" = ?`, photo_id, comment_id).Scan(&comment_user) + + if db_errors.EmptySet(err) { + return ERR_NOT_FOUND, "", nil + } + + if err != nil { + return ERR_INTERNAL, "", err + } + + return SUCCESS, comment_user, nil +} + +func (db *appdbimpl) DeleteComment(uid string, photo_id int64, comment_id int64) (QueryResult, error) { + + // Check if the photo exists, as API specification requires + // photos to be identified also by the user who posted them. + // But our DB implementation only requires the photo id. + exists, err := db.photoExists(uid, photo_id) + if err != nil || !exists { + return ERR_NOT_FOUND, err + } + + res, err := db.c.Exec(`DELETE FROM "comments" WHERE "photo" = ? AND "id" = ?`, photo_id, comment_id) + + if err != nil { + return ERR_INTERNAL, err + } + + rows, err := res.RowsAffected() + + if err != nil { + return ERR_INTERNAL, err + } + + if rows == 0 { + return ERR_NOT_FOUND, nil + } + return SUCCESS, nil +} + +func (db *appdbimpl) GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) { + + // Check if the photo exists, as it exist but have no comments + exists, err := db.photoExists(uid, photo_id) + if err != nil || !exists { + return ERR_NOT_FOUND, nil, err + } + + rows, err := db.c.Query(`SELECT "id", "user", "comment", "date" FROM "comments" WHERE "photo" = ?`, photo_id) + + if err != nil { + return ERR_INTERNAL, nil, err + } + + defer rows.Close() + + comments := make([]structures.Comment, 0) + + for rows.Next() { + var c structures.Comment + err = rows.Scan(&c.CommentID, &c.UID, &c.Comment, &c.Date) + if err != nil { + return ERR_INTERNAL, nil, err + } + comments = append(comments, c) + } + + return SUCCESS, &comments, nil +} diff --git a/service/database/db-photos.go b/service/database/db-photos.go index a043a75..e1bc41c 100644 --- a/service/database/db-photos.go +++ b/service/database/db-photos.go @@ -1,6 +1,12 @@ package database -import "time" +import ( + "database/sql" + "time" + + "github.com/notherealmarco/WASAPhoto/service/database/db_errors" + "github.com/notherealmarco/WASAPhoto/service/structures" +) type Photo struct { ID int64 @@ -16,17 +22,63 @@ type UserProfile struct { Photos []Photo } +type QueryResult int // todo: move to a separate file + +const ( + SUCCESS = 0 + ERR_NOT_FOUND = 1 + ERR_EXISTS = 2 + ERR_INTERNAL = 3 +) + +type dbtransaction struct { + c *sql.Tx +} + +func (tx *dbtransaction) Commit() error { + return tx.c.Commit() +} + +func (tx *dbtransaction) Rollback() error { + return tx.c.Rollback() +} + // Post a new photo -func (db *appdbimpl) PostPhoto(uid string) (int64, error) { - res, err := db.c.Exec(`INSERT INTO "photos" ("user", "date") VALUES (?, ?)`, uid, time.Now().Format(time.RFC3339)) +func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) { + tx, err := db.c.Begin() + if err != nil { - return 0, err + return nil, 0, err + } + + res, err := tx.Exec(`INSERT INTO "photos" ("user", "date") VALUES (?, ?)`, uid, time.Now().Format(time.RFC3339)) + if err != nil { + tx.Rollback() // error ? + return nil, 0, err } id, err := res.LastInsertId() + if err != nil { - return 0, err + tx.Rollback() // error ? + return nil, 0, err } - return id, nil + + return &dbtransaction{ + c: tx, + }, id, nil +} + +// Delete a photo, returns true if the photo was deleted and false if it did not exist +func (db *appdbimpl) DeletePhoto(uid string, photo int64) (bool, error) { + res, err := db.c.Exec(`DELETE FROM "photos" WHERE "id" = ? AND "user" = ?`, photo, uid) + if err != nil { + return false, err + } + rows, err := res.RowsAffected() + if err != nil { + return false, err + } + return rows > 0, nil } // Get user profile, including username, followers, following, and photos @@ -58,7 +110,7 @@ func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) { return nil, err } - var photos []Photo + photos := make([]Photo, 0) for rows.Next() { var id int64 var date string @@ -75,14 +127,105 @@ func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) { return &UserProfile{uid, name, followers, following, photos}, nil } +// Check if a given photo owned by a given user exists +func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) { + + var cnt int64 + err := db.c.QueryRow(`SELECT COUNT(*) FROM "photos" WHERE "id" = ? AND "user" = ?`, photo, uid).Scan(&cnt) + if err != nil { + return false, err + } + return cnt > 0, nil +} + +// Get the list of users who liked a photo +func (db *appdbimpl) GetPhotoLikes(uid string, photo int64) (QueryResult, *[]structures.UIDName, error) { + + // Check if the photo exists, as it could exist but have no likes + exists, err := db.photoExists(uid, photo) + if err != nil { + return ERR_INTERNAL, nil, err + } + + if !exists { + return ERR_NOT_FOUND, nil, nil + } + + rows, err := db.c.Query(`SELECT "users.uid", "users.name" FROM "likes", "users" + WHERE "likes.photo_id" = ? + AND "likes.user" = "users.uid"`, photo) + if err != nil { + return ERR_INTERNAL, nil, err + } + + likes := make([]structures.UIDName, 0) + for rows.Next() { + var uid string + var name string + err = rows.Scan(&uid, &name) + if err != nil { + return ERR_INTERNAL, nil, err + } + likes = append(likes, structures.UIDName{UID: uid, Name: name}) + } + + return SUCCESS, &likes, nil +} + // Like a photo -func (db *appdbimpl) LikePhoto(uid string, photo int64) error { - _, err := db.c.Exec(`INSERT INTO "likes" ("user", "photo_id") VALUES (?, ?)`, uid, photo) - return err +func (db *appdbimpl) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) { + + // Check if the photo exists, as API specification requires + // photos to be identified also by the user who posted them. + // But our DB implementation only requires the photo id. + exists, err := db.photoExists(uid, photo) + if err != nil || !exists { + return ERR_NOT_FOUND, err + } + + _, err = db.c.Exec(`PRAGMA foreign_keys = ON; + INSERT INTO "likes" ("user", "photo_id") VALUES (?, ?)`, liker_uid, photo) + + // The photo exists, but the user already liked it + if db_errors.UniqueViolation(err) { + return ERR_EXISTS, nil + } + + if db_errors.ForeignKeyViolation(err) { + return ERR_NOT_FOUND, nil + } + + if err != nil { + return ERR_INTERNAL, err + } + return SUCCESS, nil } // Unlike a photo -func (db *appdbimpl) UnlikePhoto(uid string, photo int64) error { - _, err := db.c.Exec(`DELETE FROM "likes" WHERE "user" = ? AND "photo_id" = ?`, uid, photo) - return err +func (db *appdbimpl) UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) { + + // Check if the photo exists, as API specification requires + // photos to be identified also by the user who posted them. + // But our DB implementation only requires the photo id. + exists, err := db.photoExists(uid, photo) + if err != nil || !exists { + return ERR_NOT_FOUND, err + } + + res, err := db.c.Exec(`DELETE FROM "likes" WHERE "user" = ? AND "photo_id" = ?`, liker_uid, photo) + + if err != nil { + return ERR_INTERNAL, err + } + + rows, err := res.RowsAffected() + + if err != nil { + return ERR_INTERNAL, err + } + + if rows == 0 { + return ERR_NOT_FOUND, nil + } + return SUCCESS, nil } diff --git a/service/database/db-users.go b/service/database/db-users.go index 82120e3..717d433 100644 --- a/service/database/db-users.go +++ b/service/database/db-users.go @@ -1,6 +1,8 @@ package database import ( + "database/sql" + "github.com/gofrs/uuid" "github.com/notherealmarco/WASAPhoto/service/database/db_errors" "github.com/notherealmarco/WASAPhoto/service/structures" @@ -46,10 +48,62 @@ func (db *appdbimpl) UpdateUsername(uid string, name string) error { } // Get user followers -func (db *appdbimpl) GetUserFollowers(uid string) ([]structures.UIDName, error) { +func (db *appdbimpl) GetUserFollowers(uid string) (QueryResult, *[]structures.UIDName, error) { + + // user may exist but have no followers + exists, err := db.UserExists(uid) + + if err != nil { + return ERR_INTERNAL, nil, err + } + + if !exists { + return ERR_NOT_FOUND, nil, nil + } + rows, err := db.c.Query(`SELECT "follower", "user.name" FROM "follows", "users" WHERE "follows.follower" = "users.uid" AND "followed" = ?`, uid) + + followers, err := db.uidNameQuery(rows, err) + + if err != nil { + return ERR_INTERNAL, nil, err + } + + return SUCCESS, followers, nil +} + +// Get user following +func (db *appdbimpl) GetUserFollowing(uid string) (QueryResult, *[]structures.UIDName, error) { + + // user may exist but have no followers + exists, err := db.UserExists(uid) + + if err != nil { + return ERR_INTERNAL, nil, err + } + + if !exists { + return ERR_NOT_FOUND, nil, nil + } + + rows, err := db.c.Query(`SELECT "followed", "user.name" FROM "follows", "users" + WHERE "follows.followed" = "users.uid" + AND "follower" = ?`, uid) + + following, err := db.uidNameQuery(rows, err) + + if err != nil { + return ERR_INTERNAL, nil, err + } + + return SUCCESS, following, nil +} + +// Evaluates a query that returns two columns: uid and name +func (db *appdbimpl) uidNameQuery(rows *sql.Rows, err error) (*[]structures.UIDName, error) { + if err != nil { return nil, err } @@ -64,29 +118,88 @@ func (db *appdbimpl) GetUserFollowers(uid string) ([]structures.UIDName, error) } followers = append(followers, structures.UIDName{UID: uid, Name: name}) } - return followers, nil + 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) - return err +func (db *appdbimpl) FollowUser(uid string, follow string) (QueryResult, error) { + _, err := db.c.Exec(`PRAGMA foreign_keys = ON; + INSERT INTO "follows" ("follower", "followed") VALUES (?, ?)`, uid, follow) + + if db_errors.UniqueViolation(err) { + return ERR_EXISTS, nil + } + + if db_errors.ForeignKeyViolation(err) { + return ERR_NOT_FOUND, nil + } + + if err != nil { + return ERR_INTERNAL, err + } + + return SUCCESS, nil } // Unfollow a user -func (db *appdbimpl) UnfollowUser(uid string, unfollow string) error { - _, err := db.c.Exec(`DELETE FROM "follows" WHERE "follower" = ? AND "followed" = ?`, uid, unfollow) - return err -} //todo: should return boolean or something similar +func (db *appdbimpl) UnfollowUser(uid string, unfollow string) (QueryResult, error) { + res, err := db.c.Exec(`DELETE FROM "follows" WHERE "follower" = ? AND "followed" = ?`, uid, unfollow) + + if err != nil { + return ERR_INTERNAL, err + } + + rows, err := res.RowsAffected() + if err != nil { + return ERR_INTERNAL, err + } + + if rows == 0 { + return ERR_NOT_FOUND, nil + } + + return SUCCESS, nil +} // Ban a user -func (db *appdbimpl) BanUser(uid string, ban string) error { - _, err := db.c.Exec(`INSERT INTO "bans" ("user", "ban") VALUES (?, ?)`, uid, ban) - return err +func (db *appdbimpl) BanUser(uid string, ban string) (QueryResult, error) { + _, err := db.c.Exec(`PRAGMA foreign_keys = ON; + INSERT INTO "bans" ("user", "ban") VALUES (?, ?)`, uid, ban) + + // The user is already banned by this user + if db_errors.UniqueViolation(err) { + return ERR_EXISTS, nil + } + + // One of the users does not exist + if db_errors.ForeignKeyViolation(err) { + return ERR_NOT_FOUND, nil + } + + // Other error + if err != nil { + return ERR_INTERNAL, err + } + return SUCCESS, nil } // Unban a user -func (db *appdbimpl) UnbanUser(uid string, unban string) error { - _, err := db.c.Exec(`DELETE FROM "bans" WHERE "user" = ? AND "ban" = ?`, uid, unban) - return err +func (db *appdbimpl) UnbanUser(uid string, unban string) (QueryResult, error) { + res, err := db.c.Exec(`DELETE FROM "bans" WHERE "user" = ? AND "ban" = ?`, uid, unban) + + if err != nil { + return ERR_INTERNAL, err + } + + rows, err := res.RowsAffected() + + if err != nil { + return ERR_INTERNAL, err + } + + // The user was not banned by this user + if rows == 0 { + return ERR_NOT_FOUND, nil + } + return SUCCESS, nil } diff --git a/service/database/db_errors/db-errors.go b/service/database/db_errors/db-errors.go index 433528d..4584c40 100644 --- a/service/database/db_errors/db-errors.go +++ b/service/database/db_errors/db-errors.go @@ -9,3 +9,17 @@ func EmptySet(err error) bool { } return strings.Contains(err.Error(), "no rows in result set") } + +func UniqueViolation(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "UNIQUE constraint failed") +} + +func ForeignKeyViolation(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "FOREIGN KEY constraint failed") +} diff --git a/service/structures/api-structures.go b/service/structures/api-structures.go index 7b0f2b8..dfc72cb 100644 --- a/service/structures/api-structures.go +++ b/service/structures/api-structures.go @@ -8,3 +8,15 @@ type UIDName struct { UID string `json:"user_id"` Name string `json:"name"` } + +type GenericResponse struct { + Status string `json:"status"` +} + +type Comment struct { + CommentID string `json:"comment_id"` + UID string `json:"user_id"` + Name string `json:"name"` + Comment string `json:"comment"` + Date string `json:"date"` +}