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 (
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,

View File

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

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
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 (
"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) {

View File

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

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"
"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 {

View File

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

View File

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

View File

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

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 {
token: ID!
owner: User
owner: User!
# Optional expire date
expire: Time
expire: Time!
# Optional password
# password: String

View File

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

View File

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

View File

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

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'
const albumQuery = gql`
query albumQuery($id: ID!) {
query albumQuery($id: Int!) {
album(id: $id) {
id
title

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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