mirror of
https://github.com/notherealmarco/WASAPhoto.git
synced 2025-03-14 06:06:15 +01:00
Add database query status (and improved response), photos, likes, comments, bans
This commit is contained in:
parent
519ae22197
commit
abbd5bc494
22 changed files with 1118 additions and 72 deletions
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
31
service/api/authorization/auth-anonymous.go
Normal file
31
service/api/authorization/auth-anonymous.go
Normal file
|
@ -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 ""
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
70
service/api/bans.go
Normal file
70
service/api/bans.go
Normal file
|
@ -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)
|
||||
}
|
167
service/api/comments.go
Normal file
167
service/api/comments.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
96
service/api/likes.go
Normal file
96
service/api/likes.go
Normal file
|
@ -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)
|
||||
}
|
131
service/api/photos.go
Normal file
131
service/api/photos.go
Normal file
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
112
service/database/db-comments.go
Normal file
112
service/database/db-comments.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue