52
src/components/Auth.jsx
Normal file
52
src/components/Auth.jsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {Redirect} from "react-router-dom";
|
||||||
|
import queryString from "query-string";
|
||||||
|
|
||||||
|
import {auth, logout} from "../services/auth_service";
|
||||||
|
import {useStateValue} from "../services/State";
|
||||||
|
|
||||||
|
export const AuthLogin = (props) => {
|
||||||
|
const [context, dispatch] = useStateValue();
|
||||||
|
|
||||||
|
if (context.user.auth) {
|
||||||
|
// El usuario ya esta logeado, no hay nada que hacer
|
||||||
|
props.history.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = queryString.parse(props.location.search);
|
||||||
|
auth(params).then((response) => {
|
||||||
|
console.debug(response, window.localStorage);
|
||||||
|
|
||||||
|
if(response.status === 'redirect_to_code'){
|
||||||
|
// Se va o ya se redirigió hacia la pagina que obtiene el código, no hay nada que hacer.
|
||||||
|
return null;
|
||||||
|
} else if (response.status === 'code_error') {
|
||||||
|
// Hubo un error al obtener el código
|
||||||
|
props.history.push('/error')
|
||||||
|
} else if (response.status === 'done') {
|
||||||
|
// Termino gud, refresh_token y expires debería estar en local storage y access_token
|
||||||
|
// debió ser retornado con la response
|
||||||
|
props.history.push('/')
|
||||||
|
dispatch({type: 'login', user:{auth: true, access_token: response.access_token}});
|
||||||
|
} else {
|
||||||
|
// Ocurrió un error inesperado al hacer auth :c
|
||||||
|
props.history.push('/error')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthLogout = (props) => {
|
||||||
|
const [context, dispatch] = useStateValue();
|
||||||
|
if (!context.user.auth) {
|
||||||
|
return <Redirect to='/'/>
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(context.user.access_token).then(result => {
|
||||||
|
console.log(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch({type: 'logout', user: {auth: false}})
|
||||||
|
return <Redirect to='/'/>
|
||||||
|
}
|
||||||
@@ -1,13 +1,41 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import './Nav.scss';
|
import './Nav.scss';
|
||||||
|
import {useStateValue} from '../services/State'
|
||||||
|
|
||||||
export const Nav = () => (
|
export const Nav = (props) => {
|
||||||
<nav className='nav'>
|
const context = useStateValue()[0];
|
||||||
<Link to='/'><h1 className='branding'>MusicList</h1></Link>
|
|
||||||
<ul className='nav-links'>
|
const showLogin = () => {
|
||||||
<li className='link'><Link to='/login'>Iniciar Sesion</Link></li>
|
return context.user.auth === false;
|
||||||
<li className='link'><Link to='/signup'>Registrate</Link></li>
|
}
|
||||||
</ul>
|
|
||||||
</nav>
|
const buttons = () => {
|
||||||
)
|
if (showLogin()) {
|
||||||
|
return <ul className='nav-links'>
|
||||||
|
<li className='link'>
|
||||||
|
<Link to='/login'>Iniciar Sesión</Link>
|
||||||
|
</li>
|
||||||
|
<li className='link'>
|
||||||
|
<Link to='/signup'>Registrate</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}else {
|
||||||
|
return <ul className='nav-links'>
|
||||||
|
<li>
|
||||||
|
Bienvenido {context.user.access_token}
|
||||||
|
</li>
|
||||||
|
<li className='link'>
|
||||||
|
<Link to='/logout'>Cerrar Sesión</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className='nav'>
|
||||||
|
<Link to='/'><h1 className='branding'>MusicList</h1></Link>
|
||||||
|
{buttons()}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import './styles/reset.css';
|
|||||||
import './styles/main.scss';
|
import './styles/main.scss';
|
||||||
import './styles/tabs.scss';
|
import './styles/tabs.scss';
|
||||||
|
|
||||||
|
import {StateProvider} from "./services/State";
|
||||||
|
|
||||||
import {Nav} from "./components/Nav";
|
import {Nav} from "./components/Nav";
|
||||||
import {SearchBar} from "./components/SearchBar";
|
import {SearchBar} from "./components/SearchBar";
|
||||||
import {ScrollToTopRouter} from "./components/ScrollToTop";
|
import {ScrollToTopRouter} from "./components/ScrollToTop";
|
||||||
@@ -17,27 +19,28 @@ import {ReleaseView} from "./views/Release";
|
|||||||
import {Recomended} from "./views/Recomended";
|
import {Recomended} from "./views/Recomended";
|
||||||
import {SongView} from "./views/Song";
|
import {SongView} from "./views/Song";
|
||||||
import {Grid, RowCol} from './components/Grid';
|
import {Grid, RowCol} from './components/Grid';
|
||||||
|
import {AuthLogin, AuthLogout} from "./components/Auth";
|
||||||
|
|
||||||
const Main = (props) => {
|
const Main = (props) => {
|
||||||
const navigate = (query) => props.history.push(`/search?query=${query}`);
|
const navigate = (query) => props.history.push(`/search?query=${query}`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid>
|
<Grid>
|
||||||
<RowCol><h1>Busca la musica que te gusta!</h1></RowCol>
|
<RowCol><h1>Busca la musica que te gusta!</h1></RowCol>
|
||||||
<RowCol><SearchBar history={props.history} onQueryChanged={navigate}/></RowCol>
|
<RowCol><SearchBar history={props.history} onQueryChanged={navigate}/></RowCol>
|
||||||
<RowCol><Recomended/></RowCol>
|
<RowCol><Recomended/></RowCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoRoute = (props) => {
|
const NoRoute = (props) => {
|
||||||
return (
|
return (
|
||||||
<Grid>
|
<Grid>
|
||||||
<RowCol><h1>Esa pagina no existe</h1></RowCol>
|
<RowCol><h1>Esa pagina no existe</h1></RowCol>
|
||||||
<RowCol>
|
<RowCol>
|
||||||
<button className='link' onClick={() => props.history.goBack()}>Volver</button>
|
<button className='link' onClick={() => props.history.goBack()}>Volver</button>
|
||||||
</RowCol>
|
</RowCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,10 +51,15 @@ const App = () => (
|
|||||||
<Nav/>
|
<Nav/>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path='/search/:who?' component={Search}/>
|
<Route path='/search/:who?' component={Search}/>
|
||||||
|
|
||||||
<Route path='/artist/:mbid?' component={ArtistView}/>
|
<Route path='/artist/:mbid?' component={ArtistView}/>
|
||||||
<Route path='/disc/:mbid?' component={DiscView}/>
|
<Route path='/disc/:mbid?' component={DiscView}/>
|
||||||
<Route path='/release/:mbid?' component={ReleaseView}/>
|
<Route path='/release/:mbid?' component={ReleaseView}/>
|
||||||
<Route path='/song/:mbid?' component={SongView}/>
|
<Route path='/song/:mbid?' component={SongView}/>
|
||||||
|
|
||||||
|
<Route exact path='/login' component={AuthLogin}/>
|
||||||
|
<Route exact path='/logout' component={AuthLogout}/>
|
||||||
|
|
||||||
<Route exact path='/' component={Main}/>
|
<Route exact path='/' component={Main}/>
|
||||||
<Route path='*' component={NoRoute}/>
|
<Route path='*' component={NoRoute}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -59,7 +67,15 @@ const App = () => (
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const AppWithState = () => {
|
||||||
|
return (
|
||||||
|
<StateProvider>
|
||||||
|
<App/>
|
||||||
|
</StateProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<App/>,
|
<AppWithState/>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
|
|||||||
35
src/services/State.jsx
Normal file
35
src/services/State.jsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React, {createContext, useContext, useReducer} from 'react';
|
||||||
|
|
||||||
|
const initialState = {user: {auth: false}};
|
||||||
|
|
||||||
|
const reducer = (state, action) => {
|
||||||
|
console.log(state, action);
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
case 'login':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: action.user
|
||||||
|
}
|
||||||
|
case 'logout':
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
user: action.user
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StateContext = createContext(null);
|
||||||
|
|
||||||
|
export const StateProvider = ({children}) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StateContext.Provider value={useReducer(reducer, initialState)}>
|
||||||
|
{children}
|
||||||
|
</StateContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useStateValue = () => useContext(StateContext);
|
||||||
113
src/services/auth_service.js
Normal file
113
src/services/auth_service.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
const current_host = `${window.location.protocol}//${window.location.host}`
|
||||||
|
const oauth_url = `${process.env.REACT_APP_API_SERVER}/oauth`;
|
||||||
|
const client_id = process.env["REACT_APP_CLIENT_ID"];
|
||||||
|
|
||||||
|
const generate_challenge = () => {
|
||||||
|
return {
|
||||||
|
code: '5d2309e5bb73b864f989753887fe52f79ce5270395e25862da6940d5',
|
||||||
|
challenge: 'MChCW5vD-3h03HMGFZYskOSTir7II_MMTb8a9rJNhnI',
|
||||||
|
method: 'S256',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const redirect_to_code = () => {
|
||||||
|
const challenge = generate_challenge()
|
||||||
|
const params = {
|
||||||
|
response_type: 'code',
|
||||||
|
client_id: client_id,
|
||||||
|
redirect_uri: `${current_host}/login`,
|
||||||
|
scope: 'read write',
|
||||||
|
code_challenge: challenge.challenge,
|
||||||
|
code_challenge_method: challenge.method
|
||||||
|
};
|
||||||
|
const url = `${oauth_url}/authorize/?${new URLSearchParams(params).toString()}`;
|
||||||
|
|
||||||
|
window.localStorage.setItem('code_verifier', challenge.code);
|
||||||
|
window.location.href = url;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const get_auth = async (code, code_verifier) => {
|
||||||
|
const params = {
|
||||||
|
code: code,
|
||||||
|
code_verifier: code_verifier,
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
redirect_uri: `${current_host}/login`,
|
||||||
|
client_id: client_id,
|
||||||
|
};
|
||||||
|
const url = `${oauth_url}/token/`
|
||||||
|
const response = await axios.post(url, new URLSearchParams(params));
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auth = async (params) => {
|
||||||
|
// Primera fase, obtener código
|
||||||
|
// Si es que no se tiene se tiene que redirigir a la pagina de oauth que entregara el código
|
||||||
|
// a la pagina de redirect como parámetro GET, este trabajo lo hace redirect y se volverá a
|
||||||
|
// ejecutar este método cuando se entre a esa ruta
|
||||||
|
if (!window.localStorage.getItem('code_verifier')) {
|
||||||
|
redirect_to_code();
|
||||||
|
return {status: 'redirect_to_code'};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Segunda fase, se llama a auth con los parámetros de la ruta.
|
||||||
|
// Estos parámetros puede contener un error, en ese caso se elimina el code_verifier del storage
|
||||||
|
// porque no sera util y fallo la request.
|
||||||
|
if (params.error) {
|
||||||
|
window.localStorage.clear()
|
||||||
|
return {status: 'code_error', value: params.error};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teniendo el código en los parámetros se intenta obtener el código de autorización.
|
||||||
|
const code = params.code;
|
||||||
|
const code_verifier = window.localStorage.getItem('code_verifier');
|
||||||
|
const response = await get_auth(code, code_verifier)
|
||||||
|
|
||||||
|
// Puede que la respuesta sea erronea por varias razones
|
||||||
|
if (response.error) {
|
||||||
|
return {status: response.error}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Una vez se tiene la respuesta se almacena el refresh_token y el expires_in
|
||||||
|
// en localstorage para ser utilizado mas tarde para renovar el access_token
|
||||||
|
window.localStorage.clear()
|
||||||
|
|
||||||
|
const refresh = response.refresh_token;
|
||||||
|
const expires = new Date(new Date().getTime() + ((response.expires_in) * 1000))
|
||||||
|
|
||||||
|
window.localStorage.setItem('refresh_token', refresh);
|
||||||
|
window.localStorage.setItem('expires', expires);
|
||||||
|
|
||||||
|
// Finalmente se retorna el access_token para ser utilizado en el estado de la app
|
||||||
|
return {status: 'done', access_token: response.access_token};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logout = async (access_token) => {
|
||||||
|
// Para hacer logout de un usuario es necesario eliminar el refresh token de su localStorage
|
||||||
|
// y se llama a revocar los tokens en el servidor oauth
|
||||||
|
const revoke_access_params = {
|
||||||
|
token: access_token,
|
||||||
|
client_id: client_id,
|
||||||
|
token_type_hint: 'access_token'
|
||||||
|
}
|
||||||
|
const revoke_refresh_params = {
|
||||||
|
token: window.localStorage.getItem('refresh_token'),
|
||||||
|
client_id: client_id,
|
||||||
|
token_type_hint: 'refresh_token'
|
||||||
|
}
|
||||||
|
|
||||||
|
window.localStorage.clear();
|
||||||
|
|
||||||
|
const url = `${oauth_url}/revoke_token/`;
|
||||||
|
const response_access_revoke = await axios.post(url, new URLSearchParams(revoke_access_params));
|
||||||
|
const response_refresh_revoke = await axios.post(url, new URLSearchParams(revoke_refresh_params));
|
||||||
|
|
||||||
|
return {
|
||||||
|
access_revoke: response_access_revoke,
|
||||||
|
refresh_revoke: response_refresh_revoke,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user