Add getUserProfile and database.IsBanned

This commit is contained in:
Marco Realacci 2022-11-21 19:44:50 +01:00
parent 44eb1e1fa6
commit 53a764e8bb
13 changed files with 328 additions and 163 deletions

View file

@ -31,6 +31,8 @@ func (rt *_router) Handler() http.Handler {
rt.router.POST("/users/:user_id/photos/:photo_id/comments", rt.wrap(rt.PostComment)) rt.router.POST("/users/:user_id/photos/:photo_id/comments", rt.wrap(rt.PostComment))
rt.router.DELETE("/users/:user_id/photos/:photo_id/comments/:comment_id", rt.wrap(rt.DeleteComment)) rt.router.DELETE("/users/:user_id/photos/:photo_id/comments/:comment_id", rt.wrap(rt.DeleteComment))
rt.router.GET("/users/:user_id", rt.wrap(rt.GetUserProfile))
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))

View file

@ -42,3 +42,20 @@ func SendAuthorizationError(f func(db database.AppDatabase, uid string) (reqcont
} }
return true return true
} }
func SendErrorIfNotLoggedIn(f func(db database.AppDatabase) (reqcontext.AuthStatus, error), db database.AppDatabase, w http.ResponseWriter, l logrus.FieldLogger) bool {
auth, err := f(db)
if err != nil {
helpers.SendInternalError(err, "Authorization error", w, l)
return false
}
if auth == reqcontext.UNAUTHORIZED {
helpers.SendStatus(http.StatusUnauthorized, w, "Unauthorized", l)
return false
}
return true
}

View file

@ -73,6 +73,9 @@ func (rt *_router) PostComment(w http.ResponseWriter, r *http.Request, ps httpro
// check if the user is authorized to post a comment // check if the user is authorized to post a comment
if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, request_body.UID, rt.db, w, rt.baseLogger, http.StatusBadRequest) { if !authorization.SendAuthorizationError(ctx.Auth.UserAuthorized, request_body.UID, rt.db, w, rt.baseLogger, http.StatusBadRequest) {
// It returns 400 Bad Request if the user_id field in the request body is missing or an invalid user_id
// It returns 401 if the user is not logged in
// It returns 403 if the user is not authorized to post a comment as the requested user
return return
} }

View file

@ -0,0 +1,46 @@
package api
import (
"encoding/json"
"net/http"
"github.com/julienschmidt/httprouter"
"github.com/notherealmarco/WASAPhoto/service/api/authorization"
"github.com/notherealmarco/WASAPhoto/service/api/helpers"
"github.com/notherealmarco/WASAPhoto/service/api/reqcontext"
"github.com/notherealmarco/WASAPhoto/service/database"
)
func (rt *_router) GetUserProfile(w http.ResponseWriter, r *http.Request, ps httprouter.Params, ctx reqcontext.RequestContext) {
// Get user id
uid := ps.ByName("user_id")
if !authorization.SendErrorIfNotLoggedIn(ctx.Auth.Authorized, rt.db, w, rt.baseLogger) ||
!helpers.SendNotFoundIfBanned(rt.db, ctx.Auth.GetUserID(), uid, w, rt.baseLogger) {
return
}
// Get user profile
status, profile, err := rt.db.GetUserProfile(uid)
if err != nil {
helpers.SendInternalError(err, "Database error: GetUserProfile", w, rt.baseLogger)
return
}
if status == database.ERR_NOT_FOUND {
helpers.SendNotFound(w, "User not found", rt.baseLogger)
return
}
// Return user profile
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(profile)
if err != nil {
helpers.SendInternalError(err, "Error encoding json", w, rt.baseLogger)
return
}
}

View file

@ -89,3 +89,16 @@ func RollbackOrLogError(tx database.DBTransaction, l logrus.FieldLogger) {
l.WithError(err).Error("Error rolling back transaction") l.WithError(err).Error("Error rolling back transaction")
} }
} }
func SendNotFoundIfBanned(db database.AppDatabase, uid string, banner string, w http.ResponseWriter, l logrus.FieldLogger) bool {
banned, err := db.IsBanned(uid, banner)
if err != nil {
SendInternalError(err, "Database error: IsBanned", w, l)
return false
}
if banned {
SendNotFound(w, "User not found", l)
return false
}
return true
}

View file

@ -53,6 +53,7 @@ type AppDatabase interface {
BanUser(uid string, ban string) (QueryResult, error) BanUser(uid string, ban string) (QueryResult, error)
UnbanUser(uid string, unban string) (QueryResult, error) UnbanUser(uid string, unban string) (QueryResult, error)
IsBanned(uid string, banner string) (bool, error)
PostPhoto(uid string) (DBTransaction, int64, error) PostPhoto(uid string) (DBTransaction, int64, error)
DeletePhoto(uid string, photo int64) (bool, error) DeletePhoto(uid string, photo int64) (bool, error)
@ -61,7 +62,7 @@ type AppDatabase interface {
LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error) LikePhoto(uid string, photo int64, liker_uid string) (QueryResult, error)
UnlikePhoto(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) (QueryResult, *structures.UserProfile, error)
GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error) GetComments(uid string, photo_id int64) (QueryResult, *[]structures.Comment, error)
PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error) PostComment(uid string, photo_id int64, comment_user string, comment string) (QueryResult, error)

View file

@ -0,0 +1,98 @@
package database
import (
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
)
// 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, 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, 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
}

View file

@ -1,48 +1,11 @@
package database package database
import ( import (
"database/sql"
"time" "time"
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures" "github.com/notherealmarco/WASAPhoto/service/structures"
) )
type Photo struct {
ID int64
Likes int64
Comments int64
}
type UserProfile struct {
UID string
Name string
Following int64
Followers int64
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) (DBTransaction, int64, error) { func (db *appdbimpl) PostPhoto(uid string) (DBTransaction, int64, error) {
tx, err := db.c.Begin() tx, err := db.c.Begin()
@ -81,50 +44,39 @@ func (db *appdbimpl) DeletePhoto(uid string, photo int64) (bool, error) {
return rows > 0, nil return rows > 0, nil
} }
// Get user profile, including username, followers, following, and photos func (db *appdbimpl) getUserPhotos(uid string) (*[]structures.Photo, error) {
func (db *appdbimpl) GetUserProfile(uid string) (*UserProfile, error) {
// Get user info
var name string
err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name)
if err != nil {
return nil, err
}
// Get followers
var followers int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "followed" = ?`, uid).Scan(&followers)
// Get following users
var following int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "follower" = ?`, uid).Scan(&following)
// Get photos // Get photos
rows, err := db.c.Query(`SELECT "photos"."id", "photos"."date", rows, err := db.c.Query(`SELECT "p"."user", "p"."id", "p"."date",
COUNT("likes"."user") AS "likes", (
COUNT("comments"."user") AS "comments" SELECT COUNT(*) AS "likes" FROM "likes" AS "l"
FROM "photos", "likes", "comments" WHERE "l"."photo_id" = "p"."id"
WHERE "likes"."photo_id" = "photos"."id" ),
AND "comments"."photo" = "photos"."id" (
AND "user" = ?`, uid) SELECT COUNT(*) AS "comments" FROM "comments" AS "c"
WHERE "c"."photo" = "p"."id"
)
FROM "photos" AS "p"
WHERE "p"."user" = ?`, uid)
if err != nil { if err != nil {
// Return the error
return nil, err return nil, err
} }
photos := make([]Photo, 0) photos := make([]structures.Photo, 0)
for rows.Next() { for rows.Next() {
var id int64 // If there is a next row, we create an instance of Photo and add it to the slice
var date string var photo structures.Photo
var likes int64 err = rows.Scan(&photo.UID, &photo.ID, &photo.Date, &photo.Likes, &photo.Comments)
var comments int64
err = rows.Scan(&id, &date, &likes, &comments)
if err != nil { if err != nil {
// Return the error
return nil, err return nil, err
} }
photo_data := Photo{id, likes, comments} photos = append(photos, photo)
photos = append(photos, photo_data)
} }
return &UserProfile{uid, name, followers, following, photos}, nil return &photos, nil
} }
// Check if a given photo owned by a given user exists // Check if a given photo owned by a given user exists
@ -137,95 +89,3 @@ func (db *appdbimpl) photoExists(uid string, photo int64) (bool, error) {
} }
return cnt > 0, nil 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, 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, 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
}

View file

@ -0,0 +1,63 @@
package database
import (
"github.com/notherealmarco/WASAPhoto/service/database/db_errors"
"github.com/notherealmarco/WASAPhoto/service/structures"
)
// Get user profile, including username, followers, following, and photos
func (db *appdbimpl) GetUserProfile(uid string) (QueryResult, *structures.UserProfile, error) {
// Get user info
var name string
err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name)
if db_errors.EmptySet(err) {
// Query returned no rows, the user does not exist
return ERR_NOT_FOUND, nil, nil
} else if err != nil {
return ERR_INTERNAL, nil, err
}
// Get followers
var followers int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "followed" = ?`, uid).Scan(&followers)
if err != nil {
// Return the error
return ERR_INTERNAL, nil, err
}
// Get following users
var following int64
err = db.c.QueryRow(`SELECT COUNT(*) FROM "follows" WHERE "follower" = ?`, uid).Scan(&following)
if err != nil {
// Return the error
return ERR_INTERNAL, nil, err
}
photos, err := db.getUserPhotos(uid)
if err != nil {
return ERR_INTERNAL, nil, err
}
// Convert []Photo to []UserPhoto
user_photos := make([]structures.UserPhoto, 0)
for _, photo := range *photos {
user_photos = append(user_photos, structures.UserPhoto{
ID: photo.ID,
Likes: photo.Likes,
Comments: photo.Comments,
Date: photo.Date,
})
}
return SUCCESS, &structures.UserProfile{
UID: uid,
Name: name,
Following: following,
Followers: followers,
Photos: &user_photos}, nil
}

View file

@ -0,0 +1,11 @@
package database
type QueryResult int
// Constants used to represent the result of queries
const (
SUCCESS = 0
ERR_NOT_FOUND = 1
ERR_EXISTS = 2
ERR_INTERNAL = 3
)

View file

@ -0,0 +1,15 @@
package database
import "database/sql"
type dbtransaction struct {
c *sql.Tx
}
func (tx *dbtransaction) Commit() error {
return tx.c.Commit()
}
func (tx *dbtransaction) Rollback() error {
return tx.c.Rollback()
}

View file

@ -12,7 +12,7 @@ import (
//todo //todo
// Check if user exists // Check if user exists
func (db *appdbimpl) UserExists(uid string) (bool, error) { func (db *appdbimpl) UserExists(uid string) (bool, error) { //todo: refactor code
var name string var name string
err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name) err := db.c.QueryRow(`SELECT "name" FROM "users" WHERE "uid" = ?`, uid).Scan(&name)
@ -203,3 +203,16 @@ func (db *appdbimpl) UnbanUser(uid string, unban string) (QueryResult, error) {
} }
return SUCCESS, nil return SUCCESS, nil
} }
// Is user banned by another user
func (db *appdbimpl) IsBanned(uid string, banner string) (bool, error) {
var cnt int
err := db.c.QueryRow(`SELECT COUNT(*) FROM "bans" WHERE "user" = ? AND "ban" = ?`, banner, uid).Scan(&cnt)
if err != nil {
return false, err
}
return cnt > 0, nil
}

View file

@ -20,3 +20,26 @@ type Comment struct {
Comment string `json:"comment"` Comment string `json:"comment"`
Date string `json:"date"` Date string `json:"date"`
} }
type Photo struct { //todo: move to structures
UID string `json:"user_id"`
ID int64 `json:"photo_id"`
Likes int64 `json:"likes"`
Comments int64 `json:"comments"`
Date string `json:"date"`
}
type UserPhoto struct {
ID int64 `json:"photo_id"`
Likes int64 `json:"likes"`
Comments int64 `json:"comments"`
Date string `json:"date"`
}
type UserProfile struct {
UID string `json:"user_id"`
Name string `json:"name"`
Following int64 `json:"following"`
Followers int64 `json:"followers"`
Photos *[]UserPhoto `json:"photos"` //todo: check json names
}