feat: implement basic login mode (#1543)

fixes #1542
This commit is contained in:
Nolan Lawson 2019-09-26 05:28:52 -07:00 committed by GitHub
parent 2ada968439
commit 4ddf47f3da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 209 additions and 94 deletions

View file

@ -8,14 +8,17 @@ import { updateCustomEmojiForInstance } from './emoji'
import { database } from '../_database/database' import { database } from '../_database/database'
import { DOMAIN_BLOCKS } from '../_static/blocks' import { DOMAIN_BLOCKS } from '../_static/blocks'
const REDIRECT_URI = process.browser && `${location.origin}/settings/instances/add`
function createKnownError (message) { function createKnownError (message) {
const err = new Error(message) const err = new Error(message)
err.knownError = true err.knownError = true
return err return err
} }
function getRedirectUri () {
const { copyPasteMode } = store.get()
return copyPasteMode ? 'urn:ietf:wg:oauth:2.0:oob' : `${location.origin}/settings/instances/add`
}
async function redirectToOauth () { async function redirectToOauth () {
let { instanceNameInSearch, loggedInInstances } = store.get() let { instanceNameInSearch, loggedInInstances } = store.get()
instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase() instanceNameInSearch = instanceNameInSearch.replace(/^https?:\/\//, '').replace(/\/+$/, '').toLowerCase()
@ -26,7 +29,8 @@ async function redirectToOauth () {
if (DOMAIN_BLOCKS.some(domain => new RegExp(`(?:\\.|^)${domain}$`, 'i').test(instanceHostname))) { if (DOMAIN_BLOCKS.some(domain => new RegExp(`(?:\\.|^)${domain}$`, 'i').test(instanceHostname))) {
throw createKnownError('This service is blocked') throw createKnownError('This service is blocked')
} }
const registrationPromise = registerApplication(instanceNameInSearch, REDIRECT_URI) const redirectUri = getRedirectUri()
const registrationPromise = registerApplication(instanceNameInSearch, redirectUri)
const instanceInfo = await getInstanceInfo(instanceNameInSearch) const instanceInfo = await getInstanceInfo(instanceNameInSearch)
await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later await database.setInstanceInfo(instanceNameInSearch, instanceInfo) // cache for later
const instanceData = await registrationPromise const instanceData = await registrationPromise
@ -38,11 +42,16 @@ async function redirectToOauth () {
const oauthUrl = generateAuthLink( const oauthUrl = generateAuthLink(
instanceNameInSearch, instanceNameInSearch,
instanceData.client_id, instanceData.client_id,
REDIRECT_URI redirectUri
) )
// setTimeout to allow the browser to *actually* save the localStorage data (fixes Safari bug apparently) // setTimeout to allow the browser to *actually* save the localStorage data (fixes Safari bug apparently)
const { copyPasteMode } = store.get()
setTimeout(() => { setTimeout(() => {
if (copyPasteMode) {
window.open(oauthUrl, '_blank', 'noopener')
} else {
document.location.href = oauthUrl document.location.href = oauthUrl
}
}, 200) }, 200)
} }
@ -72,12 +81,13 @@ export async function logInToInstance () {
async function registerNewInstance (code) { async function registerNewInstance (code) {
const { currentRegisteredInstanceName, currentRegisteredInstance } = store.get() const { currentRegisteredInstanceName, currentRegisteredInstance } = store.get()
const redirectUri = getRedirectUri()
const instanceData = await getAccessTokenFromAuthCode( const instanceData = await getAccessTokenFromAuthCode(
currentRegisteredInstanceName, currentRegisteredInstanceName,
currentRegisteredInstance.client_id, currentRegisteredInstance.client_id,
currentRegisteredInstance.client_secret, currentRegisteredInstance.client_secret,
code, code,
REDIRECT_URI redirectUri
) )
const { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get() const { loggedInInstances, loggedInInstancesInOrder, instanceThemes } = store.get()
instanceThemes[currentRegisteredInstanceName] = DEFAULT_THEME instanceThemes[currentRegisteredInstanceName] = DEFAULT_THEME
@ -92,7 +102,8 @@ async function registerNewInstance (code) {
loggedInInstances: loggedInInstances, loggedInInstances: loggedInInstances,
currentInstance: currentRegisteredInstanceName, currentInstance: currentRegisteredInstanceName,
loggedInInstancesInOrder: loggedInInstancesInOrder, loggedInInstancesInOrder: loggedInInstancesInOrder,
instanceThemes: instanceThemes instanceThemes: instanceThemes,
copyPasteMode: false
}) })
store.save() store.save()
const { enableGrayscale } = store.get() const { enableGrayscale } = store.get()
@ -113,3 +124,16 @@ export async function handleOauthCode (code) {
store.set({ logInToInstanceLoading: false }) store.set({ logInToInstanceLoading: false })
} }
} }
export async function handleCopyPasteOauthCode (code) {
const { currentRegisteredInstanceName, currentRegisteredInstance } = store.get()
if (!currentRegisteredInstanceName || !currentRegisteredInstance) {
store.set({
logInToInstanceError: 'You must log in to an instance first.',
logInToInstanceErrorForText: '',
instanceNameInSearch: ''
})
} else {
await handleOauthCode(code)
}
}

View file

@ -0,0 +1,40 @@
<aside class="info-aside {className}">
<SvgIcon href="#fa-info-circle" className="aside-icon" />
<span>
<slot></slot>
</span>
</aside>
<style>
.info-aside {
font-size: 1.2em;
color: var(--deemphasized-text-color);
display: flex;
align-items: center;
}
:global(.info-aside a) {
text-decoration: underline;
color: var(--deemphasized-text-color);
}
:global(.info-aside span) {
flex: 1;
}
:global(.aside-icon) {
fill: var(--deemphasized-text-color);
width: 18px;
height: 18px;
margin: 0 10px 0 5px;
min-width: 18px;
}
</style>
<script>
import SvgIcon from './SvgIcon.html'
export default {
data: () => ({
className: ''
}),
components: {
SvgIcon
}
}
</script>

View file

@ -1,7 +1,8 @@
<SettingsLayout page='settings/instances/add' label="Add instance"> <SettingsLayout page='settings/instances/add' label="Add instance">
<h1 id="add-an-instance-h1">Add instance</h1> <h1 id="add-an-instance-h1">Add instance</h1>
<form class="add-new-instance" on:submit='onSubmit(event)' aria-labelledby="add-an-instance-h1"> <div class="add-new-instance">
<form on:submit='onSubmitInstance(event)' aria-labelledby="add-an-instance-h1">
{#if !hasIndexedDB || !hasLocalStorage} {#if !hasIndexedDB || !hasLocalStorage}
<div class="form-error form-error-user-error" role="alert"> <div class="form-error form-error-user-error" role="alert">
@ -23,16 +24,30 @@
</div> </div>
</noscript> </noscript>
<label class="add-new-instance-label" for="instanceInput">Instance:</label> <label for="instanceInput">Instance:</label>
<input class="add-new-instance-input" type="text" inputmode="url" id="instanceInput" <input type="text" inputmode="url" id="instanceInput"
bind:value='$instanceNameInSearch' placeholder="Enter instance name" required bind:value='$instanceNameInSearch' placeholder="Enter instance name" required
> >
<button class="primary add-new-instance-button" type="submit" id="submitButton" <button class="primary" type="submit" id="submitButton"
disabled={!$instanceNameInSearch || $logInToInstanceLoading}> disabled={!$instanceNameInSearch || $logInToInstanceLoading}>
Log in Log in
</button> </button>
</form> </form>
{#if $copyPasteMode }
<form aria-label="Enter code" on:submit="onSubmitOauth(event)">
<label for="oauthCodeInput">Code:</label>
<input type="text" id="oauthCodeInput"
bind:value='oauthCode' placeholder="Enter code" required
>
<button class="primary" type="submit" id="submitOauthButton"
disabled={!oauthCode}>
Submit
</button>
</form>
{/if}
</div>
{#if !$isUserLoggedIn} {#if !$isUserLoggedIn}
<p> <p>
Don't have an Don't have an
@ -44,22 +59,25 @@
<ExternalLink href="https://joinmastodon.org">Join Mastodon!</ExternalLink> <ExternalLink href="https://joinmastodon.org">Join Mastodon!</ExternalLink>
</p> </p>
{/if} {/if}
<p>
{#if $copyPasteMode}
Switch back to
{:else}
Trouble logging in? Switch to
{/if}
<button on:click="onCopyPasteModeButtonClick()"
class="copy-paste-mode-button"
aria-pressed={$copyPasteMode}>
{$copyPasteMode ? 'regular' : 'basic'} login mode
</button>.
</p>
{#if $copyPasteMode}
<InfoAside className="add-new-instance-aside">
In basic login mode, click "log in" to open a new window. Then copy the code and paste it above.
</InfoAside>
{/if}
</SettingsLayout> </SettingsLayout>
<style> <style>
.form-error {
border: 2px solid red;
border-radius: 2px;
padding: 10px;
font-size: 1.3em;
margin: 5px;
background-color: var(--main-bg);
}
.add-new-instance-input {
min-width: 70%;
max-width: 100%;
background-color: var(--input-bg);
}
.add-new-instance { .add-new-instance {
background: var(--form-bg); background: var(--form-bg);
padding: 5px 10px 15px; padding: 5px 10px 15px;
@ -68,13 +86,41 @@
border-radius: 4px; border-radius: 4px;
} }
.add-new-instance-label, .add-new-instance-input, .add-new-instance-button { .form-error {
border: 2px solid red;
border-radius: 2px;
padding: 10px;
font-size: 1.3em;
margin: 5px;
background-color: var(--main-bg);
}
input {
min-width: 70%;
max-width: 100%;
background-color: var(--input-bg);
}
label, input, button, :global(.add-new-instance-aside) {
display: block; display: block;
margin: 20px 5px; margin: 20px 5px;
} }
button.copy-paste-mode-button {
margin: 0;
padding: 0;
display: inline-block;
background: none;
border: none;
font-size: 1em;
color: var(--anchor-text);
}
button.copy-paste-mode-button:hover {
text-decoration: underline;
}
@media (max-width: 767px) { @media (max-width: 767px) {
.add-new-instance-input { input {
min-width: 95%; min-width: 95%;
} }
} }
@ -83,10 +129,11 @@
<script> <script>
import SettingsLayout from '../../../_components/settings/SettingsLayout.html' import SettingsLayout from '../../../_components/settings/SettingsLayout.html'
import { store } from '../../../_store/store' import { store } from '../../../_store/store'
import { logInToInstance, handleOauthCode } from '../../../_actions/addInstance' import { logInToInstance, handleOauthCode, handleCopyPasteOauthCode } from '../../../_actions/addInstance'
import ExternalLink from '../../../_components/ExternalLink.html' import ExternalLink from '../../../_components/ExternalLink.html'
import { testHasIndexedDB, testHasLocalStorage } from '../../../_utils/testStorage' import { testHasIndexedDB, testHasLocalStorage } from '../../../_utils/testStorage'
import Tooltip from '../../../_components/Tooltip.html' import Tooltip from '../../../_components/Tooltip.html'
import InfoAside from '../../../_components/InfoAside.html'
export default { export default {
async oncreate () { async oncreate () {
@ -102,18 +149,30 @@
components: { components: {
SettingsLayout, SettingsLayout,
ExternalLink, ExternalLink,
Tooltip Tooltip,
InfoAside
}, },
store: () => store, store: () => store,
data: () => ({ data: () => ({
hasIndexedDB: true, hasIndexedDB: true,
hasLocalStorage: true hasLocalStorage: true,
oauthCode: ''
}), }),
methods: { methods: {
onSubmit (event) { onSubmitInstance (event) {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
logInToInstance() logInToInstance()
},
onSubmitOauth (event) {
event.preventDefault()
event.stopPropagation()
handleCopyPasteOauthCode(this.get().oauthCode)
},
onCopyPasteModeButtonClick () {
const { copyPasteMode } = this.store.get()
console.log('copyPasteMode', copyPasteMode)
this.store.set({ copyPasteMode: !copyPasteMode })
} }
} }
} }

View file

@ -44,13 +44,10 @@
</label> </label>
</form> </form>
<aside> <InfoAside className="wellness-aside">
<SvgIcon href="#fa-info-circle" className="aside-icon" />
<span>
You can filter or disable notifications in the You can filter or disable notifications in the
<a rel="prefetch" href="/settings/instances{$currentInstance ? '/' + $currentInstance : ''}">instance settings</a>. <a rel="prefetch" href="/settings/instances{$currentInstance ? '/' + $currentInstance : ''}">instance settings</a>.
</span> </InfoAside>
</aside>
<h2>UI</h2> <h2>UI</h2>
@ -78,26 +75,8 @@
display: block; display: block;
padding: 5px 0; padding: 5px 0;
} }
aside { :global(.wellness-aside) {
font-size: 1.2em;
margin: 20px 10px 0px 10px; margin: 20px 10px 0px 10px;
color: var(--deemphasized-text-color);
display: flex;
align-items: center;
}
aside a {
text-decoration: underline;
color: var(--deemphasized-text-color);
}
aside span {
flex: 1;
}
:global(.aside-icon) {
fill: var(--deemphasized-text-color);
width: 18px;
height: 18px;
margin: 0 10px 0 5px;
min-width: 18px;
} }
@media (max-width: 240px) { @media (max-width: 240px) {
@ -110,7 +89,7 @@
import SettingsLayout from '../../_components/settings/SettingsLayout.html' import SettingsLayout from '../../_components/settings/SettingsLayout.html'
import { store } from '../../_store/store' import { store } from '../../_store/store'
import ExternalLink from '../../_components/ExternalLink.html' import ExternalLink from '../../_components/ExternalLink.html'
import SvgIcon from '../../_components/SvgIcon.html' import InfoAside from '../../_components/InfoAside.html'
export default { export default {
oncreate () { oncreate () {
@ -119,7 +98,7 @@
components: { components: {
SettingsLayout, SettingsLayout,
ExternalLink, ExternalLink,
SvgIcon InfoAside
}, },
methods: { methods: {
flushChangesToCheckAll () { flushChangesToCheckAll () {

View file

@ -1,7 +1,7 @@
import { Selector as $ } from 'testcafe' import { Selector as $ } from 'testcafe'
import { import {
addInstanceButton, addInstanceButton,
authorizeInput, confirmationDialogOKButton, authorizeInput, confirmationDialogOKButton, copyPasteModeButton,
emailInput, emailInput,
formError, formError,
getFirstVisibleStatus, getNthStatus, getOpacity, getFirstVisibleStatus, getNthStatus, getOpacity,
@ -9,10 +9,10 @@ import {
homeNavButton, homeNavButton,
instanceInput, instanceInput,
logInToInstanceLink, logInToInstanceLink,
mastodonLogInButton, mastodonLogInButton, oauthCodeInput,
passwordInput, reload, passwordInput, reload,
settingsButton, settingsButton,
sleep sleep, submitOauthButton
} from '../utils' } from '../utils'
import { loginAsFoobar } from '../roles' import { loginAsFoobar } from '../roles'
@ -96,3 +96,13 @@ test('Logs in, refreshes, then logs out', async t => {
.click(homeNavButton) .click(homeNavButton)
.expect(getOpacity('.hidden-from-ssr')()).eql('1') .expect(getOpacity('.hidden-from-ssr')()).eql('1')
}) })
test('Shows error when entering only oauth code in basic mode', async t => {
await t
.click(logInToInstanceLink)
.click(copyPasteModeButton)
.typeText(oauthCodeInput, 'blahblahblah')
.click(submitOauthButton)
.expect(formError.exists).ok()
.expect(formError.innerText).contains('You must log in to an instance first')
})

View file

@ -29,6 +29,8 @@ export const emailInput = $('input#user_email')
export const passwordInput = $('input#user_password') export const passwordInput = $('input#user_password')
export const authorizeInput = $('button[type=submit]:not(.negative)') export const authorizeInput = $('button[type=submit]:not(.negative)')
export const logInToInstanceLink = $('a[href="/settings/instances/add"]') export const logInToInstanceLink = $('a[href="/settings/instances/add"]')
export const copyPasteModeButton = $('.copy-paste-mode-button')
export const oauthCodeInput = $('#oauthCodeInput')
export const searchInput = $('.search-input') export const searchInput = $('.search-input')
export const postStatusButton = $('.compose-box-button') export const postStatusButton = $('.compose-box-button')
export const showMoreButton = $('.more-items-header button') export const showMoreButton = $('.more-items-header button')
@ -39,6 +41,7 @@ export const accountProfileFollowButton = $('.account-profile .account-profile-f
export const goBackButton = $('.dynamic-page-go-back') export const goBackButton = $('.dynamic-page-go-back')
export const accountProfileMoreOptionsButton = $('.account-profile-more-options button') export const accountProfileMoreOptionsButton = $('.account-profile-more-options button')
export const addInstanceButton = $('#submitButton') export const addInstanceButton = $('#submitButton')
export const submitOauthButton = $('#submitOauthButton')
export const mastodonLogInButton = $('button[type="submit"]') export const mastodonLogInButton = $('button[type="submit"]')
export const followsButton = $('.account-profile-details > *:nth-child(2)') export const followsButton = $('.account-profile-details > *:nth-child(2)')
export const followersButton = $('.account-profile-details > *:nth-child(3)') export const followersButton = $('.account-profile-details > *:nth-child(3)')