From 79be99698525be88d260ebb243572f52abfa52e8 Mon Sep 17 00:00:00 2001
From: viktorstrate <viktorstrate@gmail.com>
Date: Sun, 14 Jun 2020 18:28:12 +0200
Subject: [PATCH] Detect if share token is password protected

---
 api/graphql/generated.go             | 102 ++++++++++++++++++++++++---
 api/graphql/resolvers/share_token.go |  15 ++++
 api/graphql/schema.graphql           |   1 +
 ui/src/Pages/SharePage/SharePage.js  |  77 ++++++++++++++------
 4 files changed, 163 insertions(+), 32 deletions(-)

diff --git a/api/graphql/generated.go b/api/graphql/generated.go
index 5611b95..a2a2887 100644
--- a/api/graphql/generated.go
+++ b/api/graphql/generated.go
@@ -137,15 +137,16 @@ type ComplexityRoot struct {
 	}
 
 	Query struct {
-		Album      func(childComplexity int, id int) int
-		MyAlbums   func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool) int
-		MyPhotos   func(childComplexity int, filter *models.Filter) int
-		MyUser     func(childComplexity int) int
-		Photo      func(childComplexity int, id int) int
-		Search     func(childComplexity int, query string, limitPhotos *int, limitAlbums *int) int
-		ShareToken func(childComplexity int, token string, password *string) int
-		SiteInfo   func(childComplexity int) int
-		User       func(childComplexity int, filter *models.Filter) int
+		Album                      func(childComplexity int, id int) int
+		MyAlbums                   func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool) int
+		MyPhotos                   func(childComplexity int, filter *models.Filter) int
+		MyUser                     func(childComplexity int) int
+		Photo                      func(childComplexity int, id int) int
+		Search                     func(childComplexity int, query string, limitPhotos *int, limitAlbums *int) int
+		ShareToken                 func(childComplexity int, token string, password *string) int
+		ShareTokenRequiresPassword func(childComplexity int, token string) int
+		SiteInfo                   func(childComplexity int) int
+		User                       func(childComplexity int, filter *models.Filter) int
 	}
 
 	ScannerResult struct {
@@ -228,6 +229,7 @@ type QueryResolver interface {
 	MyPhotos(ctx context.Context, filter *models.Filter) ([]*models.Photo, error)
 	Photo(ctx context.Context, id int) (*models.Photo, error)
 	ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error)
+	ShareTokenRequiresPassword(ctx context.Context, token string) (bool, error)
 	Search(ctx context.Context, query string, limitPhotos *int, limitAlbums *int) (*models.SearchResult, error)
 }
 type ShareTokenResolver interface {
@@ -827,6 +829,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Query.ShareToken(childComplexity, args["token"].(string), args["password"].(*string)), true
 
+	case "Query.shareTokenRequiresPassword":
+		if e.complexity.Query.ShareTokenRequiresPassword == nil {
+			break
+		}
+
+		args, err := ec.field_Query_shareTokenRequiresPassword_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.ShareTokenRequiresPassword(childComplexity, args["token"].(string)), true
+
 	case "Query.siteInfo":
 		if e.complexity.Query.SiteInfo == nil {
 			break
@@ -1108,6 +1122,7 @@ type Query {
   photo(id: Int!): Photo!
 
   shareToken(token: String!, password: String): ShareToken!
+  shareTokenRequiresPassword(token: String!): Boolean!
 
   search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
 }
@@ -1752,6 +1767,20 @@ func (ec *executionContext) field_Query_search_args(ctx context.Context, rawArgs
 	return args, nil
 }
 
+func (ec *executionContext) field_Query_shareTokenRequiresPassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 string
+	if tmp, ok := rawArgs["token"]; ok {
+		arg0, err = ec.unmarshalNString2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["token"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Query_shareToken_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -4325,6 +4354,47 @@ func (ec *executionContext) _Query_shareToken(ctx context.Context, field graphql
 	return ec.marshalNShareToken2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareToken(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Query_shareTokenRequiresPassword(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:   "Query",
+		Field:    field,
+		Args:     nil,
+		IsMethod: true,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Query_shareTokenRequiresPassword_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	fc.Args = args
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Query().ShareTokenRequiresPassword(rctx, args["token"].(string))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	fc.Result = res
+	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Query_search(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -6880,6 +6950,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 				}
 				return res
 			})
+		case "shareTokenRequiresPassword":
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_shareTokenRequiresPassword(ctx, field)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "search":
 			field := field
 			out.Concurrently(i, func() (res graphql.Marshaler) {
diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go
index e06e356..cd9ff0f 100644
--- a/api/graphql/resolvers/share_token.go
+++ b/api/graphql/resolvers/share_token.go
@@ -84,6 +84,21 @@ func (r *queryResolver) ShareToken(ctx context.Context, tokenValue string, passw
 	return token, nil
 }
 
+func (r *queryResolver) ShareTokenRequiresPassword(ctx context.Context, tokenValue string) (bool, error) {
+	row := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ?", tokenValue)
+	token, err := models.NewShareTokenFromRow(row)
+	if err != nil {
+		if err == sql.ErrNoRows {
+			return false, errors.New("share not found")
+		} else {
+			return false, err
+		}
+	}
+
+	requiresPassword := token.Password != nil
+	return requiresPassword, 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 {
diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql
index 916e3a3..25dbc01 100644
--- a/api/graphql/schema.graphql
+++ b/api/graphql/schema.graphql
@@ -39,6 +39,7 @@ type Query {
   photo(id: Int!): Photo!
 
   shareToken(token: String!, password: String): ShareToken!
+  shareTokenRequiresPassword(token: String!): Boolean!
 
   search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
 }
diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.js
index 2a1be2b..c02200b 100644
--- a/ui/src/Pages/SharePage/SharePage.js
+++ b/ui/src/Pages/SharePage/SharePage.js
@@ -1,14 +1,15 @@
 import React from 'react'
+import PropTypes from 'prop-types'
 import RouterProps from 'react-router-prop-types'
 import { Route, Switch } from 'react-router-dom'
 import AlbumSharePage from './AlbumSharePage'
 import PhotoSharePage from './PhotoSharePage'
-import { Query } from 'react-apollo'
+import { useQuery } from 'react-apollo'
 import gql from 'graphql-tag'
 
 const tokenQuery = gql`
-  query SharePageToken($token: String!) {
-    shareToken(token: $token) {
+  query SharePageToken($token: String!, $password: String) {
+    shareToken(token: $token, password: $password) {
       token
       album {
         ...AlbumProps
@@ -70,30 +71,60 @@ const tokenQuery = gql`
   }
 `
 
+const tokenPasswordProtectedQuery = gql`
+  query ShareTokenRequiresPassword($token: String!) {
+    shareTokenRequiresPassword(token: $token)
+  }
+`
+
+const AuthorizedTokenRoute = ({ match, password }) => {
+  const { loading, error, data } = useQuery(tokenQuery, {
+    variables: { token: match.params.token, password },
+  })
+
+  if (error) return error.message
+  if (loading) return 'Loading...'
+
+  if (data.shareToken.album) {
+    return <AlbumSharePage album={data.shareToken.album} match={match} />
+  }
+
+  if (data.shareToken.photo) {
+    return <PhotoSharePage photo={data.shareToken.photo} />
+  }
+
+  return <h1>Share not found</h1>
+}
+
+AuthorizedTokenRoute.propTypes = {
+  match: PropTypes.object.isRequired,
+  password: PropTypes.string,
+}
+
+const TokenRoute = ({ match }) => {
+  const { loading, error, data } = useQuery(tokenPasswordProtectedQuery, {
+    variables: { token: match.params.token },
+  })
+
+  if (error) return error.message
+  if (loading) return 'Loading...'
+
+  if (data.shareTokenRequiresPassword == true) {
+    return 'Please provide password'
+  }
+
+  return <AuthorizedTokenRoute match={match} />
+}
+
+TokenRoute.propTypes = {
+  match: PropTypes.object.isRequired,
+}
+
 const SharePage = ({ match }) => {
   return (
     <Switch>
       <Route path={`${match.url}/:token`}>
-        {({ match }) => (
-          <Query query={tokenQuery} variables={{ token: match.params.token }}>
-            {({ loading, error, data }) => {
-              if (error) return error.message
-              if (loading) return 'Loading...'
-
-              if (data.shareToken.album) {
-                return (
-                  <AlbumSharePage album={data.shareToken.album} match={match} />
-                )
-              }
-
-              if (data.shareToken.photo) {
-                return <PhotoSharePage photo={data.shareToken.photo} />
-              }
-
-              return <h1>Share not found</h1>
-            }}
-          </Query>
-        )}
+        {({ match }) => <TokenRoute match={match} />}
       </Route>
       <Route path="/">Share not found</Route>
     </Switch>
