diff --git a/api/routes/authenticate_media.go b/api/routes/authenticate_media.go index 7b707c9e..0659ca07 100644 --- a/api/routes/authenticate_media.go +++ b/api/routes/authenticate_media.go @@ -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 { diff --git a/api/routes/videos.go b/api/routes/videos.go index 240cfc1e..2cb27cda 100644 --- a/api/routes/videos.go +++ b/api/routes/videos.go @@ -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) diff --git a/ui/src/Pages/SharePage/MediaSharePage.js b/ui/src/Pages/SharePage/MediaSharePage.js index 197fb250..cad90b63 100644 --- a/ui/src/Pages/SharePage/MediaSharePage.js +++ b/ui/src/Pages/SharePage/MediaSharePage.js @@ -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 ( - - - - ) + return } throw new Error(`Unsupported media type: ${media.type}`) diff --git a/ui/src/Pages/SharePage/SharePage.js b/ui/src/Pages/SharePage/SharePage.js index ebd35409..b52f1046 100644 --- a/ui/src/Pages/SharePage/SharePage.js +++ b/ui/src/Pages/SharePage/SharePage.js @@ -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 }) => { { - sessionStorage.setItem(`share-token-pw-${token}`, password) - refetch({ variables: { password: password } }) + saveSharePassword(token, password) + refetch({ variables: { password } }) }} loading={loading} /> diff --git a/ui/src/authentication.js b/ui/src/authentication.js index bfa2c422..3a606430 100644 --- a/ui/src/authentication.js +++ b/ui/src/authentication.js @@ -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] +} diff --git a/ui/src/components/albumGallery/AlbumBox.js b/ui/src/components/albumGallery/AlbumBox.js index 561a6007..ce2b787d 100644 --- a/ui/src/components/albumGallery/AlbumBox.js +++ b/ui/src/components/albumGallery/AlbumBox.js @@ -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; diff --git a/ui/src/components/header/Searchbar.js b/ui/src/components/header/Searchbar.js index 3f1895da..ed55875b 100644 --- a/ui/src/components/header/Searchbar.js +++ b/ui/src/components/header/Searchbar.js @@ -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` diff --git a/ui/src/components/photoGallery/MediaThumbnail.js b/ui/src/components/photoGallery/MediaThumbnail.js index c127983e..db168b0c 100644 --- a/ui/src/components/photoGallery/MediaThumbnail.js +++ b/ui/src/components/photoGallery/MediaThumbnail.js @@ -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!) { diff --git a/ui/src/components/photoGallery/ProtectedImage.js b/ui/src/components/photoGallery/ProtectedMedia.js similarity index 56% rename from ui/src/components/photoGallery/ProtectedImage.js rename to ui/src/components/photoGallery/ProtectedMedia.js index a9194a8d..6d41e2a4 100644 --- a/ui/src/components/photoGallery/ProtectedImage.js +++ b/ui/src/components/photoGallery/ProtectedMedia.js @@ -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 + return ( + + ) } ProtectedImage.propTypes = { - src: PropTypes.string, + src: PropTypes.string.isRequired, } -export default ProtectedImage +export const ProtectedVideo = ({ media, ...props }) => { + return ( + + ) +} + +ProtectedVideo.propTypes = { + media: PropTypes.object.isRequired, +} diff --git a/ui/src/components/photoGallery/presentView/PresentMedia.js b/ui/src/components/photoGallery/presentView/PresentMedia.js index 4fa55e20..0001dfc1 100644 --- a/ui/src/components/photoGallery/presentView/PresentMedia.js +++ b/ui/src/components/photoGallery/presentView/PresentMedia.js @@ -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 ( -
- - - -
- ) + return } throw new Error(`Unknown media type '${media.type}'`) diff --git a/ui/src/components/sidebar/MediaSidebar.js b/ui/src/components/sidebar/MediaSidebar.js index da44bbeb..5fd1c647 100644 --- a/ui/src/components/sidebar/MediaSidebar.js +++ b/ui/src/components/sidebar/MediaSidebar.js @@ -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 ( - - - - ) + return } throw new Error('Unknown media type')