Add login page for protected shares
This commit is contained in:
parent
79be996985
commit
fef27c5640
|
@ -144,7 +144,7 @@ type ComplexityRoot struct {
|
||||||
Photo func(childComplexity int, id int) int
|
Photo func(childComplexity int, id int) int
|
||||||
Search func(childComplexity int, query string, limitPhotos *int, limitAlbums *int) int
|
Search func(childComplexity int, query string, limitPhotos *int, limitAlbums *int) int
|
||||||
ShareToken func(childComplexity int, token string, password *string) 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
|
SiteInfo func(childComplexity int) int
|
||||||
User func(childComplexity int, filter *models.Filter) 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)
|
MyPhotos(ctx context.Context, filter *models.Filter) ([]*models.Photo, error)
|
||||||
Photo(ctx context.Context, id int) (*models.Photo, error)
|
Photo(ctx context.Context, id int) (*models.Photo, error)
|
||||||
ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, 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)
|
Search(ctx context.Context, query string, limitPhotos *int, limitAlbums *int) (*models.SearchResult, error)
|
||||||
}
|
}
|
||||||
type ShareTokenResolver interface {
|
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
|
return e.complexity.Query.ShareToken(childComplexity, args["token"].(string), args["password"].(*string)), true
|
||||||
|
|
||||||
case "Query.shareTokenRequiresPassword":
|
case "Query.shareTokenValidatePassword":
|
||||||
if e.complexity.Query.ShareTokenRequiresPassword == nil {
|
if e.complexity.Query.ShareTokenValidatePassword == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
args, err := ec.field_Query_shareTokenRequiresPassword_args(context.TODO(), rawArgs)
|
args, err := ec.field_Query_shareTokenValidatePassword_args(context.TODO(), rawArgs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, false
|
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":
|
case "Query.siteInfo":
|
||||||
if e.complexity.Query.SiteInfo == nil {
|
if e.complexity.Query.SiteInfo == nil {
|
||||||
|
@ -1122,7 +1122,7 @@ type Query {
|
||||||
photo(id: Int!): Photo!
|
photo(id: Int!): Photo!
|
||||||
|
|
||||||
shareToken(token: String!, password: String): ShareToken!
|
shareToken(token: String!, password: String): ShareToken!
|
||||||
shareTokenRequiresPassword(token: String!): Boolean!
|
shareTokenValidatePassword(token: String!, password: String): Boolean!
|
||||||
|
|
||||||
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
|
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
|
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
|
var err error
|
||||||
args := map[string]interface{}{}
|
args := map[string]interface{}{}
|
||||||
var arg0 string
|
var arg0 string
|
||||||
|
@ -1778,6 +1778,14 @@ func (ec *executionContext) field_Query_shareTokenRequiresPassword_args(ctx cont
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
args["token"] = arg0
|
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
|
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)
|
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() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
@ -4370,7 +4378,7 @@ func (ec *executionContext) _Query_shareTokenRequiresPassword(ctx context.Contex
|
||||||
|
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
rawArgs := field.ArgumentMap(ec.Variables)
|
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 {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
|
@ -4378,7 +4386,7 @@ func (ec *executionContext) _Query_shareTokenRequiresPassword(ctx context.Contex
|
||||||
fc.Args = args
|
fc.Args = args
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
ctx = rctx // use context from middleware stack in children
|
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 {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
|
@ -6950,7 +6958,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
case "shareTokenRequiresPassword":
|
case "shareTokenValidatePassword":
|
||||||
field := field
|
field := field
|
||||||
out.Concurrently(i, func() (res graphql.Marshaler) {
|
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -6958,7 +6966,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
res = ec._Query_shareTokenRequiresPassword(ctx, field)
|
res = ec._Query_shareTokenValidatePassword(ctx, field)
|
||||||
if res == graphql.Null {
|
if res == graphql.Null {
|
||||||
atomic.AddUint32(&invalids, 1)
|
atomic.AddUint32(&invalids, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
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)
|
row := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ?", tokenValue)
|
||||||
token, err := models.NewShareTokenFromRow(row)
|
token, err := models.NewShareTokenFromRow(row)
|
||||||
if err != nil {
|
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 {
|
if token.Password != nil {
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(*token.Password), []byte(*password)); err != nil {
|
||||||
|
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||||
return nil, errors.New("unauthorized")
|
return nil, errors.New("unauthorized")
|
||||||
|
} else {
|
||||||
|
return nil, errors.New("internal server error")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return token, nil
|
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)
|
row := r.Database.QueryRow("SELECT * FROM share_token WHERE value = ?", tokenValue)
|
||||||
token, err := models.NewShareTokenFromRow(row)
|
token, err := models.NewShareTokenFromRow(row)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -95,8 +96,23 @@ func (r *queryResolver) ShareTokenRequiresPassword(ctx context.Context, tokenVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
requiresPassword := token.Password != nil
|
if token.Password == nil {
|
||||||
return requiresPassword, 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) {
|
func (r *mutationResolver) ShareAlbum(ctx context.Context, albumID int, expire *time.Time, password *string) (*models.ShareToken, error) {
|
||||||
|
|
|
@ -39,7 +39,7 @@ type Query {
|
||||||
photo(id: Int!): Photo!
|
photo(id: Int!): Photo!
|
||||||
|
|
||||||
shareToken(token: String!, password: String): ShareToken!
|
shareToken(token: String!, password: String): ShareToken!
|
||||||
shareTokenRequiresPassword(token: String!): Boolean!
|
shareTokenValidatePassword(token: String!, password: String): Boolean!
|
||||||
|
|
||||||
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
|
search(query: String!, limitPhotos: Int, limitAlbums: Int): SearchResult!
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ func CORSMiddleware(devMode bool) mux.MiddlewareFunc {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
methods := []string{http.MethodGet, http.MethodPost, http.MethodOptions}
|
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-Methods", strings.Join(methods, ","))
|
||||||
w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
|
w.Header().Set("Access-Control-Allow-Headers", strings.Join(headers, ","))
|
||||||
|
|
|
@ -1,13 +1,23 @@
|
||||||
import React from 'react'
|
import React, { useState } from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
|
import styled from 'styled-components'
|
||||||
import RouterProps from 'react-router-prop-types'
|
import RouterProps from 'react-router-prop-types'
|
||||||
import { Route, Switch } from 'react-router-dom'
|
import { Route, Switch } from 'react-router-dom'
|
||||||
import AlbumSharePage from './AlbumSharePage'
|
import AlbumSharePage from './AlbumSharePage'
|
||||||
import PhotoSharePage from './PhotoSharePage'
|
import PhotoSharePage from './PhotoSharePage'
|
||||||
import { useQuery } from 'react-apollo'
|
import { useQuery } from 'react-apollo'
|
||||||
import gql from 'graphql-tag'
|
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) {
|
query SharePageToken($token: String!, $password: String) {
|
||||||
shareToken(token: $token, password: $password) {
|
shareToken(token: $token, password: $password) {
|
||||||
token
|
token
|
||||||
|
@ -71,15 +81,20 @@ const tokenQuery = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const tokenPasswordProtectedQuery = gql`
|
const validateTokenPasswordQuery = gql`
|
||||||
query ShareTokenRequiresPassword($token: String!) {
|
query ShareTokenValidatePassword($token: String!, $password: String) {
|
||||||
shareTokenRequiresPassword(token: $token)
|
shareTokenValidatePassword(token: $token, password: $password)
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const AuthorizedTokenRoute = ({ match, password }) => {
|
const AuthorizedTokenRoute = ({ match }) => {
|
||||||
const { loading, error, data } = useQuery(tokenQuery, {
|
const token = match.params.token
|
||||||
variables: { token: match.params.token, password },
|
|
||||||
|
const { loading, error, data } = useQuery(shareTokenQuery, {
|
||||||
|
variables: {
|
||||||
|
token,
|
||||||
|
password: sessionStorage.getItem(`share-token-pw-${token}`),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (error) return error.message
|
if (error) return error.message
|
||||||
|
@ -98,20 +113,106 @@ const AuthorizedTokenRoute = ({ match, password }) => {
|
||||||
|
|
||||||
AuthorizedTokenRoute.propTypes = {
|
AuthorizedTokenRoute.propTypes = {
|
||||||
match: PropTypes.object.isRequired,
|
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 = (
|
||||||
|
<Message negative>
|
||||||
|
<Message.Content>Wrong password, please try again.</Message.Content>
|
||||||
|
</Message>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MessageContainer>
|
||||||
|
<Header as="h1" style={{ fontWeight: 400 }}>
|
||||||
|
Protected share
|
||||||
|
</Header>
|
||||||
|
<p>This share is protected with a password.</p>
|
||||||
|
<Form>
|
||||||
|
<Form.Field>
|
||||||
|
<label>Password</label>
|
||||||
|
<Input
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading}
|
||||||
|
onKeyUp={event => event.key == 'Enter' && onSubmit()}
|
||||||
|
onChange={e => setPasswordValue(e.target.value)}
|
||||||
|
placeholder="Password"
|
||||||
|
type="password"
|
||||||
|
icon={<Icon onClick={onSubmit} link name="arrow right" />}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
{errorMessage}
|
||||||
|
</Form>
|
||||||
|
</MessageContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ProtectedTokenEnterPassword.propTypes = {
|
||||||
|
match: PropTypes.object.isRequired,
|
||||||
|
refetchWithPassword: PropTypes.func.isRequired,
|
||||||
|
loading: PropTypes.bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TokenRoute = ({ match }) => {
|
const TokenRoute = ({ match }) => {
|
||||||
const { loading, error, data } = useQuery(tokenPasswordProtectedQuery, {
|
const token = match.params.token
|
||||||
variables: { token: match.params.token },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) return error.message
|
const { loading, error, data, refetch } = useQuery(
|
||||||
if (loading) return 'Loading...'
|
validateTokenPasswordQuery,
|
||||||
|
{
|
||||||
if (data.shareTokenRequiresPassword == true) {
|
variables: {
|
||||||
return 'Please provide password'
|
token: match.params.token,
|
||||||
|
password: sessionStorage.getItem(`share-token-pw-${token}`),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error.message == 'GraphQL error: share not found') {
|
||||||
|
return (
|
||||||
|
<MessageContainer>
|
||||||
|
<h1>Share not found</h1>
|
||||||
|
<p>Maybe the share has expired or has been deleted.</p>
|
||||||
|
</MessageContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.message
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.shareTokenValidatePassword == false) {
|
||||||
|
return (
|
||||||
|
<ProtectedTokenEnterPassword
|
||||||
|
match={match}
|
||||||
|
refetchWithPassword={password => {
|
||||||
|
sessionStorage.setItem(`share-token-pw-${token}`, password)
|
||||||
|
refetch({ variables: { password: password } })
|
||||||
|
}}
|
||||||
|
loading={loading}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading) return 'Loading...'
|
||||||
|
|
||||||
return <AuthorizedTokenRoute match={match} />
|
return <AuthorizedTokenRoute match={match} />
|
||||||
}
|
}
|
||||||
|
@ -126,7 +227,7 @@ const SharePage = ({ match }) => {
|
||||||
<Route path={`${match.url}/:token`}>
|
<Route path={`${match.url}/:token`}>
|
||||||
{({ match }) => <TokenRoute match={match} />}
|
{({ match }) => <TokenRoute match={match} />}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/">Share not found</Route>
|
<Route path="/">Route not found</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue