mirror of
https://github.com/notherealmarco/WASAPhoto.git
synced 2025-03-14 14:16: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 (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/ardanlabs/conf"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ardanlabs/conf"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WebAPIConfiguration describes the web API configuration. This structure is automatically parsed by
|
// WebAPIConfiguration describes the web API configuration. This structure is automatically parsed by
|
||||||
|
@ -27,6 +28,9 @@ type WebAPIConfiguration struct {
|
||||||
DB struct {
|
DB struct {
|
||||||
Filename string `conf:"default:/tmp/decaf.db"`
|
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.
|
// loadConfiguration creates a WebAPIConfiguration starting from flags, environment variables and configuration file.
|
||||||
|
|
|
@ -28,17 +28,18 @@ import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"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"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"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
|
// 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{
|
apirouter, err := api.New(api.Config{
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
Database: db,
|
Database: db,
|
||||||
|
DataPath: cfg.Data.Path,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithError(err).Error("error creating the API server instance")
|
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"))
|
auth, err := authorization.BuildAuth(r.Header.Get("Authorization"))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
auth = authorization.BuildAnonymous()
|
||||||
rt.baseLogger.WithError(err).Info("User not authorized")
|
rt.baseLogger.WithError(err).Info("User not authorized")
|
||||||
return
|
// is not an error, just a not logged in user!
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctx = reqcontext.RequestContext{
|
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.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("/", rt.getHelloWorld)
|
||||||
rt.router.GET("/context", rt.wrap(rt.getContextReply))
|
rt.router.GET("/context", rt.wrap(rt.getContextReply))
|
||||||
|
|
|
@ -38,10 +38,11 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"github.com/notherealmarco/WASAPhoto/service/database"
|
|
||||||
"github.com/julienschmidt/httprouter"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
"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.
|
// 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 is the instance of database.AppDatabase where data are saved
|
||||||
Database database.AppDatabase
|
Database database.AppDatabase
|
||||||
|
|
||||||
|
DataPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Router is the package API interface representing an API handler builder
|
// Router is the package API interface representing an API handler builder
|
||||||
|
@ -82,6 +85,7 @@ func New(cfg Config) (Router, error) {
|
||||||
router: router,
|
router: router,
|
||||||
baseLogger: cfg.Logger,
|
baseLogger: cfg.Logger,
|
||||||
db: cfg.Database,
|
db: cfg.Database,
|
||||||
|
dataPath: cfg.DataPath,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,4 +97,6 @@ type _router struct {
|
||||||
baseLogger logrus.FieldLogger
|
baseLogger logrus.FieldLogger
|
||||||
|
|
||||||
db database.AppDatabase
|
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
|
return &BearerAuth{token: header[7:]}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BearerAuth) GetToken() string {
|
func (b *BearerAuth) GetUserID() string {
|
||||||
return b.token
|
return b.token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ func BuildAuth(header string) (reqcontext.Authorization, error) {
|
||||||
return auth, nil
|
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)
|
auth, err := f(db, uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
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
|
// requested user is not found -> 404 as the resource is not found
|
||||||
if auth == reqcontext.USER_NOT_FOUND {
|
if auth == reqcontext.USER_NOT_FOUND {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
w.WriteHeader(notFoundStatus)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/julienschmidt/httprouter"
|
"github.com/julienschmidt/httprouter"
|
||||||
|
"github.com/notherealmarco/WASAPhoto/service/api/authorization"
|
||||||
"github.com/notherealmarco/WASAPhoto/service/api/helpers"
|
"github.com/notherealmarco/WASAPhoto/service/api/helpers"
|
||||||
"github.com/notherealmarco/WASAPhoto/service/api/reqcontext"
|
"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")
|
uid := ps.ByName("user_id")
|
||||||
|
|
||||||
if !helpers.VerifyUserOrNotFound(rt.db, uid, w) {
|
if !helpers.VerifyUserOrNotFound(rt.db, uid, w, rt.baseLogger) {
|
||||||
return
|
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 {
|
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
|
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")
|
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 {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusInternalServerError) // todo: is not ok
|
helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,12 +62,52 @@ func (rt *_router) PutFollow(w http.ResponseWriter, r *http.Request, ps httprout
|
||||||
uid := ps.ByName("user_id")
|
uid := ps.ByName("user_id")
|
||||||
followed := ps.ByName("follower_uid")
|
followed := ps.ByName("follower_uid")
|
||||||
|
|
||||||
err := rt.db.FollowUser(uid, followed)
|
// send error if the user has no permission to perform this action
|
||||||
|
if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, uid, rt.db, w, http.StatusNotFound) {
|
||||||
if err != nil {
|
|
||||||
w.WriteHeader(http.StatusInternalServerError) // todo: is not ok, maybe let's use a helper
|
|
||||||
return
|
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"
|
"net/http"
|
||||||
|
|
||||||
"github.com/notherealmarco/WASAPhoto/service/database"
|
"github.com/notherealmarco/WASAPhoto/service/database"
|
||||||
|
"github.com/notherealmarco/WASAPhoto/service/structures"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,24 +20,72 @@ func DecodeJsonOrBadRequest(r io.Reader, w http.ResponseWriter, v interface{}, l
|
||||||
return true
|
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)
|
user_exists, err := db.UserExists(uid)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendInternalError(err, "Error verifying user existence", w, nil)
|
SendInternalError(err, "Error verifying user existence", w, l)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user_exists {
|
if !user_exists {
|
||||||
w.WriteHeader(http.StatusNotFound)
|
SendNotFound(w, "User not found", l)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
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) {
|
func SendInternalError(err error, description string, w http.ResponseWriter, l logrus.FieldLogger) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
l.WithError(err).Error(description)
|
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) {
|
func (rt *_router) UpdateUsername(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
|
||||||
|
|
||||||
uid := ps.ByName("user_id")
|
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
|
return
|
||||||
}
|
}
|
||||||
var req structures.UserDetails
|
var req structures.UserDetails
|
||||||
|
|
|
@ -19,7 +19,7 @@ const (
|
||||||
UNAUTHORIZED = 1
|
UNAUTHORIZED = 1
|
||||||
FORBIDDEN = 2
|
FORBIDDEN = 2
|
||||||
USER_NOT_FOUND = 3
|
USER_NOT_FOUND = 3
|
||||||
)
|
) // todo: here?
|
||||||
|
|
||||||
// RequestContext is the context of the request, for request-dependent parameters
|
// RequestContext is the context of the request, for request-dependent parameters
|
||||||
type RequestContext struct {
|
type RequestContext struct {
|
||||||
|
@ -34,6 +34,7 @@ type RequestContext struct {
|
||||||
|
|
||||||
type Authorization interface {
|
type Authorization interface {
|
||||||
GetType() string
|
GetType() string
|
||||||
Authorized(db database.AppDatabase) (bool, error)
|
GetUserID() string
|
||||||
|
Authorized(db database.AppDatabase) (AuthStatus, error)
|
||||||
UserAuthorized(db database.AppDatabase, uid string) (AuthStatus, error)
|
UserAuthorized(db database.AppDatabase, uid string) (AuthStatus, error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,25 +40,42 @@ import (
|
||||||
|
|
||||||
// AppDatabase is the high level interface for the DB
|
// AppDatabase is the high level interface for the DB
|
||||||
type AppDatabase interface {
|
type AppDatabase interface {
|
||||||
|
CreateUser(name string) (string, error)
|
||||||
UserExists(uid string) (bool, error)
|
UserExists(uid string) (bool, error)
|
||||||
GetUserID(name string) (string, 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)
|
UpdateUsername(uid, name string) error
|
||||||
LikePhoto(uid string, photo int64) error
|
|
||||||
UnlikePhoto(uid string, photo int64) 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)
|
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
|
Ping() error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DBTransaction interface {
|
||||||
|
Commit() error
|
||||||
|
Rollback() error
|
||||||
|
}
|
||||||
|
|
||||||
type appdbimpl struct {
|
type appdbimpl struct {
|
||||||
c *sql.DB
|
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
|
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 {
|
type Photo struct {
|
||||||
ID int64
|
ID int64
|
||||||
|
@ -16,17 +22,63 @@ type UserProfile struct {
|
||||||
Photos []Photo
|
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
|
// Post a new photo
|
||||||
func (db *appdbimpl) PostPhoto(uid string) (int64, error) {
|
func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) {
|
||||||
res, err := db.c.Exec(`INSERT INTO "photos" ("user", "date") VALUES (?, ?)`, uid, time.Now().Format(time.RFC3339))
|
tx, err := db.c.Begin()
|
||||||
|
|
||||||
if err != nil {
|
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()
|
id, err := res.LastInsertId()
|
||||||
|
|
||||||
if err != nil {
|
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
|
// Get user profile, including username, followers, following, and photos
|
||||||
|
@ -58,7 +110,7 @@ func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var photos []Photo
|
photos := make([]Photo, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var id int64
|
var id int64
|
||||||
var date string
|
var date string
|
||||||
|
@ -75,14 +127,105 @@ func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) {
|
||||||
return &UserProfile{uid, name, followers, following, photos}, nil
|
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
|
// Like a photo
|
||||||
func (db *appdbimpl) LikePhoto(uid string, photo int64) error {
|
func (db *appdbimpl) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
|
||||||
_, err := db.c.Exec(`INSERT INTO "likes" ("user", "photo_id") VALUES (?, ?)`, uid, photo)
|
|
||||||
return err
|
// 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
|
// Unlike a photo
|
||||||
func (db *appdbimpl) UnlikePhoto(uid string, photo int64) error {
|
func (db *appdbimpl) UnlikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) {
|
||||||
_, err := db.c.Exec(`DELETE FROM "likes" WHERE "user" = ? AND "photo_id" = ?`, uid, photo)
|
|
||||||
return err
|
// 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
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
"github.com/gofrs/uuid"
|
"github.com/gofrs/uuid"
|
||||||
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
|
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
|
||||||
"github.com/notherealmarco/WASAPhoto/service/structures"
|
"github.com/notherealmarco/WASAPhoto/service/structures"
|
||||||
|
@ -46,10 +48,62 @@ func (db *appdbimpl) UpdateUsername(uid string, name string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user followers
|
// 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"
|
rows, err := db.c.Query(`SELECT "follower", "user.name" FROM "follows", "users"
|
||||||
WHERE "follows.follower" = "users.uid"
|
WHERE "follows.follower" = "users.uid"
|
||||||
AND "followed" = ?`, 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 {
|
if err != nil {
|
||||||
return nil, err
|
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})
|
followers = append(followers, structures.UIDName{UID: uid, Name: name})
|
||||||
}
|
}
|
||||||
return followers, nil
|
return &followers, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Follow a user
|
// Follow a user
|
||||||
func (db *appdbimpl) FollowUser(uid string, follow string) error {
|
func (db *appdbimpl) FollowUser(uid string, follow string) (QueryResult, error) {
|
||||||
_, err := db.c.Exec(`INSERT INTO "follows" ("follower", "followed") VALUES (?, ?)`, uid, follow)
|
_, err := db.c.Exec(`PRAGMA foreign_keys = ON;
|
||||||
return err
|
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
|
// Unfollow a user
|
||||||
func (db *appdbimpl) UnfollowUser(uid string, unfollow string) error {
|
func (db *appdbimpl) UnfollowUser(uid string, unfollow string) (QueryResult, error) {
|
||||||
_, err := db.c.Exec(`DELETE FROM "follows" WHERE "follower" = ? AND "followed" = ?`, uid, unfollow)
|
res, err := db.c.Exec(`DELETE FROM "follows" WHERE "follower" = ? AND "followed" = ?`, uid, unfollow)
|
||||||
return err
|
|
||||||
} //todo: should return boolean or something similar
|
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
|
// Ban a user
|
||||||
func (db *appdbimpl) BanUser(uid string, ban string) error {
|
func (db *appdbimpl) BanUser(uid string, ban string) (QueryResult, error) {
|
||||||
_, err := db.c.Exec(`INSERT INTO "bans" ("user", "ban") VALUES (?, ?)`, uid, ban)
|
_, err := db.c.Exec(`PRAGMA foreign_keys = ON;
|
||||||
return err
|
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
|
// Unban a user
|
||||||
func (db *appdbimpl) UnbanUser(uid string, unban string) error {
|
func (db *appdbimpl) UnbanUser(uid string, unban string) (QueryResult, error) {
|
||||||
_, err := db.c.Exec(`DELETE FROM "bans" WHERE "user" = ? AND "ban" = ?`, uid, unban)
|
res, err := db.c.Exec(`DELETE FROM "bans" WHERE "user" = ? AND "ban" = ?`, uid, unban)
|
||||||
return err
|
|
||||||
|
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")
|
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"`
|
UID string `json:"user_id"`
|
||||||
Name string `json:"name"`
|
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