1
Fork 0

Use cookie based auth for shares with password

This commit is contained in:
viktorstrate 2020-07-13 17:51:53 +02:00
parent f669812efb
commit 21f66b9e62
11 changed files with 75 additions and 83 deletions

View File

@ -2,6 +2,7 @@ package routes
import (
"database/sql"
"fmt"
"net/http"
"github.com/viktorstrate/photoview/api/graphql/auth"
@ -39,7 +40,12 @@ func authenticateMedia(media *models.Media, db *sql.DB, r *http.Request) (succes
// Validate share token password, if set
if shareToken.Password != nil {
tokenPassword := r.Header.Get("TokenPassword")
tokenPasswordCookie, err := r.Cookie(fmt.Sprintf("share-token-pw-%s", shareToken.Value))
if err != nil {
return false, "unauthorized", http.StatusForbidden, nil
}
// tokenPassword := r.Header.Get("TokenPassword")
tokenPassword := tokenPasswordCookie.Value
if err := bcrypt.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {

View File

@ -37,7 +37,6 @@ func RegisterVideoRoutes(db *sql.DB, router *mux.Router) {
w.Write([]byte("internal server error"))
}
// TODO: Make sure user is authorized to access video
if success, response, status, err := authenticateMedia(media, db, r); !success {
if err != nil {
log.Printf("WARN: error authenticating video: %s\n", err)

View File

@ -2,7 +2,10 @@ import React, { useContext, useEffect } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import Layout from '../../Layout'
import ProtectedImage from '../../components/photoGallery/ProtectedImage'
import {
ProtectedImage,
ProtectedVideo,
} from '../../components/photoGallery/ProtectedMedia'
import { SidebarContext } from '../../components/sidebar/Sidebar'
import MediaSidebar from '../../components/sidebar/MediaSidebar'
@ -12,7 +15,7 @@ const DisplayPhoto = styled(ProtectedImage)`
object-fit: contain;
`
const DisplayVideo = styled.video`
const DisplayVideo = styled(ProtectedVideo)`
width: 100%;
max-height: calc(80vh);
`
@ -29,16 +32,7 @@ const MediaView = ({ media }) => {
}
if (media.type == 'video') {
return (
<DisplayVideo
controls
key={media.id}
crossorigin="use-credentials"
poster={media.thumbnail.url}
>
<source src={media.videoWeb.url} type="video/mp4" />
</DisplayVideo>
)
return <DisplayVideo media={media} />
}
throw new Error(`Unsupported media type: ${media.type}`)

View File

@ -16,6 +16,7 @@ import {
Icon,
Message,
} from 'semantic-ui-react'
import { saveSharePassword, getSharePassword } from '../../authentication'
const shareTokenQuery = gql`
query SharePageToken($token: String!, $password: String) {
@ -97,7 +98,7 @@ const AuthorizedTokenRoute = ({ match }) => {
const { loading, error, data } = useQuery(shareTokenQuery, {
variables: {
token,
password: sessionStorage.getItem(`share-token-pw-${token}`),
password: getSharePassword(token),
},
})
@ -185,7 +186,7 @@ const TokenRoute = ({ match }) => {
{
variables: {
token: match.params.token,
password: sessionStorage.getItem(`share-token-pw-${token}`),
password: getSharePassword(match.params.token),
},
}
)
@ -208,8 +209,8 @@ const TokenRoute = ({ match }) => {
<ProtectedTokenEnterPassword
match={match}
refetchWithPassword={password => {
sessionStorage.setItem(`share-token-pw-${token}`, password)
refetch({ variables: { password: password } })
saveSharePassword(token, password)
refetch({ variables: { password } })
}}
loading={loading}
/>

View File

@ -5,10 +5,21 @@ export function saveTokenCookie(token) {
}
export function clearTokenCookie() {
document.cookie = 'auth-token= ;max-age=0'
document.cookie = 'auth-token= ;max-age=0 ;path=/ ;sameSite=Lax'
}
export function authToken() {
const match = document.cookie.match(/auth-token=([\d\w]+)/)
return match && match[1]
}
export function saveSharePassword(shareToken, password) {
document.cookie = `share-token-pw-${shareToken}=${password} ;path=/ ;sameSite=Lax`
}
export function getSharePassword(shareToken) {
const match = document.cookie.match(
`share-token-pw-${shareToken}=([\\d\\w]+)`
)
return match && match[1]
}

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import { Link } from 'react-router-dom'
import ProtectedImage from '../photoGallery/ProtectedImage'
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
const AlbumBoxLink = styled(Link)`
width: 240px;

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
import { useLazyQuery } from '@apollo/react-hooks'
import gql from 'graphql-tag'
import debounce from '../../debounce'
import ProtectedImage from '../photoGallery/ProtectedImage'
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
import { NavLink } from 'react-router-dom'
const Container = styled.div`

View File

@ -5,7 +5,7 @@ import PropTypes from 'prop-types'
import styled from 'styled-components'
import LazyLoad from 'react-lazyload'
import { Icon } from 'semantic-ui-react'
import ProtectedImage from './ProtectedImage'
import { ProtectedImage } from './ProtectedMedia'
const markFavoriteMutation = gql`
mutation markMediaFavorite($mediaId: Int!, $favorite: Boolean!) {

View File

@ -1,43 +1,26 @@
import React, { useEffect, useState } from 'react'
import React from 'react'
import PropTypes from 'prop-types'
// let imageCache = {}
const getProtectedUrl = url => {
const imgUrl = new URL(url)
// export async function fetchProtectedImage(
// src,
// { signal, headers: customHeaders } = { signal: null, headers: null }
// ) {
// if (src) {
// if (imageCache[src]) {
// return imageCache[src]
// }
if (localStorage.getItem('token') == null) {
// Get share token if not authorized
// let headers = {
// ...customHeaders,
// }
// if (localStorage.getItem('token')) {
// headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
// }
const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
if (tokenRegex) {
const token = tokenRegex[1]
imgUrl.searchParams.set('token', token)
}
}
// let image = await fetch(src, {
// headers,
// signal,
// })
// image = await image.blob()
// const url = URL.createObjectURL(image)
// // eslint-disable-next-line require-atomic-updates
// imageCache[src] = url
// return url
// }
// }
return imgUrl.href
}
/**
* An image that needs a authorization header to load
* An image that needs authorization to load
*/
const ProtectedImage = ({ src, ...props }) => {
export const ProtectedImage = ({ src, ...props }) => {
// const [imgSrc, setImgSrc] = useState(null)
// useEffect(() => {
@ -85,11 +68,29 @@ const ProtectedImage = ({ src, ...props }) => {
// }
// }, [src])
return <img {...props} src={src} crossOrigin="use-credentials" />
return (
<img {...props} src={getProtectedUrl(src)} crossOrigin="use-credentials" />
)
}
ProtectedImage.propTypes = {
src: PropTypes.string,
src: PropTypes.string.isRequired,
}
export default ProtectedImage
export const ProtectedVideo = ({ media, ...props }) => {
return (
<video
{...props}
controls
key={media.id}
crossOrigin="use-credentials"
poster={getProtectedUrl(media.thumbnail.url)}
>
<source src={getProtectedUrl(media.videoWeb.url)} type="video/mp4" />
</video>
)
}
ProtectedVideo.propTypes = {
media: PropTypes.object.isRequired,
}

View File

@ -1,7 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import styled from 'styled-components'
import ProtectedImage from '../ProtectedImage'
import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia'
const StyledPhoto = styled(ProtectedImage)`
position: absolute;
@ -13,7 +13,7 @@ const StyledPhoto = styled(ProtectedImage)`
object-position: center;
`
const StyledVideo = styled.video`
const StyledVideo = styled(ProtectedVideo)`
position: absolute;
top: 0;
left: 0;
@ -39,18 +39,7 @@ const PresentMedia = ({ media, imageLoaded, ...otherProps }) => {
}
if (media.type == 'video') {
return (
<div {...otherProps}>
<StyledVideo
controls
key={media.id}
crossorigin="use-credentials"
poster={media.thumbnail.url}
>
<source src={media.videoWeb.url} type="video/mp4" />
</StyledVideo>
</div>
)
return <StyledVideo media={media} />
}
throw new Error(`Unknown media type '${media.type}'`)

View File

@ -4,7 +4,7 @@ import styled from 'styled-components'
import { useLazyQuery } from 'react-apollo'
import gql from 'graphql-tag'
import SidebarItem from './SidebarItem'
import ProtectedImage from '../photoGallery/ProtectedImage'
import { ProtectedImage, ProtectedVideo } from '../photoGallery/ProtectedMedia'
import SidebarShare from './Sharing'
import SidebarDownload from './SidebarDownload'
import { authToken } from '../../authentication'
@ -73,7 +73,7 @@ const PreviewImage = styled(ProtectedImage)`
object-fit: contain;
`
const PreviewVideo = styled.video`
const PreviewVideo = styled(ProtectedVideo)`
position: absolute;
width: 100%;
height: 100%;
@ -87,16 +87,7 @@ const PreviewMedia = ({ media, previewImage }) => {
}
if (media.type == 'video') {
return (
<PreviewVideo
controls
key={media.id}
crossorigin="use-credentials"
poster={media.thumbnail.url}
>
<source src={media.videoWeb.url} type="video/mp4" />
</PreviewVideo>
)
return <PreviewVideo media={media} />
}
throw new Error('Unknown media type')