Use cookie based auth for shares with password
This commit is contained in:
parent
f669812efb
commit
21f66b9e62
|
@ -2,6 +2,7 @@ package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/viktorstrate/photoview/api/graphql/auth"
|
"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
|
// Validate share token password, if set
|
||||||
if shareToken.Password != nil {
|
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.CompareHashAndPassword([]byte(*shareToken.Password), []byte(tokenPassword)); err != nil {
|
||||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||||
|
|
|
@ -37,7 +37,6 @@ func RegisterVideoRoutes(db *sql.DB, router *mux.Router) {
|
||||||
w.Write([]byte("internal server error"))
|
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 success, response, status, err := authenticateMedia(media, db, r); !success {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("WARN: error authenticating video: %s\n", err)
|
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 PropTypes from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Layout from '../../Layout'
|
import Layout from '../../Layout'
|
||||||
import ProtectedImage from '../../components/photoGallery/ProtectedImage'
|
import {
|
||||||
|
ProtectedImage,
|
||||||
|
ProtectedVideo,
|
||||||
|
} from '../../components/photoGallery/ProtectedMedia'
|
||||||
import { SidebarContext } from '../../components/sidebar/Sidebar'
|
import { SidebarContext } from '../../components/sidebar/Sidebar'
|
||||||
import MediaSidebar from '../../components/sidebar/MediaSidebar'
|
import MediaSidebar from '../../components/sidebar/MediaSidebar'
|
||||||
|
|
||||||
|
@ -12,7 +15,7 @@ const DisplayPhoto = styled(ProtectedImage)`
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
`
|
`
|
||||||
|
|
||||||
const DisplayVideo = styled.video`
|
const DisplayVideo = styled(ProtectedVideo)`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: calc(80vh);
|
max-height: calc(80vh);
|
||||||
`
|
`
|
||||||
|
@ -29,16 +32,7 @@ const MediaView = ({ media }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.type == 'video') {
|
if (media.type == 'video') {
|
||||||
return (
|
return <DisplayVideo media={media} />
|
||||||
<DisplayVideo
|
|
||||||
controls
|
|
||||||
key={media.id}
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
poster={media.thumbnail.url}
|
|
||||||
>
|
|
||||||
<source src={media.videoWeb.url} type="video/mp4" />
|
|
||||||
</DisplayVideo>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unsupported media type: ${media.type}`)
|
throw new Error(`Unsupported media type: ${media.type}`)
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
Icon,
|
Icon,
|
||||||
Message,
|
Message,
|
||||||
} from 'semantic-ui-react'
|
} from 'semantic-ui-react'
|
||||||
|
import { saveSharePassword, getSharePassword } from '../../authentication'
|
||||||
|
|
||||||
const shareTokenQuery = gql`
|
const shareTokenQuery = gql`
|
||||||
query SharePageToken($token: String!, $password: String) {
|
query SharePageToken($token: String!, $password: String) {
|
||||||
|
@ -97,7 +98,7 @@ const AuthorizedTokenRoute = ({ match }) => {
|
||||||
const { loading, error, data } = useQuery(shareTokenQuery, {
|
const { loading, error, data } = useQuery(shareTokenQuery, {
|
||||||
variables: {
|
variables: {
|
||||||
token,
|
token,
|
||||||
password: sessionStorage.getItem(`share-token-pw-${token}`),
|
password: getSharePassword(token),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -185,7 +186,7 @@ const TokenRoute = ({ match }) => {
|
||||||
{
|
{
|
||||||
variables: {
|
variables: {
|
||||||
token: match.params.token,
|
token: match.params.token,
|
||||||
password: sessionStorage.getItem(`share-token-pw-${token}`),
|
password: getSharePassword(match.params.token),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -208,8 +209,8 @@ const TokenRoute = ({ match }) => {
|
||||||
<ProtectedTokenEnterPassword
|
<ProtectedTokenEnterPassword
|
||||||
match={match}
|
match={match}
|
||||||
refetchWithPassword={password => {
|
refetchWithPassword={password => {
|
||||||
sessionStorage.setItem(`share-token-pw-${token}`, password)
|
saveSharePassword(token, password)
|
||||||
refetch({ variables: { password: password } })
|
refetch({ variables: { password } })
|
||||||
}}
|
}}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,10 +5,21 @@ export function saveTokenCookie(token) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearTokenCookie() {
|
export function clearTokenCookie() {
|
||||||
document.cookie = 'auth-token= ;max-age=0'
|
document.cookie = 'auth-token= ;max-age=0 ;path=/ ;sameSite=Lax'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function authToken() {
|
export function authToken() {
|
||||||
const match = document.cookie.match(/auth-token=([\d\w]+)/)
|
const match = document.cookie.match(/auth-token=([\d\w]+)/)
|
||||||
return match && match[1]
|
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 PropTypes from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import ProtectedImage from '../photoGallery/ProtectedImage'
|
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
||||||
|
|
||||||
const AlbumBoxLink = styled(Link)`
|
const AlbumBoxLink = styled(Link)`
|
||||||
width: 240px;
|
width: 240px;
|
||||||
|
|
|
@ -4,7 +4,7 @@ import styled from 'styled-components'
|
||||||
import { useLazyQuery } from '@apollo/react-hooks'
|
import { useLazyQuery } from '@apollo/react-hooks'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import debounce from '../../debounce'
|
import debounce from '../../debounce'
|
||||||
import ProtectedImage from '../photoGallery/ProtectedImage'
|
import { ProtectedImage } from '../photoGallery/ProtectedMedia'
|
||||||
import { NavLink } from 'react-router-dom'
|
import { NavLink } from 'react-router-dom'
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
|
|
@ -5,7 +5,7 @@ import PropTypes from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import LazyLoad from 'react-lazyload'
|
import LazyLoad from 'react-lazyload'
|
||||||
import { Icon } from 'semantic-ui-react'
|
import { Icon } from 'semantic-ui-react'
|
||||||
import ProtectedImage from './ProtectedImage'
|
import { ProtectedImage } from './ProtectedMedia'
|
||||||
|
|
||||||
const markFavoriteMutation = gql`
|
const markFavoriteMutation = gql`
|
||||||
mutation markMediaFavorite($mediaId: Int!, $favorite: Boolean!) {
|
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'
|
import PropTypes from 'prop-types'
|
||||||
|
|
||||||
// let imageCache = {}
|
const getProtectedUrl = url => {
|
||||||
|
const imgUrl = new URL(url)
|
||||||
|
|
||||||
// export async function fetchProtectedImage(
|
if (localStorage.getItem('token') == null) {
|
||||||
// src,
|
// Get share token if not authorized
|
||||||
// { signal, headers: customHeaders } = { signal: null, headers: null }
|
|
||||||
// ) {
|
|
||||||
// if (src) {
|
|
||||||
// if (imageCache[src]) {
|
|
||||||
// return imageCache[src]
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let headers = {
|
const tokenRegex = location.pathname.match(/^\/share\/([\d\w]+)(\/?.*)$/)
|
||||||
// ...customHeaders,
|
if (tokenRegex) {
|
||||||
// }
|
const token = tokenRegex[1]
|
||||||
// if (localStorage.getItem('token')) {
|
imgUrl.searchParams.set('token', token)
|
||||||
// headers['Authorization'] = `Bearer ${localStorage.getItem('token')}`
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
// let image = await fetch(src, {
|
return imgUrl.href
|
||||||
// headers,
|
}
|
||||||
// signal,
|
|
||||||
// })
|
|
||||||
|
|
||||||
// image = await image.blob()
|
|
||||||
// const url = URL.createObjectURL(image)
|
|
||||||
|
|
||||||
// // eslint-disable-next-line require-atomic-updates
|
|
||||||
// imageCache[src] = url
|
|
||||||
|
|
||||||
// return url
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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)
|
// const [imgSrc, setImgSrc] = useState(null)
|
||||||
|
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
|
@ -85,11 +68,29 @@ const ProtectedImage = ({ src, ...props }) => {
|
||||||
// }
|
// }
|
||||||
// }, [src])
|
// }, [src])
|
||||||
|
|
||||||
return <img {...props} src={src} crossOrigin="use-credentials" />
|
return (
|
||||||
|
<img {...props} src={getProtectedUrl(src)} crossOrigin="use-credentials" />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtectedImage.propTypes = {
|
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 React from 'react'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import ProtectedImage from '../ProtectedImage'
|
import { ProtectedImage, ProtectedVideo } from '../ProtectedMedia'
|
||||||
|
|
||||||
const StyledPhoto = styled(ProtectedImage)`
|
const StyledPhoto = styled(ProtectedImage)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -13,7 +13,7 @@ const StyledPhoto = styled(ProtectedImage)`
|
||||||
object-position: center;
|
object-position: center;
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledVideo = styled.video`
|
const StyledVideo = styled(ProtectedVideo)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -39,18 +39,7 @@ const PresentMedia = ({ media, imageLoaded, ...otherProps }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.type == 'video') {
|
if (media.type == 'video') {
|
||||||
return (
|
return <StyledVideo media={media} />
|
||||||
<div {...otherProps}>
|
|
||||||
<StyledVideo
|
|
||||||
controls
|
|
||||||
key={media.id}
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
poster={media.thumbnail.url}
|
|
||||||
>
|
|
||||||
<source src={media.videoWeb.url} type="video/mp4" />
|
|
||||||
</StyledVideo>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Unknown media type '${media.type}'`)
|
throw new Error(`Unknown media type '${media.type}'`)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import styled from 'styled-components'
|
||||||
import { useLazyQuery } from 'react-apollo'
|
import { useLazyQuery } from 'react-apollo'
|
||||||
import gql from 'graphql-tag'
|
import gql from 'graphql-tag'
|
||||||
import SidebarItem from './SidebarItem'
|
import SidebarItem from './SidebarItem'
|
||||||
import ProtectedImage from '../photoGallery/ProtectedImage'
|
import { ProtectedImage, ProtectedVideo } from '../photoGallery/ProtectedMedia'
|
||||||
import SidebarShare from './Sharing'
|
import SidebarShare from './Sharing'
|
||||||
import SidebarDownload from './SidebarDownload'
|
import SidebarDownload from './SidebarDownload'
|
||||||
import { authToken } from '../../authentication'
|
import { authToken } from '../../authentication'
|
||||||
|
@ -73,7 +73,7 @@ const PreviewImage = styled(ProtectedImage)`
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
`
|
`
|
||||||
|
|
||||||
const PreviewVideo = styled.video`
|
const PreviewVideo = styled(ProtectedVideo)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -87,16 +87,7 @@ const PreviewMedia = ({ media, previewImage }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (media.type == 'video') {
|
if (media.type == 'video') {
|
||||||
return (
|
return <PreviewVideo media={media} />
|
||||||
<PreviewVideo
|
|
||||||
controls
|
|
||||||
key={media.id}
|
|
||||||
crossorigin="use-credentials"
|
|
||||||
poster={media.thumbnail.url}
|
|
||||||
>
|
|
||||||
<source src={media.videoWeb.url} type="video/mp4" />
|
|
||||||
</PreviewVideo>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Unknown media type')
|
throw new Error('Unknown media type')
|
||||||
|
|
Loading…
Reference in New Issue