Use cookie based auth for shares with password
This commit is contained in:
parent
f669812efb
commit
21f66b9e62
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`)
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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`
|
||||
|
|
|
@ -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!) {
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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}'`)
|
||||
|
|
|
@ -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')
|
||||
|
|
Loading…
Reference in New Issue