Work on share tokens
This commit is contained in:
parent
963acf11e8
commit
a3a4dda286
|
@ -1,7 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS user (
|
||||
user_id int NOT NULL AUTO_INCREMENT,
|
||||
username varchar(255) NOT NULL UNIQUE,
|
||||
password varchar(255) NOT NULL,
|
||||
username varchar(256) NOT NULL UNIQUE,
|
||||
password varchar(256) NOT NULL,
|
||||
root_path varchar(512),
|
||||
admin boolean NOT NULL DEFAULT 0,
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS album (
|
|||
CREATE TABLE IF NOT EXISTS photo (
|
||||
photo_id int NOT NULL AUTO_INCREMENT,
|
||||
title varchar(256) NOT NULL,
|
||||
path varchar(512) NOT NULL UNIQUE,
|
||||
path varchar(1024) NOT NULL UNIQUE,
|
||||
album_id int NOT NULL,
|
||||
exif_id int,
|
||||
|
||||
|
@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS photo (
|
|||
CREATE TABLE IF NOT EXISTS photo_url (
|
||||
url_id int NOT NULL AUTO_INCREMENT,
|
||||
photo_id int NOT NULL,
|
||||
photo_name varchar(256) NOT NULL,
|
||||
photo_name varchar(512) NOT NULL,
|
||||
width int NOT NULL,
|
||||
height int NOT NULL,
|
||||
purpose varchar(64) NOT NULL,
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
|
||||
DROP TABLE IF EXISTS share_token;
|
|
@ -0,0 +1,12 @@
|
|||
CREATE TABLE IF NOT EXISTS share_token (
|
||||
token_id int AUTO_INCREMENT,
|
||||
value char(24) NOT NULL UNIQUE,
|
||||
owner_id int NOT NULL,
|
||||
expire timestamp,
|
||||
password varchar(256) NOT NULL,
|
||||
album_id int,
|
||||
photo_id int,
|
||||
|
||||
PRIMARY KEY (token_id),
|
||||
CHECK (album_id IS NOT NULL OR photo_id IS NOT NULL)
|
||||
);
|
|
@ -28,3 +28,5 @@ models:
|
|||
model: github.com/viktorstrate/photoview/api/graphql/models.PhotoURL
|
||||
Album:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.Album
|
||||
ShareToken:
|
||||
model: github.com/viktorstrate/photoview/api/graphql/models.ShareToken
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,6 @@ package models
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Album struct {
|
||||
|
@ -13,8 +12,8 @@ type Album struct {
|
|||
Path string
|
||||
}
|
||||
|
||||
func (a *Album) ID() string {
|
||||
return strconv.Itoa(a.AlbumID)
|
||||
func (a *Album) ID() int {
|
||||
return a.AlbumID
|
||||
}
|
||||
|
||||
func NewAlbumFromRow(row *sql.Row) (*Album, error) {
|
||||
|
|
|
@ -5,7 +5,6 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Photo struct {
|
||||
|
@ -16,6 +15,10 @@ type Photo struct {
|
|||
ExifId *int
|
||||
}
|
||||
|
||||
func (p *Photo) ID() int {
|
||||
return p.PhotoID
|
||||
}
|
||||
|
||||
type PhotoPurpose string
|
||||
|
||||
const (
|
||||
|
@ -34,10 +37,6 @@ type PhotoURL struct {
|
|||
ContentType string
|
||||
}
|
||||
|
||||
func (p *Photo) ID() string {
|
||||
return strconv.Itoa(p.PhotoID)
|
||||
}
|
||||
|
||||
func NewPhotoFromRow(row *sql.Row) (*Photo, error) {
|
||||
photo := Photo{}
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
type ShareToken struct {
|
||||
TokenID int
|
||||
Value string
|
||||
OwnerID int
|
||||
Expire *time.Time
|
||||
Password *string
|
||||
AlbumID *int
|
||||
PhotoID *int
|
||||
}
|
||||
|
||||
func (share *ShareToken) Token() string {
|
||||
return share.Value
|
||||
}
|
||||
|
||||
func (share *ShareToken) ID() int {
|
||||
return share.TokenID
|
||||
}
|
|
@ -6,7 +6,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
@ -20,8 +19,8 @@ type User struct {
|
|||
Admin bool
|
||||
}
|
||||
|
||||
func (u *User) ID() string {
|
||||
return strconv.Itoa(u.UserID)
|
||||
func (u *User) ID() int {
|
||||
return u.UserID
|
||||
}
|
||||
|
||||
type AccessToken struct {
|
||||
|
|
|
@ -33,7 +33,7 @@ func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter) ([]
|
|||
return albums, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) Album(ctx context.Context, id *string) (*models.Album, error) {
|
||||
func (r *queryResolver) Album(ctx context.Context, id int) (*models.Album, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
|
|
|
@ -28,7 +28,7 @@ func (r *queryResolver) MyPhotos(ctx context.Context, filter *models.Filter) ([]
|
|||
return models.NewPhotosFromRows(rows)
|
||||
}
|
||||
|
||||
func (r *queryResolver) Photo(ctx context.Context, id string) (*models.Photo, error) {
|
||||
func (r *queryResolver) Photo(ctx context.Context, id int) (*models.Photo, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
func (r *mutationResolver) ScanAll(ctx context.Context) (*models.ScannerResult, error) {
|
||||
panic("Not implemented")
|
||||
}
|
||||
func (r *mutationResolver) ScanUser(ctx context.Context, userID string) (*models.ScannerResult, error) {
|
||||
func (r *mutationResolver) ScanUser(ctx context.Context, userID int) (*models.ScannerResult, error) {
|
||||
if err := scanner.ScanUser(r.Database, userID); err != nil {
|
||||
errorMessage := fmt.Sprintf("Error scanning user: %s", err.Error())
|
||||
return &models.ScannerResult{
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
package resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
api "github.com/viktorstrate/photoview/api/graphql"
|
||||
"github.com/viktorstrate/photoview/api/graphql/auth"
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
"github.com/viktorstrate/photoview/api/utils"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type shareTokenResolver struct {
|
||||
*Resolver
|
||||
}
|
||||
|
||||
func (r *Resolver) ShareToken() api.ShareTokenResolver {
|
||||
return &shareTokenResolver{r}
|
||||
}
|
||||
|
||||
func (r *shareTokenResolver) Owner(ctx context.Context, obj *models.ShareToken) (*models.User, error) {
|
||||
row := r.Database.QueryRow("SELECT * FROM user WHERE user.user_id = ?", obj.OwnerID)
|
||||
return models.NewUserFromRow(row)
|
||||
}
|
||||
|
||||
func (r *shareTokenResolver) Album(ctx context.Context, obj *models.ShareToken) (*models.Album, error) {
|
||||
row := r.Database.QueryRow("SELECT * FROM album WHERE album.album_id = ?", obj.AlbumID)
|
||||
album, err := models.NewAlbumFromRow(row)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return album, nil
|
||||
}
|
||||
|
||||
func (r *shareTokenResolver) Photo(ctx context.Context, obj *models.ShareToken) (*models.Photo, error) {
|
||||
row := r.Database.QueryRow("SELECT * FROM photo WHERE photo.photo_id = ?", obj.PhotoID)
|
||||
photo, err := models.NewPhotoFromRow(row)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return photo, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) AlbumShares(ctx context.Context, id int, password *string) ([]*models.ShareToken, error) {
|
||||
log.Println("Query AlbumShares: not implemented")
|
||||
|
||||
tokens := make([]*models.ShareToken, 0)
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (r *queryResolver) PhotoShares(ctx context.Context, id int, password *string) ([]*models.ShareToken, error) {
|
||||
log.Println("Query PhotoShares: not implemented")
|
||||
|
||||
tokens := make([]*models.ShareToken, 0)
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
||||
user := auth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
|
||||
rows, err := r.Database.Query("SELECT owner_id FROM album WHERE album.album_id = ? AND album.owner_id = ?", albumID, user.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rows.Next() == false {
|
||||
return nil, auth.ErrUnauthorized
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
var hashed_password *string = nil
|
||||
if password != nil {
|
||||
hashedPassBytes, err := bcrypt.GenerateFromPassword([]byte(*password), 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hashed_str := string(hashedPassBytes)
|
||||
hashed_password = &hashed_str
|
||||
}
|
||||
|
||||
token := utils.GenerateToken()
|
||||
res, err := r.Database.Exec("INSERT INTO share_token (value, owner_id, expire, password, album_id) VALUES (?, ?, ?, ?, ?)", token, user.UserID, expire, hashed_password, albumID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token_id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &models.ShareToken{
|
||||
TokenID: int(token_id),
|
||||
Value: token,
|
||||
OwnerID: user.UserID,
|
||||
Expire: expire,
|
||||
Password: password,
|
||||
AlbumID: &albumID,
|
||||
PhotoID: nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) SharePhoto(ctx context.Context, photoID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
||||
panic("not implemented")
|
||||
}
|
|
@ -74,9 +74,9 @@ type Photo {
|
|||
|
||||
type ShareToken {
|
||||
token: ID!
|
||||
owner: User
|
||||
owner: User!
|
||||
# Optional expire date
|
||||
expire: Time
|
||||
expire: Time!
|
||||
# Optional password
|
||||
# password: String
|
||||
|
||||
|
|
|
@ -25,12 +25,15 @@ type Query {
|
|||
"List of albums owned by the logged in user"
|
||||
myAlbums(filter: Filter): [Album!]!
|
||||
"Get album by id, user must own the album or be admin"
|
||||
album(id: ID): Album!
|
||||
album(id: Int!): Album!
|
||||
|
||||
"List of photos owned by the logged in user"
|
||||
myPhotos(filter: Filter): [Photo!]!
|
||||
"Get photo by id, user must own the photo or be admin"
|
||||
photo(id: ID!): Photo!
|
||||
photo(id: Int!): Photo!
|
||||
|
||||
albumShares(id: Int!, password: String): [ShareToken!]!
|
||||
photoShares(id: Int!, password: String): [ShareToken!]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -53,7 +56,12 @@ type Mutation {
|
|||
"Scan all users for new photos"
|
||||
scanAll: ScannerResult!
|
||||
"Scan a single user for new photos"
|
||||
scanUser(userId: ID!): ScannerResult!
|
||||
scanUser(userId: Int!): ScannerResult!
|
||||
|
||||
"Generate share token for album"
|
||||
shareAlbum(albumId: Int!, expire: Time, password: String): ShareToken
|
||||
"Generate share token for photo"
|
||||
sharePhoto(photoId: Int!, expire: Time, password: String): ShareToken
|
||||
}
|
||||
|
||||
type AuthorizeResult {
|
||||
|
@ -69,13 +77,28 @@ type ScannerResult {
|
|||
message: String
|
||||
}
|
||||
|
||||
"A token used to publicly access an album or photo"
|
||||
type ShareToken {
|
||||
id: Int!
|
||||
token: String!
|
||||
"The user who created the token"
|
||||
owner: User!
|
||||
"Optional expire date"
|
||||
expire: Time
|
||||
|
||||
"The album this token shares"
|
||||
album: Album
|
||||
"The photo this token shares"
|
||||
photo: Photo
|
||||
}
|
||||
|
||||
"General public information about the site"
|
||||
type SiteInfo {
|
||||
initialSetup: Boolean!
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID!
|
||||
id: Int!
|
||||
username: String!
|
||||
#albums: [Album]
|
||||
"Local filepath for the user's photos"
|
||||
|
@ -85,13 +108,19 @@ type User {
|
|||
}
|
||||
|
||||
type Album {
|
||||
id: ID!
|
||||
id: Int!
|
||||
title: String!
|
||||
"The photos inside this album"
|
||||
photos(filter: Filter): [Photo!]!
|
||||
"The albums contained in this album"
|
||||
subAlbums(filter: Filter): [Album!]!
|
||||
"The album witch contains this album"
|
||||
parentAlbum: Album
|
||||
"The user who owns this album"
|
||||
owner: User!
|
||||
"The path on the filesystem of the server, where this album is located"
|
||||
path: String!
|
||||
"An image in this album used for previewing this album"
|
||||
thumbnail: Photo
|
||||
|
||||
# shares: [ShareToken]
|
||||
|
@ -107,7 +136,7 @@ type PhotoURL {
|
|||
}
|
||||
|
||||
type Photo {
|
||||
id: ID!
|
||||
id: Int!
|
||||
title: String!
|
||||
"Local filepath for the photo"
|
||||
path: String!
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"image"
|
||||
"image/jpeg"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
|
@ -14,6 +13,7 @@ import (
|
|||
|
||||
"github.com/nfnt/resize"
|
||||
"github.com/viktorstrate/photoview/api/graphql/models"
|
||||
"github.com/viktorstrate/photoview/api/utils"
|
||||
|
||||
// Image decoders
|
||||
_ "golang.org/x/image/bmp"
|
||||
|
@ -68,7 +68,7 @@ func ProcessImage(tx *sql.Tx, photoPath string, albumId int, content_type string
|
|||
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
|
||||
photoBaseExt := path.Ext(photoName)
|
||||
|
||||
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, generateToken())
|
||||
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
|
||||
original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
|
||||
|
||||
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo_id, original_image_name, image.Bounds().Max.X, image.Bounds().Max.Y, models.PhotoOriginal, content_type)
|
||||
|
@ -96,7 +96,7 @@ func ProcessImage(tx *sql.Tx, photoPath string, albumId int, content_type string
|
|||
}
|
||||
|
||||
// Save thumbnail as jpg
|
||||
thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", photoName, generateToken())
|
||||
thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", photoName, utils.GenerateToken())
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, ".", "_")
|
||||
thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
|
||||
thumbnail_name = thumbnail_name + ".jpg"
|
||||
|
@ -118,14 +118,3 @@ func ProcessImage(tx *sql.Tx, photoPath string, albumId int, content_type string
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateToken() string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
const length = 8
|
||||
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
|
|
@ -29,7 +29,7 @@ func (cache *scanner_cache) get_photo_type(path string) *string {
|
|||
return &photo_type
|
||||
}
|
||||
|
||||
func ScanUser(database *sql.DB, userId string) error {
|
||||
func ScanUser(database *sql.DB, userId int) error {
|
||||
|
||||
row := database.QueryRow("SELECT * FROM user WHERE user_id = ?", userId)
|
||||
user, err := models.NewUserFromRow(row)
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package utils
|
||||
|
||||
import "math/rand"
|
||||
|
||||
func GenerateToken() string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
const length = 8
|
||||
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
|
@ -10,7 +10,7 @@ import AlbumTitle from '../../components/AlbumTitle'
|
|||
import AlbumGallery from '../../components/albumGallery/AlbumGallery'
|
||||
|
||||
const albumQuery = gql`
|
||||
query albumQuery($id: ID!) {
|
||||
query albumQuery($id: Int!) {
|
||||
album(id: $id) {
|
||||
id
|
||||
title
|
||||
|
|
|
@ -14,7 +14,7 @@ import gql from 'graphql-tag'
|
|||
|
||||
const updateUserMutation = gql`
|
||||
mutation updateUser(
|
||||
$id: ID!
|
||||
$id: Int!
|
||||
$username: String
|
||||
$rootPath: String
|
||||
$admin: Boolean
|
||||
|
@ -34,7 +34,7 @@ const updateUserMutation = gql`
|
|||
`
|
||||
|
||||
const deleteUserMutation = gql`
|
||||
mutation deleteUser($id: ID!) {
|
||||
mutation deleteUser($id: Int!) {
|
||||
deleteUser(id: $id) {
|
||||
id
|
||||
username
|
||||
|
|
|
@ -7,7 +7,7 @@ import { Query } from 'react-apollo'
|
|||
import gql from 'graphql-tag'
|
||||
|
||||
const tokenQuery = gql`
|
||||
query SharePageToken($token: ID!) {
|
||||
query SharePageToken($token: Int!) {
|
||||
shareToken(token: $token) {
|
||||
token
|
||||
album {
|
||||
|
|
|
@ -5,7 +5,7 @@ import gql from 'graphql-tag'
|
|||
import SidebarShare from './Sharing'
|
||||
|
||||
const albumQuery = gql`
|
||||
query getAlbumSidebar($id: ID!) {
|
||||
query getAlbumSidebar($id: Int!) {
|
||||
album(id: $id) {
|
||||
id
|
||||
title
|
||||
|
|
|
@ -9,7 +9,7 @@ import SidebarShare from './Sharing'
|
|||
import SidebarDownload from './SidebarDownload'
|
||||
|
||||
const photoQuery = gql`
|
||||
query sidebarPhoto($id: ID!) {
|
||||
query sidebarPhoto($id: Int!) {
|
||||
photo(id: $id) {
|
||||
id
|
||||
title
|
||||
|
|
|
@ -6,7 +6,7 @@ import { Table, Button, Dropdown } from 'semantic-ui-react'
|
|||
import copy from 'copy-to-clipboard'
|
||||
|
||||
const sharePhotoQuery = gql`
|
||||
query sidbarGetPhotoShares($id: ID!) {
|
||||
query sidbarGetPhotoShares($id: Int!) {
|
||||
photoShares(id: $id) {
|
||||
token
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ const sharePhotoQuery = gql`
|
|||
`
|
||||
|
||||
const shareAlbumQuery = gql`
|
||||
query sidbarGetAlbumShares($id: ID!) {
|
||||
query sidbarGetAlbumShares($id: Int!) {
|
||||
albumShares(id: $id) {
|
||||
token
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const shareAlbumQuery = gql`
|
|||
|
||||
const addPhotoShareMutation = gql`
|
||||
mutation sidebarPhotoAddShare(
|
||||
$id: ID!
|
||||
$id: Int!
|
||||
$password: String
|
||||
$expire: _Neo4jDateInput
|
||||
) {
|
||||
|
@ -35,7 +35,7 @@ const addPhotoShareMutation = gql`
|
|||
|
||||
const addAlbumShareMutation = gql`
|
||||
mutation sidebarAlbumAddShare(
|
||||
$id: ID!
|
||||
$id: Int!
|
||||
$password: String
|
||||
$expire: _Neo4jDateInput
|
||||
) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import gql from 'graphql-tag'
|
|||
import download from 'downloadjs'
|
||||
|
||||
const downloadQuery = gql`
|
||||
query sidebarDownloadQuery($photoId: ID!) {
|
||||
query sidebarDownloadQuery($photoId: Int!) {
|
||||
photo(id: $photoId) {
|
||||
id
|
||||
downloads {
|
||||
|
|
Loading…
Reference in New Issue