2020-02-09 21:25:33 +01:00
|
|
|
package resolvers
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"time"
|
|
|
|
|
2020-06-14 15:07:07 +02:00
|
|
|
"github.com/pkg/errors"
|
2020-11-23 19:39:44 +01:00
|
|
|
"gorm.io/gorm"
|
2020-12-17 22:29:24 +01:00
|
|
|
"gorm.io/gorm/clause"
|
2020-06-14 15:07:07 +02:00
|
|
|
|
2020-12-17 22:51:43 +01:00
|
|
|
api "github.com/photoview/photoview/api/graphql"
|
|
|
|
"github.com/photoview/photoview/api/graphql/auth"
|
|
|
|
"github.com/photoview/photoview/api/graphql/models"
|
|
|
|
"github.com/photoview/photoview/api/utils"
|
2020-02-09 21:25:33 +01:00
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
)
|
|
|
|
|
|
|
|
type shareTokenResolver struct {
|
|
|
|
*Resolver
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Resolver) ShareToken() api.ShareTokenResolver {
|
|
|
|
return &shareTokenResolver{r}
|
|
|
|
}
|
|
|
|
|
2020-12-17 22:29:24 +01:00
|
|
|
func (r *shareTokenResolver) Owner(ctx context.Context, obj *models.ShareToken) (*models.User, error) {
|
|
|
|
return &obj.Owner, nil
|
|
|
|
}
|
2020-11-27 16:02:10 +01:00
|
|
|
|
2020-12-17 22:29:24 +01:00
|
|
|
func (r *shareTokenResolver) Album(ctx context.Context, obj *models.ShareToken) (*models.Album, error) {
|
|
|
|
return obj.Album, nil
|
|
|
|
}
|
2020-11-27 16:02:10 +01:00
|
|
|
|
2020-12-17 22:29:24 +01:00
|
|
|
func (r *shareTokenResolver) Media(ctx context.Context, obj *models.ShareToken) (*models.Media, error) {
|
|
|
|
return obj.Media, nil
|
|
|
|
}
|
2020-02-09 21:25:33 +01:00
|
|
|
|
2020-06-14 15:07:07 +02:00
|
|
|
func (r *shareTokenResolver) HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error) {
|
|
|
|
hasPassword := obj.Password != nil
|
|
|
|
return hasPassword, nil
|
|
|
|
}
|
|
|
|
|
2021-03-03 15:55:55 +01:00
|
|
|
func (r *queryResolver) ShareToken(ctx context.Context, credentials models.ShareTokenCredentials) (*models.ShareToken, error) {
|
2020-02-09 21:25:33 +01:00
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
var token models.ShareToken
|
2021-03-03 15:55:55 +01:00
|
|
|
if err := r.Database.Preload(clause.Associations).Where("value = ?", credentials.Token).First(&token).Error; err != nil {
|
2020-11-27 16:02:10 +01:00
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
2020-02-11 15:36:12 +01:00
|
|
|
return nil, errors.New("share not found")
|
2020-02-11 14:32:35 +01:00
|
|
|
} else {
|
2020-07-10 18:35:37 +02:00
|
|
|
return nil, errors.Wrap(err, "failed to get share token from database")
|
2020-02-11 14:32:35 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-14 20:56:48 +02:00
|
|
|
if token.Password != nil {
|
2021-03-03 15:55:55 +01:00
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*credentials.Password)); err != nil {
|
2020-06-14 20:56:48 +02:00
|
|
|
if err == bcrypt.ErrMismatchedHashAndPassword {
|
|
|
|
return nil, errors.New("unauthorized")
|
|
|
|
} else {
|
2020-07-10 18:35:37 +02:00
|
|
|
return nil, errors.Wrap(err, "failed to compare token password hashes")
|
2020-06-14 20:56:48 +02:00
|
|
|
}
|
|
|
|
}
|
2020-06-14 17:58:50 +02:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
return &token, nil
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|
|
|
|
|
2021-03-03 15:55:55 +01:00
|
|
|
func (r *queryResolver) ShareTokenValidatePassword(ctx context.Context, credentials models.ShareTokenCredentials) (bool, error) {
|
2020-11-27 16:02:10 +01:00
|
|
|
var token models.ShareToken
|
2021-03-03 15:55:55 +01:00
|
|
|
if err := r.Database.Where("value = ?", credentials.Token).First(&token).Error; err != nil {
|
2020-11-27 16:02:10 +01:00
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
2020-06-14 18:28:12 +02:00
|
|
|
return false, errors.New("share not found")
|
|
|
|
} else {
|
2020-07-10 18:35:37 +02:00
|
|
|
return false, errors.Wrap(err, "failed to get share token from database")
|
2020-06-14 18:28:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-14 20:56:48 +02:00
|
|
|
if token.Password == nil {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2021-03-03 15:55:55 +01:00
|
|
|
if credentials.Password == nil {
|
2020-06-14 20:56:48 +02:00
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
2021-03-03 15:55:55 +01:00
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*credentials.Password)); err != nil {
|
2020-06-14 20:56:48 +02:00
|
|
|
if err == bcrypt.ErrMismatchedHashAndPassword {
|
|
|
|
return false, nil
|
|
|
|
} else {
|
2020-07-10 18:35:37 +02:00
|
|
|
return false, errors.Wrap(err, "could not compare token password hashes")
|
2020-06-14 20:56:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
2020-06-14 18:28:12 +02:00
|
|
|
}
|
|
|
|
|
2020-11-28 21:29:31 +01:00
|
|
|
func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
2020-02-09 21:25:33 +01:00
|
|
|
user := auth.UserFromContext(ctx)
|
|
|
|
if user == nil {
|
|
|
|
return nil, auth.ErrUnauthorized
|
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
var count int64
|
2021-01-02 23:07:44 +01:00
|
|
|
err := r.Database.
|
|
|
|
Model(&models.Album{}).
|
|
|
|
Where("EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = albums.id AND user_albums.user_id = ?)", user.ID).
|
|
|
|
Count(&count).Error
|
|
|
|
|
|
|
|
if err != nil {
|
2020-07-10 18:35:37 +02:00
|
|
|
return nil, errors.Wrap(err, "failed to validate album owner with database")
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|
2020-11-27 16:02:10 +01:00
|
|
|
|
|
|
|
if count == 0 {
|
2020-02-09 21:25:33 +01:00
|
|
|
return nil, auth.ErrUnauthorized
|
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
var hashedPassword *string = nil
|
2020-02-09 21:25:33 +01:00
|
|
|
if password != nil {
|
|
|
|
hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12)
|
|
|
|
if err != nil {
|
2020-07-10 18:35:37 +02:00
|
|
|
return nil, errors.Wrap(err, "failed to hash token password")
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|
2020-11-27 16:02:10 +01:00
|
|
|
hashedStr := string(hashedPassBytes)
|
|
|
|
hashedPassword = &hashedStr
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
shareToken := models.ShareToken{
|
|
|
|
Value: utils.GenerateToken(),
|
|
|
|
OwnerID: user.ID,
|
|
|
|
Expire: expire,
|
|
|
|
Password: hashedPassword,
|
|
|
|
AlbumID: &albumID,
|
|
|
|
MediaID: nil,
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
if err := r.Database.Create(&shareToken).Error; err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to insert new share token into database")
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
return &shareToken, nil
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|
|
|
|
|
2020-11-28 21:29:31 +01:00
|
|
|
func (r *mutationResolver) ShareMedia(ctx context.Context, mediaID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
2020-02-11 15:36:12 +01:00
|
|
|
user := auth.UserFromContext(ctx)
|
|
|
|
if user == nil {
|
|
|
|
return nil, auth.ErrUnauthorized
|
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
var media models.Media
|
2020-12-17 22:29:24 +01:00
|
|
|
|
2021-03-23 14:04:44 +01:00
|
|
|
var query string
|
|
|
|
if r.Database.Dialector.Name() == "postgres" {
|
|
|
|
query = "EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = \"Album\".id AND user_albums.user_id = ?)"
|
|
|
|
} else {
|
|
|
|
query = "EXISTS (SELECT * FROM user_albums WHERE user_albums.album_id = Album.id AND user_albums.user_id = ?)"
|
|
|
|
}
|
|
|
|
|
2021-01-02 23:07:44 +01:00
|
|
|
err := r.Database.Joins("Album").
|
2021-03-23 14:04:44 +01:00
|
|
|
Where(query, user.ID).
|
2021-01-02 23:07:44 +01:00
|
|
|
First(&media, mediaID).
|
|
|
|
Error
|
|
|
|
|
|
|
|
if err != nil {
|
2020-11-27 16:02:10 +01:00
|
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
|
|
return nil, auth.ErrUnauthorized
|
|
|
|
} else {
|
|
|
|
return nil, errors.Wrap(err, "failed to validate media owner with database")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
hashedPassword, err := hashSharePassword(password)
|
2020-06-14 15:07:07 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2020-02-11 15:36:12 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
shareToken := models.ShareToken{
|
|
|
|
Value: utils.GenerateToken(),
|
|
|
|
OwnerID: user.ID,
|
|
|
|
Expire: expire,
|
|
|
|
Password: hashedPassword,
|
|
|
|
AlbumID: nil,
|
|
|
|
MediaID: &mediaID,
|
2020-02-11 15:36:12 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
if err := r.Database.Create(&shareToken).Error; err != nil {
|
|
|
|
return nil, errors.Wrap(err, "failed to insert new share token into database")
|
2020-02-11 15:36:12 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
return &shareToken, nil
|
2020-02-11 15:36:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func (r *mutationResolver) DeleteShareToken(ctx context.Context, tokenValue string) (*models.ShareToken, error) {
|
|
|
|
user := auth.UserFromContext(ctx)
|
|
|
|
if user == nil {
|
|
|
|
return nil, auth.ErrUnauthorized
|
|
|
|
}
|
|
|
|
|
2020-06-14 15:07:07 +02:00
|
|
|
token, err := getUserToken(r.Database, user, tokenValue)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
if err := r.Database.Delete(&token).Error; err != nil {
|
2020-07-10 14:26:19 +02:00
|
|
|
return nil, errors.Wrapf(err, "failed to delete share token (%s) from database", tokenValue)
|
2020-06-14 15:07:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return token, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *mutationResolver) ProtectShareToken(ctx context.Context, tokenValue string, password *string) (*models.ShareToken, error) {
|
|
|
|
user := auth.UserFromContext(ctx)
|
|
|
|
if user == nil {
|
|
|
|
return nil, auth.ErrUnauthorized
|
|
|
|
}
|
|
|
|
|
|
|
|
token, err := getUserToken(r.Database, user, tokenValue)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
hashedPassword, err := hashSharePassword(password)
|
2020-06-14 15:07:07 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
token.Password = hashedPassword
|
|
|
|
|
|
|
|
if err := r.Database.Save(&token).Error; err != nil {
|
2020-07-10 14:26:19 +02:00
|
|
|
return nil, errors.Wrap(err, "failed to update password for share token")
|
2020-06-14 15:07:07 +02:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
return token, nil
|
2020-06-14 15:07:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func hashSharePassword(password *string) (*string, error) {
|
2020-11-27 16:02:10 +01:00
|
|
|
var hashedPassword *string = nil
|
2020-06-14 15:07:07 +02:00
|
|
|
if password != nil {
|
|
|
|
hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12)
|
|
|
|
if err != nil {
|
2020-07-10 18:35:37 +02:00
|
|
|
return nil, errors.Wrap(err, "failed to generate hash for share password")
|
2020-06-14 15:07:07 +02:00
|
|
|
}
|
2020-11-27 16:02:10 +01:00
|
|
|
hashedStr := string(hashedPassBytes)
|
|
|
|
hashedPassword = &hashedStr
|
2020-06-14 15:07:07 +02:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
return hashedPassword, nil
|
2020-06-14 15:07:07 +02:00
|
|
|
}
|
|
|
|
|
2020-11-23 19:39:44 +01:00
|
|
|
func getUserToken(db *gorm.DB, user *models.User, tokenValue string) (*models.ShareToken, error) {
|
2020-11-27 16:02:10 +01:00
|
|
|
|
|
|
|
var token models.ShareToken
|
2020-12-17 22:29:24 +01:00
|
|
|
err := db.Where("share_tokens.value = ?", tokenValue).Joins("Owner").Where("Owner.id = ? OR Owner.admin = TRUE", user.ID).First(&token).Error
|
2020-11-27 16:02:10 +01:00
|
|
|
|
2020-02-11 15:36:12 +01:00
|
|
|
if err != nil {
|
2020-07-10 18:35:37 +02:00
|
|
|
return nil, errors.Wrap(err, "failed to get user share token from database")
|
2020-02-11 15:36:12 +01:00
|
|
|
}
|
|
|
|
|
2020-11-27 16:02:10 +01:00
|
|
|
return &token, nil
|
2020-02-09 21:25:33 +01:00
|
|
|
}
|