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' ;
2019-05-02 22:49:09 +02:00
import { createHyperspaceApp , getRedirectAddress } from '../utilities/login' ;
2019-04-07 23:25:39 +02:00
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' ;
2019-05-08 15:14:46 +02:00
import { Config } from '../types/Config' ;
2019-04-23 19:06:31 +02:00
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-26 21:53:40 +02:00
version : string ;
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 : '' ,
2019-04-26 21:53:40 +02:00
emergencyMode : false ,
version : ''
2019-04-07 23:25:39 +02:00
}
2019-04-08 00:31:18 +02:00
getConfig ( ) . then ( ( result : any ) = > {
2019-05-08 15:14:46 +02:00
if ( result !== undefined ) {
let config : Config = result ;
2019-05-11 18:59:36 +02:00
if ( result . location === "dynamic" ) {
2019-05-08 15:14:46 +02:00
console . warn ( "Recirect URI is set to dyanmic, which may affect how sign-in works for some users. Careful!" ) ;
2019-05-11 18:59:36 +02:00
}
this . setState ( {
logoUrl : config.branding? result . branding . logo : "logo.png" ,
backgroundUrl : config.branding? result . branding . background : "background.png" ,
brandName : config.branding? result . branding . name : "Hyperspace" ,
registerBase : config.registration? result . registration . defaultInstance : "" ,
federates : config.federation.universalLogin ,
license : config.license.url ,
repo : config.repository ,
defaultRedirectAddress : config.location != "dynamic" ? config . location : ` https:// ${ window . location . host } ` ,
version : config.version
} ) ;
2019-05-08 15:14:46 +02:00
}
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 ) {
2019-04-29 20:44:05 +02:00
if ( user . includes ( "@" ) ) {
if ( this . state . federates ) {
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" ) ;
}
2019-04-07 23:25:39 +02:00
} 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-28 21:50:18 +02:00
createHyperspaceApp (
this . state . brandName ? this . state . brandName : "Hyperspace" ,
scopes ,
baseurl ,
2019-05-11 22:03:40 +02:00
getRedirectAddress ( this . state . defaultRedirectAddress )
2019-04-28 21:50:18 +02:00
) . 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-28 21:50:18 +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() {
2019-05-03 19:09:33 +02:00
window . location . href = ` ${ this . state . defaultRedirectAddress } /?code= ${ this . state . authCode } #/ ` ;
2019-04-23 01:05:30 +02:00
}
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 ( "@" ) ) {
2019-04-27 19:37:36 +02:00
if ( this . state . federates && ( this . state . federates === true ) ) {
let baseUrl = this . state . user . split ( "@" ) [ 1 ] ;
axios . get ( "https://" + baseUrl + "/api/v1/timelines/public" ) . catch ( ( err : Error ) = > {
let userInputError = true ;
let userInputErrorMessage = "Instance name is invalid." ;
this . setState ( { userInputError , userInputErrorMessage } ) ;
return true ;
} ) ;
} else if ( this . state . user . includes ( this . state . registerBase ? this . state . registerBase : "mastodon.social" ) ) {
this . setState ( { userInputError , userInputErrorMessage } ) ;
return false ;
} else {
userInputError = true ;
userInputErrorMessage = "You cannot sign in with this username." ;
2019-04-09 22:53:00 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
2019-04-12 20:26:27 +02:00
return true ;
2019-04-27 19:37:36 +02:00
}
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-27 19:37:36 +02:00
this . setState ( { userInputError , userInputErrorMessage } ) ;
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 :
2019-05-11 22:03:40 +02:00
window . location . protocol === "hyperspace:" ? "hyperspace://hyperspace/app/" : ` https:// ${ window . location . host } ` ,
2019-04-23 19:06:31 +02:00
) . then ( ( tokenData : any ) = > {
localStorage . setItem ( "access_token" , tokenData . access_token ) ;
2019-05-11 22:03:40 +02:00
window . location . href = window . location . protocol === "hyperspace:" ? "hyperspace://hyperspace/app/" : ` https:// ${ window . location . host } /#/ ` ;
2019-04-23 19:06:31 +02:00
} ) . catch ( ( err : Error ) = > {
2019-04-28 21:50:18 +02:00
this . props . enqueueSnackbar ( ` Couldn't authorize ${ this . state . brandName ? this . state . brandName : "Hyperspace" } : ${ err . name } ` , { variant : 'error' } ) ;
2019-04-23 19:06:31 +02:00
console . error ( err . message ) ;
} )
}
2019-04-07 23:25:39 +02:00
}
}
2019-05-12 20:11:21 +02:00
titlebar() {
const { classes } = this . props ;
if ( ( navigator . userAgent . includes ( this . state . brandName || "Hyperspace" ) || navigator . userAgent . includes ( "Electron" ) ) && navigator . userAgent . includes ( "Macintosh" ) ) {
return (
< div className = { classes . titleBarRoot } >
< Typography className = { classes . titleBarText } > { this . state . brandName ? this . state . brandName : "Hyperspace" } < / Typography >
< / div >
) ;
}
}
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"
2019-04-26 21:53:40 +02:00
label = "Username"
2019-04-07 23:25:39 +02:00
fullWidth
2019-04-26 21:53:40 +02:00
placeholder = "example@mastodon.example"
2019-04-07 23:25:39 +02:00
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
{
2019-04-28 01:15:50 +02:00
this . state . registerBase && this . state . federates ? < Typography variant = "caption" > Not from < b > { this . state . registerBase ? this . state . registerBase : "noinstance" } < / b > ? Sign in with your < Link href = "https://docs.joinmastodon.org/usage/decentralization/#addressing-people" target = "_blank" rel = "noopener noreferrer" color = "secondary" > full username < / Link > . < / Typography > : null
2019-04-07 23:25:39 +02:00
}
< br / >
{
this . state . foundSavedLogin ?
< Typography >
2019-04-27 20:01:49 +02:00
Signing in from a previous session ? < Link className = { classes . welcomeLink } onClick = { ( ) = > this . resumeLogin ( ) } > Continue login < / Link > .
2019-04-07 23:25:39 +02:00
< / 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
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 >
2019-04-28 21:50:18 +02:00
< Typography > To continue , finish signing in on your instance ' s website and authorize { this . state . brandName ? this . state . brandName : "Hyperspace" } . < / Typography >
2019-04-07 23:25:39 +02:00
< 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-27 20:01:49 +02:00
< Typography > Having trouble signing in ? < Link onClick = { ( ) = > this . startEmergencyLogin ( ) } className = { classes . welcomeLink } > 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 (
2019-05-12 20:11:21 +02:00
< div >
{ this . titlebar ( ) }
< div className = { classes . root } style = { { backgroundImage : ` url( ${ this . state !== null ? this . state . backgroundUrl : "background.png" } ) ` } } >
< Paper className = { classes . paper } >
< img className = { classes . logo } alt = { this . state ? this . state . brandName : "Hyperspace" } src = { this . state ? this . state . logoUrl : "logo.png" } / >
< br / >
< Fade in = { true } >
{
this . state . authority ?
this . showAuthority ( ) :
this . state . wantsToLogin ?
this . showLoginAuth ( ) :
this . showLanding ( )
}
< / Fade >
< br / >
< Typography variant = "caption" >
& copy ; { new Date ( ) . getFullYear ( ) } { this . state . brandName && this . state . brandName !== "Hyperspace" ? ` ${ this . state . brandName } developers and the ` : "" } < Link className = { classes . welcomeLink } href = "https://hyperspace.marquiskurt.net" target = "_blank" rel = "noreferrer" > Hyperspace < / Link > developers . All rights reserved .
< / Typography >
< Typography variant = "caption" >
{ this . state . repo ? < span >
< Link className = { classes . welcomeLink } href = { this . state . repo ? this . state . repo : "https://github.com/hyperspacedev" } target = "_blank" rel = "noreferrer" > Source code < / Link > | < / span > : null }
< Link className = { classes . welcomeLink } href = { this . state . license ? this . state . license : "https://www.apache.org/licenses/LICENSE-2.0" } target = "_blank" rel = "noreferrer" > License < / Link > |
< Link className = { classes . welcomeLink } href = "https://github.com/hyperspacedev/hyperspace/issues/new" target = "_blank" rel = "noreferrer" > File an Issue < / Link >
< / Typography >
< Typography variant = "caption" color = "textSecondary" >
{ this . state . brandName ? this . state . brandName : "Hypersapce" } v . { this . state . version } { this . state . brandName && this . state . brandName !== "Hyperspace" ? "(Hyperspace-like)" : null }
< / Typography >
< / Paper >
{ this . showAuthDialog ( ) }
< / div >
< / div >
2019-04-07 23:25:39 +02:00
) ;
}
}
2019-04-23 19:06:31 +02:00
export default withStyles ( styles ) ( withSnackbar ( WelcomePage ) ) ;