1
Fork 0

Work on share tokens

This commit is contained in:
viktorstrate 2020-02-09 21:25:33 +01:00
parent 963acf11e8
commit a3a4dda286
26 changed files with 1067 additions and 124 deletions

View File

@ -1,7 +1,7 @@
CREATE TABLE IF NOT EXISTS user ( CREATE TABLE IF NOT EXISTS user (
user_id int NOT NULL AUTO_INCREMENT, user_id int NOT NULL AUTO_INCREMENT,
username varchar(255) NOT NULL UNIQUE, username varchar(256) NOT NULL UNIQUE,
password varchar(255) NOT NULL, password varchar(256) NOT NULL,
root_path varchar(512), root_path varchar(512),
admin boolean NOT NULL DEFAULT 0, admin boolean NOT NULL DEFAULT 0,

View File

@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS album (
CREATE TABLE IF NOT EXISTS photo ( CREATE TABLE IF NOT EXISTS photo (
photo_id int NOT NULL AUTO_INCREMENT, photo_id int NOT NULL AUTO_INCREMENT,
title varchar(256) NOT NULL, title varchar(256) NOT NULL,
path varchar(512) NOT NULL UNIQUE, path varchar(1024) NOT NULL UNIQUE,
album_id int NOT NULL, album_id int NOT NULL,
exif_id int, exif_id int,
@ -41,7 +41,7 @@ CREATE TABLE IF NOT EXISTS photo (
CREATE TABLE IF NOT EXISTS photo_url ( CREATE TABLE IF NOT EXISTS photo_url (
url_id int NOT NULL AUTO_INCREMENT, url_id int NOT NULL AUTO_INCREMENT,
photo_id int NOT NULL, photo_id int NOT NULL,
photo_name varchar(256) NOT NULL, photo_name varchar(512) NOT NULL,
width int NOT NULL, width int NOT NULL,
height int NOT NULL, height int NOT NULL,
purpose varchar(64) NOT NULL, purpose varchar(64) NOT NULL,

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS share_token;

View File

@ -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)
);

View File

@ -28,3 +28,5 @@ models:
model: github.com/viktorstrate/photoview/api/graphql/models.PhotoURL model: github.com/viktorstrate/photoview/api/graphql/models.PhotoURL
Album: Album:
model: github.com/viktorstrate/photoview/api/graphql/models.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

View File

@ -2,7 +2,6 @@ package models
import ( import (
"database/sql" "database/sql"
"strconv"
) )
type Album struct { type Album struct {
@ -13,8 +12,8 @@ type Album struct {
Path string Path string
} }
func (a *Album) ID() string { func (a *Album) ID() int {
return strconv.Itoa(a.AlbumID) return a.AlbumID
} }
func NewAlbumFromRow(row *sql.Row) (*Album, error) { func NewAlbumFromRow(row *sql.Row) (*Album, error) {

View File

@ -5,7 +5,6 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"strconv"
) )
type Photo struct { type Photo struct {
@ -16,6 +15,10 @@ type Photo struct {
ExifId *int ExifId *int
} }
func (p *Photo) ID() int {
return p.PhotoID
}
type PhotoPurpose string type PhotoPurpose string
const ( const (
@ -34,10 +37,6 @@ type PhotoURL struct {
ContentType string ContentType string
} }
func (p *Photo) ID() string {
return strconv.Itoa(p.PhotoID)
}
func NewPhotoFromRow(row *sql.Row) (*Photo, error) { func NewPhotoFromRow(row *sql.Row) (*Photo, error) {
photo := Photo{} photo := Photo{}

View File

@ -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
}

View File

@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"log" "log"
"strconv"
"time" "time"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -20,8 +19,8 @@ type User struct {
Admin bool Admin bool
} }
func (u *User) ID() string { func (u *User) ID() int {
return strconv.Itoa(u.UserID) return u.UserID
} }
type AccessToken struct { type AccessToken struct {

View File

@ -33,7 +33,7 @@ func (r *queryResolver) MyAlbums(ctx context.Context, filter *models.Filter) ([]
return albums, nil 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) user := auth.UserFromContext(ctx)
if user == nil { if user == nil {
return nil, auth.ErrUnauthorized return nil, auth.ErrUnauthorized

View File

@ -28,7 +28,7 @@ func (r *queryResolver) MyPhotos(ctx context.Context, filter *models.Filter) ([]
return models.NewPhotosFromRows(rows) 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) user := auth.UserFromContext(ctx)
if user == nil { if user == nil {
return nil, auth.ErrUnauthorized return nil, auth.ErrUnauthorized

View File

@ -11,7 +11,7 @@ import (
func (r *mutationResolver) ScanAll(ctx context.Context) (*models.ScannerResult, error) { func (r *mutationResolver) ScanAll(ctx context.Context) (*models.ScannerResult, error) {
panic("Not implemented") 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 { if err := scanner.ScanUser(r.Database, userID); err != nil {
errorMessage := fmt.Sprintf("Error scanning user: %s", err.Error()) errorMessage := fmt.Sprintf("Error scanning user: %s", err.Error())
return &models.ScannerResult{ return &models.ScannerResult{

View File

@ -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")
}

View File

@ -74,9 +74,9 @@ type Photo {
type ShareToken { type ShareToken {
token: ID! token: ID!
owner: User owner: User!
# Optional expire date # Optional expire date
expire: Time expire: Time!
# Optional password # Optional password
# password: String # password: String

View File

@ -25,12 +25,15 @@ type Query {
"List of albums owned by the logged in user" "List of albums owned by the logged in user"
myAlbums(filter: Filter): [Album!]! myAlbums(filter: Filter): [Album!]!
"Get album by id, user must own the album or be admin" "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" "List of photos owned by the logged in user"
myPhotos(filter: Filter): [Photo!]! myPhotos(filter: Filter): [Photo!]!
"Get photo by id, user must own the photo or be admin" "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 { type Mutation {
@ -53,7 +56,12 @@ type Mutation {
"Scan all users for new photos" "Scan all users for new photos"
scanAll: ScannerResult! scanAll: ScannerResult!
"Scan a single user for new photos" "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 { type AuthorizeResult {
@ -69,13 +77,28 @@ type ScannerResult {
message: String 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" "General public information about the site"
type SiteInfo { type SiteInfo {
initialSetup: Boolean! initialSetup: Boolean!
} }
type User { type User {
id: ID! id: Int!
username: String! username: String!
#albums: [Album] #albums: [Album]
"Local filepath for the user's photos" "Local filepath for the user's photos"
@ -85,13 +108,19 @@ type User {
} }
type Album { type Album {
id: ID! id: Int!
title: String! title: String!
"The photos inside this album"
photos(filter: Filter): [Photo!]! photos(filter: Filter): [Photo!]!
"The albums contained in this album"
subAlbums(filter: Filter): [Album!]! subAlbums(filter: Filter): [Album!]!
"The album witch contains this album"
parentAlbum: Album parentAlbum: Album
"The user who owns this album"
owner: User! owner: User!
"The path on the filesystem of the server, where this album is located"
path: String! path: String!
"An image in this album used for previewing this album"
thumbnail: Photo thumbnail: Photo
# shares: [ShareToken] # shares: [ShareToken]
@ -107,7 +136,7 @@ type PhotoURL {
} }
type Photo { type Photo {
id: ID! id: Int!
title: String! title: String!
"Local filepath for the photo" "Local filepath for the photo"
path: String! path: String!

View File

@ -6,7 +6,6 @@ import (
"image" "image"
"image/jpeg" "image/jpeg"
"log" "log"
"math/rand"
"os" "os"
"path" "path"
"strconv" "strconv"
@ -14,6 +13,7 @@ import (
"github.com/nfnt/resize" "github.com/nfnt/resize"
"github.com/viktorstrate/photoview/api/graphql/models" "github.com/viktorstrate/photoview/api/graphql/models"
"github.com/viktorstrate/photoview/api/utils"
// Image decoders // Image decoders
_ "golang.org/x/image/bmp" _ "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))] photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
photoBaseExt := 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 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) _, 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 // 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 = strings.ReplaceAll(thumbnail_name, " ", "_") thumbnail_name = strings.ReplaceAll(thumbnail_name, " ", "_")
thumbnail_name = thumbnail_name + ".jpg" thumbnail_name = thumbnail_name + ".jpg"
@ -118,14 +118,3 @@ func ProcessImage(tx *sql.Tx, photoPath string, albumId int, content_type string
return nil 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)
}

View File

@ -29,7 +29,7 @@ func (cache *scanner_cache) get_photo_type(path string) *string {
return &photo_type 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) row := database.QueryRow("SELECT * FROM user WHERE user_id = ?", userId)
user, err := models.NewUserFromRow(row) user, err := models.NewUserFromRow(row)

14
api/utils/utils.go Normal file
View File

@ -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)
}

View File

@ -10,7 +10,7 @@ import AlbumTitle from '../../components/AlbumTitle'
import AlbumGallery from '../../components/albumGallery/AlbumGallery' import AlbumGallery from '../../components/albumGallery/AlbumGallery'
const albumQuery = gql` const albumQuery = gql`
query albumQuery($id: ID!) { query albumQuery($id: Int!) {
album(id: $id) { album(id: $id) {
id id
title title

View File

@ -14,7 +14,7 @@ import gql from 'graphql-tag'
const updateUserMutation = gql` const updateUserMutation = gql`
mutation updateUser( mutation updateUser(
$id: ID! $id: Int!
$username: String $username: String
$rootPath: String $rootPath: String
$admin: Boolean $admin: Boolean
@ -34,7 +34,7 @@ const updateUserMutation = gql`
` `
const deleteUserMutation = gql` const deleteUserMutation = gql`
mutation deleteUser($id: ID!) { mutation deleteUser($id: Int!) {
deleteUser(id: $id) { deleteUser(id: $id) {
id id
username username

View File

@ -7,7 +7,7 @@ import { Query } from 'react-apollo'
import gql from 'graphql-tag' import gql from 'graphql-tag'
const tokenQuery = gql` const tokenQuery = gql`
query SharePageToken($token: ID!) { query SharePageToken($token: Int!) {
shareToken(token: $token) { shareToken(token: $token) {
token token
album { album {

View File

@ -5,7 +5,7 @@ import gql from 'graphql-tag'
import SidebarShare from './Sharing' import SidebarShare from './Sharing'
const albumQuery = gql` const albumQuery = gql`
query getAlbumSidebar($id: ID!) { query getAlbumSidebar($id: Int!) {
album(id: $id) { album(id: $id) {
id id
title title

View File

@ -9,7 +9,7 @@ import SidebarShare from './Sharing'
import SidebarDownload from './SidebarDownload' import SidebarDownload from './SidebarDownload'
const photoQuery = gql` const photoQuery = gql`
query sidebarPhoto($id: ID!) { query sidebarPhoto($id: Int!) {
photo(id: $id) { photo(id: $id) {
id id
title title

View File

@ -6,7 +6,7 @@ import { Table, Button, Dropdown } from 'semantic-ui-react'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
const sharePhotoQuery = gql` const sharePhotoQuery = gql`
query sidbarGetPhotoShares($id: ID!) { query sidbarGetPhotoShares($id: Int!) {
photoShares(id: $id) { photoShares(id: $id) {
token token
} }
@ -14,7 +14,7 @@ const sharePhotoQuery = gql`
` `
const shareAlbumQuery = gql` const shareAlbumQuery = gql`
query sidbarGetAlbumShares($id: ID!) { query sidbarGetAlbumShares($id: Int!) {
albumShares(id: $id) { albumShares(id: $id) {
token token
} }
@ -23,7 +23,7 @@ const shareAlbumQuery = gql`
const addPhotoShareMutation = gql` const addPhotoShareMutation = gql`
mutation sidebarPhotoAddShare( mutation sidebarPhotoAddShare(
$id: ID! $id: Int!
$password: String $password: String
$expire: _Neo4jDateInput $expire: _Neo4jDateInput
) { ) {
@ -35,7 +35,7 @@ const addPhotoShareMutation = gql`
const addAlbumShareMutation = gql` const addAlbumShareMutation = gql`
mutation sidebarAlbumAddShare( mutation sidebarAlbumAddShare(
$id: ID! $id: Int!
$password: String $password: String
$expire: _Neo4jDateInput $expire: _Neo4jDateInput
) { ) {

View File

@ -6,7 +6,7 @@ import gql from 'graphql-tag'
import download from 'downloadjs' import download from 'downloadjs'
const downloadQuery = gql` const downloadQuery = gql`
query sidebarDownloadQuery($photoId: ID!) { query sidebarDownloadQuery($photoId: Int!) {
photo(id: $photoId) { photo(id: $photoId) {
id id
downloads { downloads {