diff --git a/api/graphql/generated.go b/api/graphql/generated.go index a2a28871..bb1481fc 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -144,7 +144,7 @@ type ComplexityRoot struct { 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 + ShareTokenValidatePassword func(childComplexity int, token string, password *string) int SiteInfo func(childComplexity int) int User func(childComplexity int, filter *models.Filter) int } @@ -229,7 +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) + ShareTokenValidatePassword(ctx context.Context, token string, password *string) (bool, error) Search(ctx context.Context, query string, limitPhotos *int, limitAlbums *int) (*models.SearchResult, error) } type ShareTokenResolver interface { @@ -829,17 +829,17 @@ 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 { + case "Query.shareTokenValidatePassword": + if e.complexity.Query.ShareTokenValidatePassword == nil { break } - args, err := ec.field_Query_shareTokenRequiresPassword_args(context.TODO(), rawArgs) + args, err := ec.field_Query_shareTokenValidatePassword_args(context.TODO(), rawArgs) if err != nil { return 0, false } - return e.complexity.Query.ShareTokenRequiresPassword(childComplexity, args["token"].(string)), true + return e.complexity.Query.ShareTokenValidatePassword(childComplexity, args["token"].(string), args["password"].(*string)), true case "Query.siteInfo": if e.complexity.Query.SiteInfo == nil { @@ -1122,7 +1122,7 @@ type Query { photo(id: Int!): Photo! shareToken(token: String!, password: String): ShareToken! - shareTokenRequiresPassword(token: String!): Boolean! + shareTokenValidatePassword(token: String!, password: String): Boolean! search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult! } @@ -1767,7 +1767,7 @@ 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) { +func (ec *executionContext) field_Query_shareTokenValidatePassword_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} var arg0 string @@ -1778,6 +1778,14 @@ func (ec *executionContext) field_Query_shareTokenRequiresPassword_args(ctx cont } } args["token"] = arg0 + var arg1 *string + if tmp, ok := rawArgs["password"]; ok { + arg1, err = ec.unmarshalOString2ᚖstring(ctx, tmp) + if err != nil { + return nil, err + } + } + args["password"] = arg1 return args, nil } @@ -4354,7 +4362,7 @@ 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) { +func (ec *executionContext) _Query_shareTokenValidatePassword(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { ec.Error(ctx, ec.Recover(ctx, r)) @@ -4370,7 +4378,7 @@ func (ec *executionContext) _Query_shareTokenRequiresPassword(ctx context.Contex ctx = graphql.WithFieldContext(ctx, fc) rawArgs := field.ArgumentMap(ec.Variables) - args, err := ec.field_Query_shareTokenRequiresPassword_args(ctx, rawArgs) + args, err := ec.field_Query_shareTokenValidatePassword_args(ctx, rawArgs) if err != nil { ec.Error(ctx, err) return graphql.Null @@ -4378,7 +4386,7 @@ func (ec *executionContext) _Query_shareTokenRequiresPassword(ctx context.Contex 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)) + return ec.resolvers.Query().ShareTokenValidatePassword(rctx, args["token"].(string), args["password"].(*string)) }) if err != nil { ec.Error(ctx, err) @@ -6950,7 +6958,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) - case "shareTokenRequiresPassword": + case "shareTokenValidatePassword": field := field out.Concurrently(i, func() (res graphql.Marshaler) { defer func() { @@ -6958,7 +6966,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr ec.Error(ctx, ec.Recover(ctx, r)) } }() - res = ec._Query_shareTokenRequiresPassword(ctx, field) + res = ec._Query_shareTokenValidatePassword(ctx, field) if res == graphql.Null { atomic.AddUint32(&invalids, 1) } diff --git a/api/graphql/resolvers/share_token.go b/api/graphql/resolvers/share_token.go index cd9ff0fc..7ace2f32 100644 --- a/api/graphql/resolvers/share_token.go +++ b/api/graphql/resolvers/share_token.go @@ -62,11 +62,6 @@ func (r *shareTokenResolver) HasPassword(ctx context.Context, obj *models.ShareT func (r *queryResolver) ShareToken(ctx context.Context, tokenValue string, password *string) (*models.ShareToken, error) { - hashed_password, err := hashSharePassword(password) - if err != nil { - return nil, errors.Wrap(err, "Failed to hash password") - } - row := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ?", tokenValue) token, err := models.NewShareTokenFromRow(row) if err != nil { @@ -77,14 +72,20 @@ func (r *queryResolver) ShareToken(ctx context.Context, tokenValue string, passw } } - if token.Password != nil && hashed_password != token.Password { - return nil, errors.New("unauthorized") + if token.Password != nil { + if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*password)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + return nil, errors.New("unauthorized") + } else { + return nil, errors.New("internal server error") + } + } } return token, nil } -func (r *queryResolver) ShareTokenRequiresPassword(ctx context.Context, tokenValue string) (bool, error) { +func (r *queryResolver) ShareTokenValidatePassword(ctx context.Context, tokenValue string, password *string) (bool, error) { row := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ?", tokenValue) token, err := models.NewShareTokenFromRow(row) if err != nil { @@ -95,8 +96,23 @@ func (r *queryResolver) ShareTokenRequiresPassword(ctx context.Context, tokenVal } } - requiresPassword := token.Password != nil - return requiresPassword, nil + if token.Password == nil { + return true, nil + } + + if password == nil { + return false, nil + } + + if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*password)); err != nil { + if err == bcrypt.ErrMismatchedHashAndPassword { + return false, nil + } else { + return false, err + } + } + + return true, nil } func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) { diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 25dbc01b..7187a80d 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -39,7 +39,7 @@ type Query { photo(id: Int!): Photo! shareToken(token: String!, password: String): ShareToken! - shareTokenRequiresPassword(token: String!): Boolean! + shareTokenValidatePassword(token: String!, password: String): Boolean! search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult! } diff --git a/api/server/cors_middleware.go b/api/server/cors_middleware.go index 37e185d2..007ee380 100644 --- a/api/server/cors_middleware.go +++ b/api/server/cors_middleware.go @@ -14,7 +14,7 @@ func CORSMiddleware(devMode bool) mux.MiddlewareFunc { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { methods := []string{http.MethodGet, http.MethodPost, http.MethodOptions} - headers := []string{"authorization", "content-type", "content-length"} + headers := []string{"authorization", "content-type", "content-length", "TokenPassword"} w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ",")) w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ",")) diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.js index c02200b8..2321a8c9 100644 --- a/ui/src/Pages/SharePage/SharePage.js +++ b/ui/src/Pages/SharePage/SharePage.js @@ -1,13 +1,23 @@ -import React from 'react' +import React, { useState } from 'react' import PropTypes from 'prop-types' +import styled from 'styled-components' import RouterProps from 'react-router-prop-types' import { Route, Switch } from 'react-router-dom' import AlbumSharePage from './AlbumSharePage' import PhotoSharePage from './PhotoSharePage' import { useQuery } from 'react-apollo' import gql from 'graphql-tag' +import { + Container, + Header, + Form, + Button, + Input, + Icon, + Message, +} from 'semantic-ui-react' -const tokenQuery = gql` +const shareTokenQuery = gql` query SharePageToken($token: String!, $password: String) { shareToken(token: $token, password: $password) { token @@ -71,15 +81,20 @@ const tokenQuery = gql` } ` -const tokenPasswordProtectedQuery = gql` - query ShareTokenRequiresPassword($token: String!) { - shareTokenRequiresPassword(token: $token) +const validateTokenPasswordQuery = gql` + query ShareTokenValidatePassword($token: String!, $password: String) { + shareTokenValidatePassword(token: $token, password: $password) } ` -const AuthorizedTokenRoute = ({ match, password }) => { - const { loading, error, data } = useQuery(tokenQuery, { - variables: { token: match.params.token, password }, +const AuthorizedTokenRoute = ({ match }) => { + const token = match.params.token + + const { loading, error, data } = useQuery(shareTokenQuery, { + variables: { + token, + password: sessionStorage.getItem(`share-token-pw-${token}`), + }, }) if (error) return error.message @@ -98,21 +113,107 @@ const AuthorizedTokenRoute = ({ match, password }) => { AuthorizedTokenRoute.propTypes = { match: PropTypes.object.isRequired, - password: PropTypes.string, +} + +const MessageContainer = styled.div` + max-width: 400px; + margin: 100px auto 0; +` + +const ProtectedTokenEnterPassword = ({ + match, + refetchWithPassword, + loading = false, +}) => { + const [passwordValue, setPasswordValue] = useState('') + const [invalidPassword, setInvalidPassword] = useState(false) + + const onSubmit = () => { + refetchWithPassword(passwordValue) + setInvalidPassword(true) + } + + let errorMessage = null + if (invalidPassword && !loading) { + errorMessage = ( + + Wrong password, please try again. + + ) + } + + return ( + + + Protected share + + This share is protected with a password. + + + Password + event.key == 'Enter' && onSubmit()} + onChange={e => setPasswordValue(e.target.value)} + placeholder="Password" + type="password" + icon={} + /> + + {errorMessage} + + + ) +} + +ProtectedTokenEnterPassword.propTypes = { + match: PropTypes.object.isRequired, + refetchWithPassword: PropTypes.func.isRequired, + loading: PropTypes.bool, } const TokenRoute = ({ match }) => { - const { loading, error, data } = useQuery(tokenPasswordProtectedQuery, { - variables: { token: match.params.token }, - }) + const token = match.params.token - if (error) return error.message - if (loading) return 'Loading...' + const { loading, error, data, refetch } = useQuery( + validateTokenPasswordQuery, + { + variables: { + token: match.params.token, + password: sessionStorage.getItem(`share-token-pw-${token}`), + }, + } + ) - if (data.shareTokenRequiresPassword == true) { - return 'Please provide password' + if (error) { + if (error.message == 'GraphQL error: share not found') { + return ( + + Share not found + Maybe the share has expired or has been deleted. + + ) + } + + return error.message } + if (data && data.shareTokenValidatePassword == false) { + return ( + { + sessionStorage.setItem(`share-token-pw-${token}`, password) + refetch({ variables: { password: password } }) + }} + loading={loading} + /> + ) + } + + if (loading) return 'Loading...' + return } @@ -126,7 +227,7 @@ const SharePage = ({ match }) => { {({ match }) => } - Share not found + Route not found ) }
This share is protected with a password.
Maybe the share has expired or has been deleted.