2019-04-07 23:25:39 +02:00
import React , { Component } from 'react' ;
2019-04-23 01:05:30 +02:00
import { withStyles , Paper , Typography , Button , TextField , Fade , Link , CircularProgress , Tooltip , Dialog , DialogTitle , DialogActions , DialogContent } from '@material-ui/core' ;
2019-04-07 23:25:39 +02:00
import { styles } from './WelcomePage.styles' ;
import Mastodon from 'megalodon' ;
import { SaveClientSession } from '../types/SessionData' ;
import { createHyperspaceApp } from '../utilities/login' ;
import { parseUrl } from 'query-string' ;
2019-04-08 00:31:18 +02:00
import { getConfig } from '../utilities/settings' ;
2019-04-09 22:53:00 +02:00
import axios from 'axios' ;
2019-04-23 19:06:31 +02:00
import { withSnackbar , withSnackbarProps } from 'notistack' ;
interface IWelcomeProps extends withSnackbarProps {
classes : any ;
}
2019-04-07 23:25:39 +02:00
interface IWelcomeState {
logoUrl? : string ;
backgroundUrl? : string ;
brandName? : string ;
registerBase? : string ;
federates? : boolean ;
wantsToLogin : boolean ;
user : string ;
userInputError : boolean ;
2019-04-09 22:53:00 +02:00
userInputErrorMessage : string ;
2019-04-07 23:25:39 +02:00
clientId? : string ;
clientSecret? : string ;
authUrl? : string ;
foundSavedLogin : boolean ;
authority : boolean ;
2019-04-12 20:26:27 +02:00
license? : string ;
repo? : string ;
2019-04-20 20:59:32 +02:00
defaultRedirectAddress : string ;
2019-04-23 01:05:30 +02:00
openAuthDialog : boolean ;
authCode : string ;
emergencyMode : boolean ;
2019-04-07 23:25:39 +02:00
}
2019-04-23 19:06:31 +02:00
class WelcomePage extends Component < IWelcomeProps , IWelcomeState > {
2019-04-07 23:25:39 +02:00
client : any ;
constructor ( props : any ) {
super ( props ) ;
this . state = {
wantsToLogin : false ,
user : "" ,
userInputError : false ,
foundSavedLogin : false ,
2019-04-09 22:53:00 +02:00
authority : false ,
2019-04-20 20:59:32 +02:00
userInputErrorMessage : '' ,
2019-04-23 01:05:30 +02:00
defaultRedirectAddress : '' ,
openAuthDialog : false ,
authCode : '' ,
emergencyMode : false
2019-04-07 23:25:39 +02:00
}
2019-04-08 00:31:18 +02:00
getConfig ( ) . then ( ( result : any ) = > {
2019-04-20 20:59:32 +02:00
if ( result . location === "dynamic" ) {
console . warn ( "Recirect URI is set to dyanmic, which may affect how sign-in works for some users. Careful!" ) ;
}
2019-04-07 23:25:39 +02:00
this . setState ( {
logoUrl : result.branding? result . branding . logo : "logo.png" ,
backgroundUrl : result.branding? result . branding . background : "background.png" ,
brandName : result.branding? result . branding . name : "Hyperspace" ,
registerBase : result.registration? result . registration . defaultInstance : "" ,
2019-04-12 20:26:27 +02:00
federates : result.federated? result . federated === "true" : true ,
license : result.license.url ,
2019-04-20 20:59:32 +02:00
repo : result.repository ,
defaultRedirectAddress : result.location != "dynamic" ? result . location : ` https:// ${ window . location . host } `
2019-04-07 23:25:39 +02:00
} ) ;
} ) . catch ( ( ) = > {
2019-04-20 20:59:32 +02:00
console . error ( 'config.json is missing. If you want to customize Hyperspace, please include config.json' ) ;
2019-04-07 23:25:39 +02:00
} )
}
2019-04-23 19:06:31 +02:00
updateUserInfo ( user : string ) {
this . setState ( { user } ) ;
}
updateAuthCode ( code : string ) {
this . setState ( { authCode : code } ) ;
}
toggleAuthDialog() {
this . setState ( { openAuthDialog : ! this . state . openAuthDialog } ) ;
}
readyForAuth() {
if ( localStorage . getItem ( 'baseurl' ) ) {
return true ;
} else {
return false ;
}
}
2019-04-07 23:25:39 +02:00
componentDidMount() {
if ( localStorage . getItem ( "login" ) ) {
2019-04-23 01:05:30 +02:00
this . getSavedSession ( ) ;
2019-04-07 23:25:39 +02:00
this . setState ( {
foundSavedLogin : true
} )
this . checkForToken ( ) ;
}
}
2019-04-23 19:06:31 +02:00
getSavedSession() {
let loginData = localStorage . getItem ( "login" ) ;
if ( loginData ) {
let session : SaveClientSession = JSON . parse ( loginData ) ;
this . setState ( {
clientId : session.clientId ,
clientSecret : session.clientSecret ,
authUrl : session.authUrl ,
emergencyMode : session.emergency
} )
2019-04-07 23:25:39 +02:00
}
}
2019-04-23 01:05:30 +02:00
startEmergencyLogin() {
if ( ! this . state . emergencyMode ) {
this . createEmergencyLogin ( ) ;
} ;
this . toggleAuthDialog ( ) ;
}
2019-04-23 19:06:31 +02:00
startRegistration() {
if ( this . state . registerBase ) {
return "https://" + this . state . registerBase + "/auth/sign_up" ;
} else {
return "https://joinmastodon.org/#getting-started" ;
}
}
2019-04-07 23:25:39 +02:00
getLoginUser ( user : string ) {
if ( user . includes ( "@" ) ) {
let newUser = user ;
this . setState ( { user : newUser } )
return "https://" + newUser . split ( "@" ) [ 1 ] ;
} else {
let newUser = ` ${ user } @ ${ this . state . registerBase ? this . state . registerBase : "mastodon.social" } ` ;
this . setState ( { user : newUser } ) ;
return "https://" + ( this . state . registerBase ? this . state . registerBase : "mastodon.social" ) ;
}
}
startLogin() {
2019-04-12 20:26:27 +02:00
let error = this . checkForErrors ( ) ;
if ( ! error ) {
2019-04-07 23:25:39 +02:00
const scopes = 'read write follow' ;
const baseurl = this . getLoginUser ( this . state . user ) ;
localStorage . setItem ( "baseurl" , baseurl ) ;
2019-04-20 20:59:32 +02:00
createHyperspaceApp ( scopes , baseurl , this . state . defaultRedirectAddress ) . then ( ( resp : any ) = > {
2019-04-07 23:25:39 +02:00
let saveSessionForCrashing : SaveClientSession = {
clientId : resp.clientId ,
clientSecret : resp.clientSecret ,
2019-04-23 01:05:30 +02:00
authUrl : resp.url ,
emergency : false
2019-04-07 23:25:39 +02:00
}
localStorage . setItem ( "login" , JSON . stringify ( saveSessionForCrashing ) ) ;
this . setState ( {
clientId : resp.clientId ,
clientSecret : resp.clientSecret ,
authUrl : resp.url ,
wantsToLogin : true
} )
} )
} else {
2019-04-12 20:26:27 +02:00
2019-04-07 23:25:39 +02:00
}
}
2019-04-23 01:05:30 +02:00
createEmergencyLogin() {
console . log ( "Creating an emergency login..." )
const scopes = "read write follow" ;
const baseurl = localStorage . getItem ( 'baseurl' ) || this . getLoginUser ( this . state . user ) ;
2019-04-23 22:32:57 +02:00
Mastodon . registerApp ( this . state . brandName ? this . state . brandName : "Hyperspace" , {
scopes : scopes
} , baseurl ) . then ( ( appData : any ) = > {
2019-04-23 01:05:30 +02:00
let saveSessionForCrashing : SaveClientSession = {
2019-04-23 22:32:57 +02:00
clientId : appData.clientId ,
clientSecret : appData.clientSecret ,
authUrl : appData.url ,
2019-04-23 01:05:30 +02:00
emergency : true
} ;
localStorage . setItem ( "login" , JSON . stringify ( saveSessionForCrashing ) ) ;
this . setState ( {
2019-04-23 22:32:57 +02:00
clientId : appData.clientId ,
clientSecret : appData.clientSecret ,
authUrl : appData.url
2019-04-23 01:05:30 +02:00
} ) ;
2019-04-23 22:32:57 +02:00
} ) ;
2019-04-23 01:05:30 +02:00
}
authorizeEmergencyLogin() {
window . location . href = ` /?code= ${ this . state . authCode } #/ ` ;
}
2019-04-07 23:25:39 +02:00
resumeLogin() {
let loginData = localStorage . getItem ( "login" ) ;
if ( loginData ) {
let session : SaveClientSession = JSON . parse ( loginData ) ;
this . setState ( {
clientId : session.clientId ,
clientSecret : session.clientSecret ,
authUrl : session.authUrl ,
2019-04-23 01:05:30 +02:00
emergencyMode : session.emergency ,
2019-04-07 23:25:39 +02:00
wantsToLogin : true
} )
}
}
2019-04-12 20:26:27 +02:00
checkForErrors ( ) : boolean {
2019-04-09 22:53:00 +02:00
let userInputError = false ;
let userInputErrorMessage = "" ;
if ( this . state . user === "" ) {
userInputError = true ;
2019-04-12 20:26:27 +02:00
userInputErrorMessage = "Username cannot be blank." ;
2019-04-09 22:53:00 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return true ;
2019-04-09 22:53:00 +02:00
} else {
if ( this . state . user . includes ( "@" ) ) {
let baseUrl = this . state . user . split ( "@" ) [ 1 ] ;
axios . get ( "https://" + baseUrl + "/api/v1/timelines/public" ) . catch ( ( err : Error ) = > {
let userInputError = true ;
2019-04-12 20:26:27 +02:00
let userInputErrorMessage = "Instance name is invalid." ;
2019-04-09 22:53:00 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return true ;
2019-04-09 22:53:00 +02:00
} )
} else {
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return false ;
2019-04-09 22:53:00 +02:00
}
2019-04-12 20:26:27 +02:00
return false ;
2019-04-09 22:53:00 +02:00
}
2019-04-07 23:25:39 +02:00
}
2019-04-23 19:06:31 +02:00
checkForToken() {
let location = window . location . href ;
if ( location . includes ( "?code=" ) ) {
let code = parseUrl ( location ) . query . code as string ;
this . setState ( { authority : true } ) ;
let loginData = localStorage . getItem ( "login" ) ;
if ( loginData ) {
let clientLoginSession : SaveClientSession = JSON . parse ( loginData ) ;
Mastodon . fetchAccessToken (
clientLoginSession . clientId ,
clientLoginSession . clientSecret ,
code ,
( localStorage . getItem ( "baseurl" ) as string ) ,
this . state . emergencyMode ?
undefined :
clientLoginSession . authUrl . includes ( "urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob" ) ?
undefined :
` https:// ${ window . location . host } ` ,
) . then ( ( tokenData : any ) = > {
localStorage . setItem ( "access_token" , tokenData . access_token ) ;
window . location . href = ` https:// ${ window . location . host } /#/ ` ;
} ) . catch ( ( err : Error ) = > {
this . props . enqueueSnackbar ( "Couldn't authorize Hyperspace: " + err . name , { variant : 'error' } ) ;
console . error ( err . message ) ;
} )
}
2019-04-07 23:25:39 +02:00
}
}
showLanding() {
const { classes } = this . props ;
return (
< div >
< Typography variant = "h5" > Sign in < / Typography >
< Typography > with your Mastodon account < / Typography >
< div className = { classes . middlePadding } / >
< TextField
variant = "outlined"
label = "Account name"
fullWidth
placeholder = "example@mastodon.host"
onChange = { ( event ) = > this . updateUserInfo ( event . target . value ) }
error = { this . state . userInputError }
onBlur = { ( ) = > this . checkForErrors ( ) }
> < / TextField >
2019-04-09 22:53:00 +02:00
{
this . state . userInputError ? < Typography color = "error" > { this . state . userInputErrorMessage } < / Typography > : null
}
2019-04-12 20:26:27 +02:00
< br / >
2019-04-07 23:25:39 +02:00
{
this . state . registerBase ? < Typography variant = "caption" > If you are from < b > { this . state . registerBase ? this . state . registerBase : "noinstance" } < / b > , sign in with your username . < / Typography > : null
}
< br / >
{
this . state . foundSavedLogin ?
< Typography >
Signing in from a previous session ? < Link onClick = { ( ) = > this . resumeLogin ( ) } > Continue login < / Link > .
< / Typography > : null
}
< div className = { classes . middlePadding } / >
< div style = { { display : "flex" } } >
2019-04-12 20:26:27 +02:00
< Tooltip title = "Create account on site" >
2019-04-09 22:58:45 +02:00
< Button
color = "primary"
href = { this . startRegistration ( ) }
target = "_blank"
rel = "noreferrer"
> Create account < / Button >
< / Tooltip >
2019-04-07 23:25:39 +02:00
< div className = { classes . flexGrow } / >
2019-04-12 20:26:27 +02:00
< Tooltip title = "Continue sign-in" >
< Button color = "primary" variant = "contained" onClick = { ( ) = > this . startLogin ( ) } > Next < / Button >
< / Tooltip >
2019-04-07 23:25:39 +02:00
< / div >
< / div >
) ;
}
showLoginAuth() {
const { classes } = this . props ;
return (
< div >
< Typography variant = "h5" > Howdy , { this . state . user ? this . state . user . split ( "@" ) [ 0 ] : "user" } < / Typography >
< Typography > To continue , finish signing in on your instance ' s website and authorize Hyperspace . < / Typography >
< div className = { classes . middlePadding } / >
< div style = { { display : "flex" } } >
< div className = { classes . flexGrow } / >
< Button
color = "primary"
variant = "contained"
size = "large"
href = { this . state . authUrl ? this . state . authUrl : "" }
>
Authorize
< / Button >
< div className = { classes . flexGrow } / >
< / div >
< div className = { classes . middlePadding } / >
2019-04-23 01:05:30 +02:00
< Typography > Having trouble signing in ? < Link onClick = { ( ) = > this . startEmergencyLogin ( ) } > Sign in with a code . < / Link > < / Typography >
2019-04-07 23:25:39 +02:00
< / div >
) ;
}
2019-04-23 01:05:30 +02:00
showAuthDialog() {
const { classes } = this . props ;
return (
< Dialog
open = { this . state . openAuthDialog }
disableBackdropClick
disableEscapeKeyDown
maxWidth = "sm"
fullWidth = { true }
>
< DialogTitle >
Authorize with a code
< / DialogTitle >
< DialogContent >
< Typography paragraph >
If you 're having trouble authorizing Hyperspace, you can manually request for an authorization code. Click ' Request Code ' and then paste the code in the authorization code box to continue .
< / Typography >
< Button
color = "primary"
variant = "contained"
href = { this . state . authUrl ? this . state . authUrl : "" }
target = "_blank"
rel = "noopener noreferrer"
> Request Code < / Button >
< br / > < br / >
< TextField
variant = "outlined"
label = "Authorization code"
fullWidth
onChange = { ( event ) = > this . updateAuthCode ( event . target . value ) }
> < / TextField >
< / DialogContent >
< DialogActions >
< Button onClick = { ( ) = > this . toggleAuthDialog ( ) } > Cancel < / Button >
< Button color = "secondary" onClick = { ( ) = > this . authorizeEmergencyLogin ( ) } > Authorize < / Button >
< / DialogActions >
< / Dialog >
) ;
}
2019-04-07 23:25:39 +02:00
showAuthority() {
const { classes } = this . props ;
return (
< div >
< Typography variant = "h5" > Authorizing < / Typography >
< Typography > Please wait while Hyperspace authorizes with Mastodon . This shouldn ' t take long . . . < / Typography >
< div className = { classes . middlePadding } / >
< div style = { { display : "flex" } } >
< div className = { classes . flexGrow } / >
< CircularProgress / >
< div className = { classes . flexGrow } / >
< / div >
< div className = { classes . middlePadding } / >
< / div >
) ;
}
render() {
const { classes } = this . props ;
return (
< div className = { classes . root } style = { { backgroundImage : ` url( ${ this . state !== null ? this . state . backgroundUrl : "background.png" } ) ` } } >
< Paper className = { classes . paper } >
2019-04-09 22:53:00 +02:00
< img className = { classes . logo } alt = { this . state ? this . state . brandName : "Hyperspace" } src = { this . state ? this . state . logoUrl : "logo.png" } / >
2019-04-07 23:25:39 +02:00
< br / >
< Fade in = { true } >
{
this . state . authority ?
this . showAuthority ( ) :
this . state . wantsToLogin ?
this . showLoginAuth ( ) :
this . showLanding ( )
}
< / Fade >
< br / >
< Typography variant = "caption" >
2019-04-21 20:32:49 +02:00
& copy ; { new Date ( ) . getFullYear ( ) } { this . state . brandName && this . state . brandName !== "Hyperspace" ? ` ${ this . state . brandName } developers and the ` : "" } < Link href = "https://hyperspace.marquiskurt.net" target = "_blank" rel = "noreferrer" > Hyperspace < / Link > developers . All rights reserved .
2019-04-07 23:25:39 +02:00
< / Typography >
< Typography variant = "caption" >
2019-04-12 20:26:27 +02:00
{ this . state . repo ? < span > < Link href = { this . state . repo ? this . state . repo : "https://github.com/hyperspacedev" } target = "_blank" rel = "noreferrer" > Source code < / Link > | < / span > : null } < Link href = { this . state . license ? this . state . license : "https://www.apache.org/licenses/LICENSE-2.0" } target = "_blank" rel = "noreferrer" > License < / Link > | < Link href = "https://github.com/hyperspacedev/hyperspace/issues/new" target = "_blank" rel = "noreferrer" > File an Issue < / Link >
2019-04-07 23:25:39 +02:00
< / Typography >
< / Paper >
2019-04-23 01:05:30 +02:00
{ this . showAuthDialog ( ) }
2019-04-07 23:25:39 +02:00
< / div >
) ;
}
}
2019-04-23 19:06:31 +02:00
export default withStyles ( styles ) ( withSnackbar ( WelcomePage ) ) ;