merge develop
This commit is contained in:
commit
c92d86519c
|
@ -31,8 +31,7 @@ module.exports = {
|
|||
'vue/require-prop-types': 1,
|
||||
'vue/no-use-v-if-with-v-for': 1,
|
||||
'indent': 1,
|
||||
'import/first': 1, // ????
|
||||
'import-first': 1,
|
||||
'import/first': 1,
|
||||
'object-curly-spacing': 1,
|
||||
'prefer-promise-reject-errors': 1,
|
||||
'eol-last': 1,
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chenfengyuan/vue-qrcode": "^1.0.0",
|
||||
"babel-plugin-add-module-exports": "^0.2.1",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"chromatism": "^3.0.0",
|
||||
|
@ -25,6 +26,7 @@
|
|||
"object-path": "^0.11.3",
|
||||
"phoenix": "^1.3.0",
|
||||
"popper.js": "^1.14.7",
|
||||
"portal-vue": "^2.1.4",
|
||||
"sanitize-html": "^1.13.0",
|
||||
"v-click-outside": "^2.1.1",
|
||||
"vue": "^2.5.13",
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
</div>
|
||||
<chat-panel :floating="true" v-if="currentUser && chat" class="floating-chat mobile-hidden"></chat-panel>
|
||||
<UserReportingModal />
|
||||
<portal-target name="modal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import VueRouter from 'vue-router'
|
|||
import routes from './routes'
|
||||
import App from '../App.vue'
|
||||
import { windowWidth } from '../services/window_utils/window_utils'
|
||||
import { getOrCreateApp, getClientToken } from '../services/new_api/oauth.js'
|
||||
import backendInteractorService from '../services/backend_interactor_service/backend_interactor_service.js'
|
||||
|
||||
const getStatusnetConfig = async ({ store }) => {
|
||||
try {
|
||||
|
@ -92,6 +94,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
? 0
|
||||
: config.logoMargin
|
||||
})
|
||||
store.commit('authFlow/setInitialStrategy', config.loginMethod)
|
||||
|
||||
copyInstanceOption('redirectRootNoLogin')
|
||||
copyInstanceOption('redirectRootLogin')
|
||||
|
@ -100,7 +103,6 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
|
|||
copyInstanceOption('formattingOptionsEnabled')
|
||||
copyInstanceOption('hideMutedPosts')
|
||||
copyInstanceOption('collapseMessageWithSubject')
|
||||
copyInstanceOption('loginMethod')
|
||||
copyInstanceOption('scopeCopy')
|
||||
copyInstanceOption('subjectLineBehavior')
|
||||
copyInstanceOption('postContentType')
|
||||
|
@ -188,6 +190,17 @@ const getCustomEmoji = async ({ store }) => {
|
|||
}
|
||||
}
|
||||
|
||||
const getAppSecret = async ({ store }) => {
|
||||
const { state, commit } = store
|
||||
const { oauth, instance } = state
|
||||
return getOrCreateApp({ ...oauth, instance: instance.server, commit })
|
||||
.then((app) => getClientToken({ ...app, instance: instance.server }))
|
||||
.then((token) => {
|
||||
commit('setAppToken', token.access_token)
|
||||
commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
})
|
||||
}
|
||||
|
||||
const getNodeInfo = async ({ store }) => {
|
||||
try {
|
||||
const res = await window.fetch('/nodeinfo/2.0.json')
|
||||
|
@ -229,14 +242,14 @@ const setConfig = async ({ store }) => {
|
|||
const apiConfig = configInfos[0]
|
||||
const staticConfig = configInfos[1]
|
||||
|
||||
await setSettings({ store, apiConfig, staticConfig })
|
||||
await setSettings({ store, apiConfig, staticConfig }).then(getAppSecret({ store }))
|
||||
}
|
||||
|
||||
const checkOAuthToken = async ({ store }) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (store.state.oauth.token) {
|
||||
if (store.getters.getUserToken()) {
|
||||
try {
|
||||
await store.dispatch('loginUser', store.state.oauth.token)
|
||||
await store.dispatch('loginUser', store.getters.getUserToken())
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import FollowRequests from 'components/follow_requests/follow_requests.vue'
|
|||
import OAuthCallback from 'components/oauth_callback/oauth_callback.vue'
|
||||
import UserSearch from 'components/user_search/user_search.vue'
|
||||
import Notifications from 'components/notifications/notifications.vue'
|
||||
import LoginForm from 'components/login_form/login_form.vue'
|
||||
import AuthForm from 'components/auth_form/auth_form.js'
|
||||
import ChatPanel from 'components/chat_panel/chat_panel.vue'
|
||||
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
|
||||
import About from 'components/about/about.vue'
|
||||
|
@ -42,7 +42,7 @@ export default (store) => {
|
|||
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
|
||||
{ name: 'user-settings', path: '/user-settings', component: UserSettings },
|
||||
{ name: 'notifications', path: '/:username/notifications', component: Notifications },
|
||||
{ name: 'login', path: '/login', component: LoginForm },
|
||||
{ name: 'login', path: '/login', component: AuthForm },
|
||||
{ name: 'chat', path: '/chat', component: ChatPanel, props: () => ({ floating: false }) },
|
||||
{ name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) },
|
||||
{ name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) },
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import LoginForm from '../login_form/login_form.vue'
|
||||
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
|
||||
import MFATOTPForm from '../mfa_form/totp_form.vue'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
const AuthForm = {
|
||||
name: 'AuthForm',
|
||||
render (createElement) {
|
||||
return createElement('component', { is: this.authForm })
|
||||
},
|
||||
computed: {
|
||||
authForm () {
|
||||
if (this.requiredTOTP) { return 'MFATOTPForm' }
|
||||
if (this.requiredRecovery) { return 'MFARecoveryForm' }
|
||||
return 'LoginForm'
|
||||
},
|
||||
...mapGetters('authFlow', ['requiredTOTP', 'requiredRecovery'])
|
||||
},
|
||||
components: {
|
||||
MFARecoveryForm,
|
||||
MFATOTPForm,
|
||||
LoginForm
|
||||
}
|
||||
}
|
||||
|
||||
export default AuthForm
|
|
@ -62,6 +62,7 @@
|
|||
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,6 +81,7 @@
|
|||
background-color: var(--lightBg, $fallback--lightBg);
|
||||
border-top: 1px solid $fallback--bg;
|
||||
border-top: 1px solid var(--bg, $fallback--bg);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
|
|
|
@ -1,54 +1,80 @@
|
|||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import oauthApi from '../../services/new_api/oauth.js'
|
||||
|
||||
const LoginForm = {
|
||||
data: () => ({
|
||||
user: {},
|
||||
authError: false
|
||||
error: false
|
||||
}),
|
||||
computed: {
|
||||
loginMethod () { return this.$store.state.instance.loginMethod },
|
||||
loggingIn () { return this.$store.state.users.loggingIn },
|
||||
registrationOpen () { return this.$store.state.instance.registrationOpen }
|
||||
isPasswordAuth () { return this.requiredPassword },
|
||||
isTokenAuth () { return this.requiredToken },
|
||||
...mapState({
|
||||
registrationOpen: state => state.instance.registrationOpen,
|
||||
instance: state => state.instance,
|
||||
loggingIn: state => state.users.loggingIn,
|
||||
oauth: state => state.oauth
|
||||
}),
|
||||
...mapGetters(
|
||||
'authFlow', ['requiredPassword', 'requiredToken', 'requiredMFA']
|
||||
)
|
||||
},
|
||||
methods: {
|
||||
oAuthLogin () {
|
||||
oauthApi.login({
|
||||
oauth: this.$store.state.oauth,
|
||||
instance: this.$store.state.instance.server,
|
||||
commit: this.$store.commit
|
||||
})
|
||||
},
|
||||
...mapMutations('authFlow', ['requireMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
submit () {
|
||||
this.isTokenAuth ? this.submitToken() : this.submitPassword()
|
||||
},
|
||||
submitToken () {
|
||||
const { clientId } = this.oauth
|
||||
const data = {
|
||||
oauth: this.$store.state.oauth,
|
||||
instance: this.$store.state.instance.server
|
||||
clientId,
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
}
|
||||
this.clearError()
|
||||
|
||||
oauthApi.getOrCreateApp(data)
|
||||
.then((app) => { oauthApi.login({ ...app, ...data }) })
|
||||
},
|
||||
submitPassword () {
|
||||
const { clientId } = this.oauth
|
||||
const data = {
|
||||
clientId,
|
||||
oauth: this.oauth,
|
||||
instance: this.instance.server,
|
||||
commit: this.$store.commit
|
||||
}
|
||||
this.error = false
|
||||
|
||||
oauthApi.getOrCreateApp(data).then((app) => {
|
||||
oauthApi.getTokenWithCredentials(
|
||||
{
|
||||
app,
|
||||
...app,
|
||||
instance: data.instance,
|
||||
username: this.user.username,
|
||||
password: this.user.password
|
||||
}
|
||||
).then(async (result) => {
|
||||
).then((result) => {
|
||||
if (result.error) {
|
||||
this.authError = result.error
|
||||
this.user.password = ''
|
||||
if (result.error === 'mfa_required') {
|
||||
this.requireMFA({app: app, settings: result})
|
||||
} else {
|
||||
this.error = result.error
|
||||
this.focusOnPasswordInput()
|
||||
}
|
||||
return
|
||||
}
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
try {
|
||||
await this.$store.dispatch('loginUser', result.access_token)
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({name: 'friends'})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
this.authError = false
|
||||
clearError () { this.error = false },
|
||||
focusOnPasswordInput () {
|
||||
let passwordInput = this.$refs.passwordInput
|
||||
passwordInput.focus()
|
||||
passwordInput.setSelectionRange(0, passwordInput.value.length)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,47 +1,53 @@
|
|||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
<div class="panel-heading">
|
||||
{{$t('login.login')}}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">{{$t('login.login')}}</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<template v-if="isPasswordAuth">
|
||||
<div class='form-group'>
|
||||
<label for='username'>{{$t('login.username')}}</label>
|
||||
<input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')">
|
||||
<input :disabled="loggingIn" v-model='user.username'
|
||||
class='form-control' id='username'
|
||||
:placeholder="$t('login.placeholder')">
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='password'>{{$t('login.password')}}</label>
|
||||
<input :disabled="loggingIn" v-model='user.password' class='form-control' id='password' type='password'>
|
||||
<input :disabled="loggingIn" v-model='user.password'
|
||||
ref='passwordInput' class='form-control' id='password' type='password'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<form v-if="loginMethod == 'token'" v-on:submit.prevent='oAuthLogin' class="login-form">
|
||||
<div class="form-group">
|
||||
<p>{{$t('login.description')}}</p>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
|
||||
<div class="form-group" v-if="isTokenAuth">
|
||||
<p>{{$t('login.description')}}</p>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<router-link :to="{name: 'registration'}"
|
||||
v-if='registrationOpen'
|
||||
class='register'>
|
||||
{{$t('login.register')}}
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-if="authError" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{authError}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
<button :disabled="loggingIn" type='submit' class='btn btn-default'>
|
||||
{{$t('login.login')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./login_form.js" ></script>
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
code: null,
|
||||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authApp: 'authFlow/app',
|
||||
authSettings: 'authFlow/settings'
|
||||
}),
|
||||
...mapState({ instance: 'instance' })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
clearError () { this.error = false },
|
||||
submit () {
|
||||
const data = {
|
||||
app: this.authApp,
|
||||
instance: this.instance.server,
|
||||
mfaToken: this.authSettings.mfa_token,
|
||||
code: this.code
|
||||
}
|
||||
|
||||
mfaApi.verifyRecoveryCode(data).then((result) => {
|
||||
if (result.error) {
|
||||
this.error = result.error
|
||||
this.code = null
|
||||
return
|
||||
}
|
||||
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">{{$t('login.heading.recovery')}}</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<div class='form-group'>
|
||||
<label for='code'>{{$t('login.recovery_code')}}</label>
|
||||
<input v-model='code' class='form-control' id='code'>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<a href="#" @click.prevent="requireTOTP">
|
||||
{{$t('login.enter_two_factor_code')}}
|
||||
</a>
|
||||
<br />
|
||||
<a href="#" @click.prevent="abortMFA">
|
||||
{{$t('general.cancel')}}
|
||||
</a>
|
||||
</div>
|
||||
<button type='submit' class='btn btn-default'>
|
||||
{{$t('general.verify')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./recovery_form.js" ></script>
|
|
@ -0,0 +1,40 @@
|
|||
import mfaApi from '../../services/new_api/mfa.js'
|
||||
import { mapState, mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
export default {
|
||||
data: () => ({
|
||||
code: null,
|
||||
error: false
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters({
|
||||
authApp: 'authFlow/app',
|
||||
authSettings: 'authFlow/settings'
|
||||
}),
|
||||
...mapState({ instance: 'instance' })
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
|
||||
...mapActions({ login: 'authFlow/login' }),
|
||||
clearError () { this.error = false },
|
||||
submit () {
|
||||
const data = {
|
||||
app: this.authApp,
|
||||
instance: this.instance.server,
|
||||
mfaToken: this.authSettings.mfa_token,
|
||||
code: this.code
|
||||
}
|
||||
|
||||
mfaApi.verifyOTPCode(data).then((result) => {
|
||||
if (result.error) {
|
||||
this.error = result.error
|
||||
this.code = null
|
||||
return
|
||||
}
|
||||
|
||||
this.login(result).then(() => {
|
||||
this.$router.push({name: 'friends'})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="login panel panel-default">
|
||||
<!-- Default panel contents -->
|
||||
|
||||
<div class="panel-heading">
|
||||
{{$t('login.heading.totp')}}
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<form class='login-form' @submit.prevent='submit'>
|
||||
<div class='form-group'>
|
||||
<label for='code'>
|
||||
{{$t('login.authentication_code')}}
|
||||
</label>
|
||||
<input v-model='code' class='form-control' id='code'>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<div class='login-bottom'>
|
||||
<div>
|
||||
<a href="#" @click.prevent="requireRecovery">
|
||||
{{$t('login.enter_recovery_code')}}
|
||||
</a>
|
||||
<br />
|
||||
<a href="#" @click.prevent="abortMFA">
|
||||
{{$t('general.cancel')}}
|
||||
</a>
|
||||
</div>
|
||||
<button type='submit' class='btn btn-default'>
|
||||
{{$t('general.verify')}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class='form-group'>
|
||||
<div class='alert error'>
|
||||
{{error}}
|
||||
<i class="button-icon icon-cancel" @click="clearError"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./totp_form.js"></script>
|
|
@ -65,18 +65,20 @@
|
|||
{{ $t('user_card.admin_menu.moderation') }}
|
||||
</button>
|
||||
</Popper>
|
||||
<DialogModal v-if="showDeleteUserDialog" :onCancel='deleteUserDialog.bind(this, false)'>
|
||||
<span slot="header">{{ $t('user_card.admin_menu.delete_user') }}</span>
|
||||
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
||||
<span slot="footer">
|
||||
<button @click='deleteUserDialog(false)'>
|
||||
{{ $t('general.cancel') }}
|
||||
</button>
|
||||
<button class="danger" @click='deleteUser()'>
|
||||
{{ $t('user_card.admin_menu.delete_user') }}
|
||||
</button>
|
||||
</span>
|
||||
</DialogModal>
|
||||
<portal to="modal">
|
||||
<DialogModal v-if="showDeleteUserDialog" :onCancel='deleteUserDialog.bind(this, false)'>
|
||||
<template slot="header">{{ $t('user_card.admin_menu.delete_user') }}</template>
|
||||
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
|
||||
<template slot="footer">
|
||||
<button class="btn btn-default" @click='deleteUserDialog(false)'>
|
||||
{{ $t('general.cancel') }}
|
||||
</button>
|
||||
<button class="btn btn-default danger" @click='deleteUser()'>
|
||||
{{ $t('user_card.admin_menu.delete_user') }}
|
||||
</button>
|
||||
</template>
|
||||
</DialogModal>
|
||||
</portal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -4,14 +4,16 @@ const oac = {
|
|||
props: ['code'],
|
||||
mounted () {
|
||||
if (this.code) {
|
||||
const { clientId } = this.$store.state.oauth
|
||||
|
||||
oauth.getToken({
|
||||
app: this.$store.state.oauth,
|
||||
clientId,
|
||||
instance: this.$store.state.instance.server,
|
||||
code: this.code
|
||||
}).then((result) => {
|
||||
this.$store.commit('setToken', result.access_token)
|
||||
this.$store.dispatch('loginUser', result.access_token)
|
||||
this.$router.push({name: 'friends'})
|
||||
this.$router.push({ name: 'friends' })
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import LoginForm from '../login_form/login_form.vue'
|
||||
import AuthForm from '../auth_form/auth_form.js'
|
||||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import UserCard from '../user_card/user_card.vue'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const UserPanel = {
|
||||
computed: {
|
||||
user () { return this.$store.state.users.currentUser }
|
||||
signedIn () { return this.user },
|
||||
...mapState({ user: state => state.users.currentUser })
|
||||
},
|
||||
components: {
|
||||
LoginForm,
|
||||
AuthForm,
|
||||
PostStatusForm,
|
||||
UserCard
|
||||
}
|
||||
|
|
|
@ -1,13 +1,20 @@
|
|||
<template>
|
||||
<div class="user-panel">
|
||||
<div v-if='user' class="panel panel-default" style="overflow: visible;">
|
||||
|
||||
<div v-if="signedIn" key="user-panel" class="panel panel-default signed-in">
|
||||
<UserCard :user="user" :hideBio="true" rounded="top"/>
|
||||
<div class="panel-footer">
|
||||
<post-status-form v-if='user'></post-status-form>
|
||||
</div>
|
||||
</div>
|
||||
<login-form v-if='!user'></login-form>
|
||||
<auth-form v-else key="user-panel"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./user_panel.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.user-panel .signed-in {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
const Confirm = {
|
||||
props: ['disabled'],
|
||||
data: () => ({}),
|
||||
methods: {
|
||||
confirm () { this.$emit('confirm') },
|
||||
cancel () { this.$emit('cancel') }
|
||||
}
|
||||
}
|
||||
export default Confirm
|
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
<button class="btn btn-default" @click="confirm" :disabled="disabled">
|
||||
{{$t('general.confirm')}}
|
||||
</button>
|
||||
<button class="btn btn-default" @click="cancel" :disabled="disabled">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./confirm.js">
|
||||
</script>
|
|
@ -0,0 +1,155 @@
|
|||
import RecoveryCodes from './mfa_backup_codes.vue'
|
||||
import TOTP from './mfa_totp.vue'
|
||||
import Confirm from './confirm.vue'
|
||||
import VueQrcode from '@chenfengyuan/vue-qrcode'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
const Mfa = {
|
||||
data: () => ({
|
||||
settings: { // current settings of MFA
|
||||
available: false,
|
||||
enabled: false,
|
||||
totp: false
|
||||
},
|
||||
setupState: { // setup mfa
|
||||
state: '', // state of setup. '' -> 'getBackupCodes' -> 'setupOTP' -> 'complete'
|
||||
setupOTPState: '' // state of setup otp. '' -> 'prepare' -> 'confirm' -> 'complete'
|
||||
},
|
||||
backupCodes: {
|
||||
getNewCodes: false,
|
||||
inProgress: false, // progress of fetch codes
|
||||
codes: []
|
||||
},
|
||||
otpSettings: { // pre-setup setting of OTP. secret key, qrcode url.
|
||||
provisioning_uri: '',
|
||||
key: ''
|
||||
},
|
||||
currentPassword: null,
|
||||
otpConfirmToken: null,
|
||||
error: null,
|
||||
readyInit: false
|
||||
}),
|
||||
components: {
|
||||
'recovery-codes': RecoveryCodes,
|
||||
'totp-item': TOTP,
|
||||
'qrcode': VueQrcode,
|
||||
'confirm': Confirm
|
||||
},
|
||||
computed: {
|
||||
canSetupOTP () {
|
||||
return (
|
||||
(this.setupInProgress && this.backupCodesPrepared) ||
|
||||
this.settings.enabled
|
||||
) && !this.settings.totp && !this.setupOTPInProgress
|
||||
},
|
||||
setupInProgress () {
|
||||
return this.setupState.state !== '' && this.setupState.state !== 'complete'
|
||||
},
|
||||
setupOTPInProgress () {
|
||||
return this.setupState.state === 'setupOTP' && !this.completedOTP
|
||||
},
|
||||
prepareOTP () {
|
||||
return this.setupState.setupOTPState === 'prepare'
|
||||
},
|
||||
confirmOTP () {
|
||||
return this.setupState.setupOTPState === 'confirm'
|
||||
},
|
||||
completedOTP () {
|
||||
return this.setupState.setupOTPState === 'completed'
|
||||
},
|
||||
backupCodesPrepared () {
|
||||
return !this.backupCodes.inProgress && this.backupCodes.codes.length > 0
|
||||
},
|
||||
confirmNewBackupCodes () {
|
||||
return this.backupCodes.getNewCodes
|
||||
},
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor
|
||||
})
|
||||
},
|
||||
|
||||
methods: {
|
||||
activateOTP () {
|
||||
if (!this.settings.enabled) {
|
||||
this.setupState.state = 'getBackupcodes'
|
||||
this.fetchBackupCodes()
|
||||
}
|
||||
},
|
||||
fetchBackupCodes () {
|
||||
this.backupCodes.inProgress = true
|
||||
this.backupCodes.codes = []
|
||||
|
||||
return this.backendInteractor.generateMfaBackupCodes()
|
||||
.then((res) => {
|
||||
this.backupCodes.codes = res.codes
|
||||
this.backupCodes.inProgress = false
|
||||
})
|
||||
},
|
||||
getBackupCodes () { // get a new backup codes
|
||||
this.backupCodes.getNewCodes = true
|
||||
},
|
||||
confirmBackupCodes () { // confirm getting new backup codes
|
||||
this.fetchBackupCodes().then((res) => {
|
||||
this.backupCodes.getNewCodes = false
|
||||
})
|
||||
},
|
||||
cancelBackupCodes () { // cancel confirm form of new backup codes
|
||||
this.backupCodes.getNewCodes = false
|
||||
},
|
||||
|
||||
// Setup OTP
|
||||
setupOTP () { // prepare setup OTP
|
||||
this.setupState.state = 'setupOTP'
|
||||
this.setupState.setupOTPState = 'prepare'
|
||||
this.backendInteractor.mfaSetupOTP()
|
||||
.then((res) => {
|
||||
this.otpSettings = res
|
||||
this.setupState.setupOTPState = 'confirm'
|
||||
})
|
||||
},
|
||||
doConfirmOTP () { // handler confirm enable OTP
|
||||
this.error = null
|
||||
this.backendInteractor.mfaConfirmOTP({
|
||||
token: this.otpConfirmToken,
|
||||
password: this.currentPassword
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.error) {
|
||||
this.error = res.error
|
||||
return
|
||||
}
|
||||
this.completeSetup()
|
||||
})
|
||||
},
|
||||
|
||||
completeSetup () {
|
||||
this.setupState.setupOTPState = 'complete'
|
||||
this.setupState.state = 'complete'
|
||||
this.currentPassword = null
|
||||
this.error = null
|
||||
this.fetchSettings()
|
||||
},
|
||||
cancelSetup () { // cancel setup
|
||||
this.setupState.setupOTPState = ''
|
||||
this.setupState.state = ''
|
||||
this.currentPassword = null
|
||||
this.error = null
|
||||
},
|
||||
// end Setup OTP
|
||||
|
||||
// fetch settings from server
|
||||
async fetchSettings () {
|
||||
let result = await this.backendInteractor.fetchSettingsMFA()
|
||||
if (result.error) return
|
||||
this.settings = result.settings
|
||||
this.settings.available = true
|
||||
return result
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.fetchSettings().then(() => {
|
||||
this.readyInit = true
|
||||
})
|
||||
}
|
||||
}
|
||||
export default Mfa
|
|
@ -0,0 +1,121 @@
|
|||
<template>
|
||||
<div class="setting-item mfa-settings" v-if="readyInit && settings.available">
|
||||
|
||||
<div class="mfa-heading">
|
||||
<h2>{{$t('settings.mfa.title')}}</h2>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="setting-item" v-if="!setupInProgress">
|
||||
<!-- Enabled methods -->
|
||||
<h3>{{$t('settings.mfa.authentication_methods')}}</h3>
|
||||
<totp-item :settings="settings" @deactivate="fetchSettings" @activate="activateOTP"/>
|
||||
<br />
|
||||
|
||||
<div v-if="settings.enabled"> <!-- backup codes block-->
|
||||
<recovery-codes :backup-codes="backupCodes" v-if="!confirmNewBackupCodes" />
|
||||
<button class="btn btn-default" @click="getBackupCodes" v-if="!confirmNewBackupCodes">
|
||||
{{$t('settings.mfa.generate_new_recovery_codes')}}
|
||||
</button>
|
||||
|
||||
<div v-if="confirmNewBackupCodes">
|
||||
<confirm @confirm="confirmBackupCodes" @cancel="cancelBackupCodes"
|
||||
:disabled="backupCodes.inProgress">
|
||||
<p class="warning">{{$t('settings.mfa.warning_of_generate_new_codes')}}</p>
|
||||
</confirm>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="setupInProgress"> <!-- setup block-->
|
||||
|
||||
<h3>{{$t('settings.mfa.setup_otp')}}</h3>
|
||||
|
||||
<recovery-codes :backup-codes="backupCodes" v-if="!setupOTPInProgress"/>
|
||||
|
||||
|
||||
<button class="btn btn-default" @click="cancelSetup" v-if="canSetupOTP">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" v-if="canSetupOTP" @click="setupOTP">
|
||||
{{$t('settings.mfa.setup_otp')}}
|
||||
</button>
|
||||
|
||||
<template v-if="setupOTPInProgress">
|
||||
<i v-if="prepareOTP">{{$t('settings.mfa.wait_pre_setup_otp')}}</i>
|
||||
|
||||
<div v-if="confirmOTP">
|
||||
<div class="setup-otp">
|
||||
<div class="qr-code">
|
||||
<h4>{{$t('settings.mfa.scan.title')}}</h4>
|
||||
<p>{{$t('settings.mfa.scan.desc')}}</p>
|
||||
<qrcode :value="otpSettings.provisioning_uri" :options="{ width: 200 }"></qrcode>
|
||||
<p>
|
||||
{{$t('settings.mfa.scan.secret_code')}}:
|
||||
{{otpSettings.key}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="verify">
|
||||
<h4>{{$t('general.verify')}}</h4>
|
||||
<p>{{$t('settings.mfa.verify.desc')}}</p>
|
||||
<input type="text" v-model="otpConfirmToken">
|
||||
|
||||
<p>{{$t('settings.enter_current_password_to_confirm')}}:</p>
|
||||
<input type="password" v-model="currentPassword">
|
||||
<div class="confirm-otp-actions">
|
||||
<button class="btn btn-default" @click="doConfirmOTP">
|
||||
{{$t('settings.mfa.confirm_and_enable')}}
|
||||
</button>
|
||||
<button class="btn btn-default" @click="cancelSetup">
|
||||
{{$t('general.cancel')}}
|
||||
</button>
|
||||
</div>
|
||||
<div class="alert error" v-if="error">{{error}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./mfa.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.mfa-settings {
|
||||
.mfa-heading, .method-item {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.setup-otp {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
.qr-code {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.verify { flex: 1; }
|
||||
.error { margin: 4px 0 0 0; }
|
||||
.confirm-otp-actions {
|
||||
button {
|
||||
width: 15em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,17 @@
|
|||
export default {
|
||||
props: {
|
||||
backupCodes: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
inProgress: false,
|
||||
codes: []
|
||||
})
|
||||
}
|
||||
},
|
||||
data: () => ({}),
|
||||
computed: {
|
||||
inProgress () { return this.backupCodes.inProgress },
|
||||
ready () { return this.backupCodes.codes.length > 0 },
|
||||
displayTitle () { return this.inProgress || this.ready }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<h4 v-if="displayTitle">{{$t('settings.mfa.recovery_codes')}}</h4>
|
||||
<i v-if="inProgress">{{$t('settings.mfa.waiting_a_recovery_codes')}}</i>
|
||||
<template v-if="ready">
|
||||
<p class="alert warning">{{$t('settings.mfa.recovery_codes_warning')}}</p>
|
||||
<ul class="backup-codes"><li v-for="code in backupCodes.codes">{{code}}</li></ul>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_backup_codes.js"></script>
|
||||
<style lang="scss">
|
||||
@import '../../_variables.scss';
|
||||
|
||||
.warning {
|
||||
color: $fallback--cOrange;
|
||||
color: var(--cOrange, $fallback--cOrange);
|
||||
}
|
||||
.backup-codes {
|
||||
font-family: var(--postCodeFont, monospace);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,49 @@
|
|||
import Confirm from './confirm.vue'
|
||||
import { mapState } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: ['settings'],
|
||||
data: () => ({
|
||||
error: false,
|
||||
currentPassword: '',
|
||||
deactivate: false,
|
||||
inProgress: false // progress peform request to disable otp method
|
||||
}),
|
||||
components: {
|
||||
'confirm': Confirm
|
||||
},
|
||||
computed: {
|
||||
isActivated () {
|
||||
return this.settings.totp
|
||||
},
|
||||
...mapState({
|
||||
backendInteractor: (state) => state.api.backendInteractor
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
doActivate () {
|
||||
this.$emit('activate')
|
||||
},
|
||||
cancelDeactivate () { this.deactivate = false },
|
||||
doDeactivate () {
|
||||
this.error = null
|
||||
this.deactivate = true
|
||||
},
|
||||
confirmDeactivate () { // confirm deactivate TOTP method
|
||||
this.error = null
|
||||
this.inProgress = true
|
||||
this.backendInteractor.mfaDisableOTP({
|
||||
password: this.currentPassword
|
||||
})
|
||||
.then((res) => {
|
||||
this.inProgress = false
|
||||
if (res.error) {
|
||||
this.error = res.error
|
||||
return
|
||||
}
|
||||
this.deactivate = false
|
||||
this.$emit('deactivate')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="method-item">
|
||||
<strong>{{$t('settings.mfa.otp')}}</strong>
|
||||
<button class="btn btn-default" v-if="!isActivated" @click="doActivate">
|
||||
{{$t('general.enable')}}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" :disabled="deactivate" @click="doDeactivate"
|
||||
v-if="isActivated">
|
||||
{{$t('general.disable')}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<confirm @confirm="confirmDeactivate" @cancel="cancelDeactivate"
|
||||
:disabled="inProgress" v-if="deactivate">
|
||||
{{$t('settings.enter_current_password_to_confirm')}}:
|
||||
<input type="password" v-model="currentPassword">
|
||||
</confirm>
|
||||
<div class="alert error" v-if="error">{{error}}</div>
|
||||
</div>
|
||||
</template>
|
||||
<script src="./mfa_totp.js"></script>
|
|
@ -17,6 +17,7 @@ import Importer from '../importer/importer.vue'
|
|||
import Exporter from '../exporter/exporter.vue'
|
||||
import withSubscription from '../../hocs/with_subscription/with_subscription'
|
||||
import userSearchApi from '../../services/new_api/user_search.js'
|
||||
import Mfa from './mfa.vue'
|
||||
|
||||
const BlockList = withSubscription({
|
||||
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
|
||||
|
@ -75,7 +76,8 @@ const UserSettings = {
|
|||
MuteCard,
|
||||
ProgressButton,
|
||||
Importer,
|
||||
Exporter
|
||||
Exporter,
|
||||
Mfa
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
<scope-selector
|
||||
:showAll="true"
|
||||
:userDefault="newDefaultScope"
|
||||
:initialScope="newDefaultScope"
|
||||
:onScopeChange="changeVis"/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -151,7 +152,7 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mfa />
|
||||
<div class="setting-item">
|
||||
<h2>{{$t('settings.delete_account')}}</h2>
|
||||
<p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
|
||||
|
|
|
@ -20,7 +20,8 @@ const WhoToFollow = {
|
|||
id: 0,
|
||||
name: i.display_name,
|
||||
screen_name: i.acct,
|
||||
profile_image_url: i.avatar || '/images/avi.png'
|
||||
profile_image_url: i.avatar || '/images/avi.png',
|
||||
profile_image_url_original: i.avatar || '/images/avi.png'
|
||||
}
|
||||
this.users.push(user)
|
||||
|
||||
|
|
|
@ -6,14 +6,18 @@
|
|||
{{$t('who_to_follow.who_to_follow')}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body who-to-follow">
|
||||
<span v-for="user in usersToFollow">
|
||||
<div class="who-to-follow">
|
||||
<p v-for="user in usersToFollow" class="who-to-follow-items">
|
||||
<img v-bind:src="user.img" />
|
||||
<router-link v-bind:to="userProfileLink(user.id, user.name)">
|
||||
{{user.name}}
|
||||
</router-link><br />
|
||||
</span>
|
||||
<img v-bind:src="$store.state.instance.logo"> <router-link :to="{ name: 'who-to-follow' }">{{$t('who_to_follow.more')}}</router-link>
|
||||
</p>
|
||||
<p class="who-to-follow-more">
|
||||
<router-link :to="{ name: 'who-to-follow' }">
|
||||
{{$t('who_to_follow.more')}}
|
||||
</router-link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,11 +34,19 @@
|
|||
height: 32px;
|
||||
}
|
||||
.who-to-follow {
|
||||
padding: 0.5em 1em 0.5em 1em;
|
||||
padding: 0em 1em;
|
||||
margin: 0px;
|
||||
line-height: 40px;
|
||||
}
|
||||
.who-to-follow-items {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0px;
|
||||
margin: 1em 0em;
|
||||
}
|
||||
.who-to-follow-more {
|
||||
padding: 0px;
|
||||
margin: 1em 0em;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -27,7 +27,11 @@
|
|||
"optional": "optional",
|
||||
"show_more": "Show more",
|
||||
"show_less": "Show less",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"disable": "Disable",
|
||||
"enable": "Enable",
|
||||
"confirm": "Confirm",
|
||||
"verify": "Verify"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "Crop picture",
|
||||
|
@ -48,7 +52,15 @@
|
|||
"placeholder": "e.g. lain",
|
||||
"register": "Register",
|
||||
"username": "Username",
|
||||
"hint": "Log in to join the discussion"
|
||||
"hint": "Log in to join the discussion",
|
||||
"authentication_code": "Authentication code",
|
||||
"enter_recovery_code": "Enter a recovery code",
|
||||
"enter_two_factor_code": "Enter a two-factor code",
|
||||
"recovery_code": "Recovery code",
|
||||
"heading" : {
|
||||
"totp" : "Two-factor authentication",
|
||||
"recovery" : "Two-factor recovery"
|
||||
}
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "Previous",
|
||||
|
@ -151,6 +163,29 @@
|
|||
},
|
||||
"settings": {
|
||||
"app_name": "App name",
|
||||
"security": "Security",
|
||||
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "Setup OTP",
|
||||
"wait_pre_setup_otp" : "presetting OTP",
|
||||
"confirm_and_enable" : "Confirm & enable OTP",
|
||||
"title": "Two-factor Authentication",
|
||||
"generate_new_recovery_codes" : "Generate new recovery codes",
|
||||
"warning_of_generate_new_codes" : "When you generate new recovery codes, your old codes won’t work anymore.",
|
||||
"recovery_codes" : "Recovery codes.",
|
||||
"waiting_a_recovery_codes": "Receiving backup codes...",
|
||||
"recovery_codes_warning" : "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.",
|
||||
"authentication_methods" : "Authentication methods",
|
||||
"scan": {
|
||||
"title": "Scan",
|
||||
"desc": "Using your two-factor app, scan this QR code or enter text key:",
|
||||
"secret_code": "Key"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "To enable two-factor authentication, enter the code from your two-factor app:"
|
||||
}
|
||||
},
|
||||
"attachmentRadius": "Attachments",
|
||||
"attachments": "Attachments",
|
||||
"autoload": "Enable automatic loading when scrolled to the bottom",
|
||||
|
|
144
src/i18n/ja.json
144
src/i18n/ja.json
|
@ -2,6 +2,10 @@
|
|||
"chat": {
|
||||
"title": "チャット"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "エクスポート",
|
||||
"processing": "おまちください。しばらくすると、あなたのファイルをダウンロードするように、メッセージがでます。"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "チャット",
|
||||
"gopher": "Gopher",
|
||||
|
@ -19,7 +23,22 @@
|
|||
"apply": "てきよう",
|
||||
"submit": "そうしん",
|
||||
"more": "つづき",
|
||||
"generic_error": "エラーになりました"
|
||||
"generic_error": "エラーになりました",
|
||||
"optional": "かかなくてもよい",
|
||||
"show_more": "つづきをみる",
|
||||
"show_less": "たたむ",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "がぞうをきりぬく",
|
||||
"save": "セーブ",
|
||||
"save_without_cropping": "きりぬかずにセーブ",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "そうしん",
|
||||
"success": "インポートできました。",
|
||||
"error": "インポートがエラーになりました。"
|
||||
},
|
||||
"login": {
|
||||
"login": "ログイン",
|
||||
|
@ -31,12 +50,17 @@
|
|||
"username": "ユーザーめい",
|
||||
"hint": "はなしあいにくわわるには、ログインしてください"
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "まえ",
|
||||
"next": "つぎ"
|
||||
},
|
||||
"nav": {
|
||||
"about": "これはなに?",
|
||||
"back": "もどる",
|
||||
"chat": "ローカルチャット",
|
||||
"friend_requests": "フォローリクエスト",
|
||||
"mentions": "メンション",
|
||||
"interactions": "やりとり",
|
||||
"dms": "ダイレクトメッセージ",
|
||||
"public_tl": "パブリックタイムライン",
|
||||
"timeline": "タイムライン",
|
||||
|
@ -55,18 +79,33 @@
|
|||
"repeated_you": "あなたのステータスがリピートされました",
|
||||
"no_more_notifications": "つうちはありません"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "リピートとおきにいり",
|
||||
"follows": "あたらしいフォロー",
|
||||
"load_older": "ふるいやりとりをみる"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "とうこうする",
|
||||
"account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、だれでも、フォロワーげんていのステータスをよむことができます。",
|
||||
"account_not_locked_warning_link": "ロックされたアカウント",
|
||||
"attachments_sensitive": "ファイルをNSFWにする",
|
||||
"content_type": {
|
||||
"text/plain": "プレーンテキスト"
|
||||
"text/plain": "プレーンテキスト",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "せつめい (かかなくてもよい)",
|
||||
"default": "はねだくうこうに、つきました。",
|
||||
"direct_warning_to_all": "このとうこうは、メンションされたすべてのユーザーが、みることができます。",
|
||||
"direct_warning_to_first_only": "このとうこうは、メッセージのはじめでメンションされたユーザーだけが、みることができます。",
|
||||
"direct_warning": "このステータスは、メンションされたユーザーだけが、よむことができます。",
|
||||
"posting": "とうこう",
|
||||
"scope_notice": {
|
||||
"public": "このとうこうは、だれでもみることができます",
|
||||
"private": "このとうこうは、あなたのフォロワーだけが、みることができます",
|
||||
"unlisted": "このとうこうは、パブリックタイムラインと、つながっているすべてのネットワークでは、みることができません"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "ダイレクト: メンションされたユーザーのみにとどきます。",
|
||||
"private": "フォロワーげんてい: フォロワーのみにとどきます。",
|
||||
|
@ -83,6 +122,9 @@
|
|||
"token": "しょうたいトークン",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "もじがよめないときは、がぞうをクリックすると、あたらしいがぞうになります",
|
||||
"username_placeholder": "れい: lain",
|
||||
"fullname_placeholder": "れい: いわくら れいん",
|
||||
"bio_placeholder": "れい:\nごきげんよう。わたしはれいん。\nわたしはアニメのおんなのこで、にほんのベッドタウンにすんでいます。ワイヤードで、わたしにあったことが、あるかもしれませんね。",
|
||||
"validations": {
|
||||
"username_required": "なにかかいてください",
|
||||
"fullname_required": "なにかかいてください",
|
||||
|
@ -92,7 +134,11 @@
|
|||
"password_confirmation_match": "パスワードがちがいます"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "すべてえらぶ"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "アプリのなまえ",
|
||||
"attachmentRadius": "ファイル",
|
||||
"attachments": "ファイル",
|
||||
"autoload": "したにスクロールしたとき、じどうてきによみこむ。",
|
||||
|
@ -101,6 +147,12 @@
|
|||
"avatarRadius": "アバター",
|
||||
"background": "バックグラウンド",
|
||||
"bio": "プロフィール",
|
||||
"block_export": "ブロックのエクスポート",
|
||||
"block_export_button": "ブロックをCSVファイルにエクスポート",
|
||||
"block_import": "ブロックのインポート",
|
||||
"block_import_error": "ブロックのインポートがエラーになりました",
|
||||
"blocks_imported": "ブロックをインポートしました! じっさいにブロックするまでには、もうしばらくかかります。",
|
||||
"blocks_tab": "ブロック",
|
||||
"btnRadius": "ボタン",
|
||||
"cBlue": "リプライとフォロー",
|
||||
"cGreen": "リピート",
|
||||
|
@ -135,12 +187,15 @@
|
|||
"general": "ぜんぱん",
|
||||
"hide_attachments_in_convo": "スレッドのファイルをかくす",
|
||||
"hide_attachments_in_tl": "タイムラインのファイルをかくす",
|
||||
"hide_muted_posts": "ミュートしたユーザーのとうこうをかくす",
|
||||
"max_thumbnails": "ひとつのとうこうにいれられるサムネイルのかず",
|
||||
"hide_isp": "インスタンススペシフィックパネルをかくす",
|
||||
"preload_images": "がぞうをさきよみする",
|
||||
"use_one_click_nsfw": "NSFWなファイルを1クリックでひらく",
|
||||
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
|
||||
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
|
||||
"hide_filtered_statuses": "フィルターされたとうこうをかくす",
|
||||
"import_blocks_from_a_csv_file": "CSVファイルからブロックをインポートする",
|
||||
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
||||
"import_theme": "ロード",
|
||||
"inputRadius": "インプットフィールド",
|
||||
|
@ -155,6 +210,7 @@
|
|||
"lock_account_description": "あなたがみとめたひとだけ、あなたのアカウントをフォローできる",
|
||||
"loop_video": "ビデオをくりかえす",
|
||||
"loop_video_silent_only": "おとのないビデオだけくりかえす",
|
||||
"mutes_tab": "ミュート",
|
||||
"play_videos_in_modal": "ビデオをメディアビューアーでみる",
|
||||
"use_contain_fit": "がぞうのサムネイルを、きりぬかない",
|
||||
"name": "なまえ",
|
||||
|
@ -166,16 +222,18 @@
|
|||
"notification_visibility_mentions": "メンション",
|
||||
"notification_visibility_repeats": "リピート",
|
||||
"no_rich_text_description": "リッチテキストをつかわない",
|
||||
"no_blocks": "ブロックしていません",
|
||||
"no_mutes": "ミュートしていません",
|
||||
"hide_follows_description": "フォローしているひとをみせない",
|
||||
"hide_followers_description": "フォロワーをみせない",
|
||||
"show_admin_badge": "アドミンのしるしをみる",
|
||||
"show_moderator_badge": "モデレーターのしるしをみる",
|
||||
"show_admin_badge": "アドミンのしるしをみせる",
|
||||
"show_moderator_badge": "モデレーターのしるしをみせる",
|
||||
"nsfw_clickthrough": "NSFWなファイルをかくす",
|
||||
"oauth_tokens": "OAuthトークン",
|
||||
"token": "トークン",
|
||||
"refresh_token": "トークンを更新",
|
||||
"valid_until": "まで有効",
|
||||
"revoke_token": "取り消す",
|
||||
"refresh_token": "トークンをリフレッシュ",
|
||||
"valid_until": "おわりのとき",
|
||||
"revoke_token": "とりけす",
|
||||
"panelRadius": "パネル",
|
||||
"pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる",
|
||||
"presets": "プリセット",
|
||||
|
@ -188,10 +246,14 @@
|
|||
"reply_visibility_all": "すべてのリプライをみる",
|
||||
"reply_visibility_following": "わたしにあてられたリプライと、フォローしているひとからのリプライをみる",
|
||||
"reply_visibility_self": "わたしにあてられたリプライをみる",
|
||||
"autohide_floating_post_button": "あたらしいとうこうのボタンを、じどうてきにかくす (モバイル)",
|
||||
"saving_err": "せっていをセーブできませんでした",
|
||||
"saving_ok": "せっていをセーブしました",
|
||||
"search_user_to_block": "ブロックしたいひとを、ここでけんさくできます",
|
||||
"search_user_to_mute": "ミュートしたいひとを、ここでけんさくできます",
|
||||
"security_tab": "セキュリティ",
|
||||
"scope_copy": "リプライするとき、こうかいはんいをコピーする (DMのこうかいはんいは、つねにコピーされます)",
|
||||
"minimal_scopes_mode": "こうかいはんいせんたくオプションを、ちいさくする",
|
||||
"set_new_avatar": "あたらしいアバターをせっていする",
|
||||
"set_new_profile_background": "あたらしいプロフィールのバックグラウンドをせっていする",
|
||||
"set_new_profile_banner": "あたらしいプロフィールバナーを設定する",
|
||||
|
@ -209,6 +271,7 @@
|
|||
"theme_help": "カラーテーマをカスタマイズできます",
|
||||
"theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、いろと、とうめいどを、オーバーライドできます。「すべてクリア」ボタンをおすと、すべてのオーバーライドを、やめます。",
|
||||
"theme_help_v2_2": "バックグラウンドとテキストのコントラストをあらわすアイコンがあります。マウスをホバーすると、くわしいせつめいがでます。とうめいないろをつかっているときは、もっともわるいばあいのコントラストがしめされます。",
|
||||
"upload_a_photo": "がぞうをアップロード",
|
||||
"tooltipRadius": "ツールチップとアラート",
|
||||
"user_settings": "ユーザーせってい",
|
||||
"values": {
|
||||
|
@ -216,6 +279,13 @@
|
|||
"true": "はい"
|
||||
},
|
||||
"notifications": "つうち",
|
||||
"notification_setting": "つうちをうけとる:",
|
||||
"notification_setting_follows": "あなたがフォローしているひとから",
|
||||
"notification_setting_non_follows": "あなたがフォローしていないひとから",
|
||||
"notification_setting_followers": "あなたをフォローしているひとから",
|
||||
"notification_setting_non_followers": "あなたをフォローしていないひとから",
|
||||
"notification_mutes": "あるユーザーからのつうちをとめるには、ミュートしてください。",
|
||||
"notification_blocks": "ブロックしているユーザーからのつうちは、すべてとまります。",
|
||||
"enable_web_push_notifications": "ウェブプッシュつうちをゆるす",
|
||||
"style": {
|
||||
"switcher": {
|
||||
|
@ -325,6 +395,11 @@
|
|||
"checkbox": "りようきやくを、よみました",
|
||||
"link": "ハイパーリンク"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"title": "バージョン",
|
||||
"backend_version": "バックエンドのバージョン",
|
||||
"frontend_version": "フロントエンドのバージョン"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
@ -370,7 +445,19 @@
|
|||
"repeated": "リピート",
|
||||
"show_new": "よみこみ",
|
||||
"up_to_date": "さいしん",
|
||||
"no_more_statuses": "これでおわりです"
|
||||
"no_more_statuses": "これでおわりです",
|
||||
"no_statuses": "ありません"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "おきにいり",
|
||||
"repeats": "リピート",
|
||||
"delete": "ステータスをけす",
|
||||
"pin": "プロフィールにピンどめする",
|
||||
"unpin": "プロフィールにピンどめするのをやめる",
|
||||
"pinned": "ピンどめ",
|
||||
"delete_confirm": "ほんとうに、このステータスを、けしてもいいですか?",
|
||||
"reply_to": "へんしん:",
|
||||
"replies_list": "へんしん:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "うけいれ",
|
||||
|
@ -393,10 +480,47 @@
|
|||
"muted": "ミュートしています!",
|
||||
"per_day": "/日",
|
||||
"remote_follow": "リモートフォロー",
|
||||
"statuses": "ステータス"
|
||||
"report": "つうほう",
|
||||
"statuses": "ステータス",
|
||||
"unblock": "ブロックをやめる",
|
||||
"unblock_progress": "ブロックをとりけしています...",
|
||||
"block_progress": "ブロックしています...",
|
||||
"unmute": "ミュートをやめる",
|
||||
"unmute_progress": "ミュートをとりけしています...",
|
||||
"mute_progress": "ミュートしています...",
|
||||
"admin_menu": {
|
||||
"moderation": "モデレーション",
|
||||
"grant_admin": "アドミンにする",
|
||||
"revoke_admin": "アドミンをやめさせる",
|
||||
"grant_moderator": "モデレーターにする",
|
||||
"revoke_moderator": "モデレーターをやめさせる",
|
||||
"activate_account": "アカウントをアクティブにする",
|
||||
"deactivate_account": "アカウントをアクティブでなくする",
|
||||
"delete_account": "アカウントをけす",
|
||||
"force_nsfw": "すべてのとうこうをNSFWにする",
|
||||
"strip_media": "とうこうからメディアをなくす",
|
||||
"force_unlisted": "とうこうをアンリステッドにする",
|
||||
"sandbox": "とうこうをフォロワーのみにする",
|
||||
"disable_remote_subscription": "ほかのインスタンスからフォローされないようにする",
|
||||
"disable_any_subscription": "フォローされないようにする",
|
||||
"quarantine": "ほかのインスタンスのユーザーのとうこうをとめる",
|
||||
"delete_user": "ユーザーをけす",
|
||||
"delete_user_confirmation": "あなたは、ほんとうに、きはたしかですか? これは、とりけすことが、できません。"
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "ユーザータイムライン"
|
||||
"timeline_title": "ユーザータイムライン",
|
||||
"profile_does_not_exist": "ごめんなさい。このプロフィールは、そんざいしません。",
|
||||
"profile_loading_error": "ごめんなさい。プロフィールのロードがエラーになりました。"
|
||||
},
|
||||
"user_reporting": {
|
||||
"title": "つうほうする: {0}",
|
||||
"add_comment_description": "このつうほうは、あなたのインスタンスのモデレーターに、おくられます。このアカウントを、つうほうするりゆうを、せつめいすることができます:",
|
||||
"additional_comments": "ついかのコメント",
|
||||
"forward_description": "このアカウントは、ほかのインスタンスのものです。そのインスタンスにも、このつうほうのコピーを、おくりますか?",
|
||||
"forward_to": "コピーをおくる: {0}",
|
||||
"submit": "そうしん",
|
||||
"generic_error": "あなたのリクエストをうけつけようとしましたが、エラーになってしまいました。"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "くわしく",
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
"chat": {
|
||||
"title": "チャット"
|
||||
},
|
||||
"exporter": {
|
||||
"export": "エクスポート",
|
||||
"processing": "処理中です。処理が完了すると、ファイルをダウンロードするよう指示があります。"
|
||||
},
|
||||
"features_panel": {
|
||||
"chat": "チャット",
|
||||
"gopher": "Gopher",
|
||||
|
@ -19,7 +23,22 @@
|
|||
"apply": "適用",
|
||||
"submit": "送信",
|
||||
"more": "続き",
|
||||
"generic_error": "エラーになりました"
|
||||
"generic_error": "エラーになりました",
|
||||
"optional": "省略可",
|
||||
"show_more": "もっと見る",
|
||||
"show_less": "たたむ",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"image_cropper": {
|
||||
"crop_picture": "画像を切り抜く",
|
||||
"save": "保存",
|
||||
"save_without_cropping": "切り抜かずに保存",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"importer": {
|
||||
"submit": "送信",
|
||||
"success": "正常にインポートされました。",
|
||||
"error": "このファイルをインポートするとき、エラーが発生しました。"
|
||||
},
|
||||
"login": {
|
||||
"login": "ログイン",
|
||||
|
@ -31,12 +50,17 @@
|
|||
"username": "ユーザー名",
|
||||
"hint": "会話に加わるには、ログインしてください"
|
||||
},
|
||||
"media_modal": {
|
||||
"previous": "前",
|
||||
"next": "次"
|
||||
},
|
||||
"nav": {
|
||||
"about": "このインスタンスについて",
|
||||
"back": "戻る",
|
||||
"chat": "ローカルチャット",
|
||||
"friend_requests": "フォローリクエスト",
|
||||
"mentions": "通知",
|
||||
"interactions": "インタラクション",
|
||||
"dms": "ダイレクトメッセージ",
|
||||
"public_tl": "パブリックタイムライン",
|
||||
"timeline": "タイムライン",
|
||||
|
@ -55,18 +79,33 @@
|
|||
"repeated_you": "あなたのステータスがリピートされました",
|
||||
"no_more_notifications": "通知はありません"
|
||||
},
|
||||
"interactions": {
|
||||
"favs_repeats": "リピートとお気に入り",
|
||||
"follows": "新しいフォロワー",
|
||||
"load_older": "古いインタラクションを見る"
|
||||
},
|
||||
"post_status": {
|
||||
"new_status": "投稿する",
|
||||
"account_not_locked_warning": "あなたのアカウントは {0} ではありません。あなたをフォローすれば、誰でも、フォロワー限定のステータスを読むことができます。",
|
||||
"account_not_locked_warning_link": "ロックされたアカウント",
|
||||
"attachments_sensitive": "ファイルをNSFWにする",
|
||||
"content_type": {
|
||||
"text/plain": "プレーンテキスト"
|
||||
"text/plain": "プレーンテキスト",
|
||||
"text/html": "HTML",
|
||||
"text/markdown": "Markdown",
|
||||
"text/bbcode": "BBCode"
|
||||
},
|
||||
"content_warning": "説明 (省略可)",
|
||||
"default": "羽田空港に着きました。",
|
||||
"direct_warning_to_all": "この投稿は、メンションされたすべてのユーザーが、見ることができます。",
|
||||
"direct_warning_to_first_only": "この投稿は、メッセージの冒頭でメンションされたユーザーだけが、見ることができます。",
|
||||
"direct_warning": "このステータスは、メンションされたユーザーだけが、読むことができます。",
|
||||
"posting": "投稿",
|
||||
"scope_notice": {
|
||||
"public": "この投稿は、誰でも見ることができます",
|
||||
"private": "この投稿は、あなたのフォロワーだけが、見ることができます。",
|
||||
"unlisted": "この投稿は、パブリックタイムラインと、接続しているすべてのネットワークには、表示されません。"
|
||||
},
|
||||
"scope": {
|
||||
"direct": "ダイレクト: メンションされたユーザーのみに届きます。",
|
||||
"private": "フォロワーげんてい: フォロワーのみに届きます。",
|
||||
|
@ -83,6 +122,9 @@
|
|||
"token": "招待トークン",
|
||||
"captcha": "CAPTCHA",
|
||||
"new_captcha": "文字が読めないときは、画像をクリックすると、新しい画像になります",
|
||||
"username_placeholder": "例: lain",
|
||||
"fullname_placeholder": "例: 岩倉玲音",
|
||||
"bio_placeholder": "例:\nこんにちは。私は玲音。\n私はアニメのキャラクターで、日本の郊外に住んでいます。私をWiredで見たことがあるかもしれません。",
|
||||
"validations": {
|
||||
"username_required": "必須",
|
||||
"fullname_required": "必須",
|
||||
|
@ -92,7 +134,11 @@
|
|||
"password_confirmation_match": "パスワードが違います"
|
||||
}
|
||||
},
|
||||
"selectable_list": {
|
||||
"select_all": "すべて選択"
|
||||
},
|
||||
"settings": {
|
||||
"app_name": "アプリの名称",
|
||||
"attachmentRadius": "ファイル",
|
||||
"attachments": "ファイル",
|
||||
"autoload": "下にスクロールしたとき、自動的に読み込む。",
|
||||
|
@ -101,6 +147,12 @@
|
|||
"avatarRadius": "アバター",
|
||||
"background": "バックグラウンド",
|
||||
"bio": "プロフィール",
|
||||
"block_export": "ブロックのエクスポート",
|
||||
"block_export_button": "ブロックをCSVファイルにエクスポートする",
|
||||
"block_import": "ブロックのインポート",
|
||||
"block_import_error": "ブロックのインポートに失敗しました",
|
||||
"blocks_imported": "ブロックをインポートしました! 実際に処理されるまでに、しばらく時間がかかります。",
|
||||
"blocks_tab": "ブロック",
|
||||
"btnRadius": "ボタン",
|
||||
"cBlue": "返信とフォロー",
|
||||
"cGreen": "リピート",
|
||||
|
@ -128,19 +180,22 @@
|
|||
"follow_export": "フォローのエクスポート",
|
||||
"follow_export_button": "エクスポート",
|
||||
"follow_export_processing": "お待ちください。まもなくファイルをダウンロードできます。",
|
||||
"follow_import": "フォローインポート",
|
||||
"follow_import": "フォローのインポート",
|
||||
"follow_import_error": "フォローのインポートがエラーになりました。",
|
||||
"follows_imported": "フォローがインポートされました! 少し時間がかかるかもしれません。",
|
||||
"foreground": "フォアグラウンド",
|
||||
"general": "全般",
|
||||
"hide_attachments_in_convo": "スレッドのファイルを隠す",
|
||||
"hide_attachments_in_tl": "タイムラインのファイルを隠す",
|
||||
"hide_muted_posts": "ミュートしているユーザーの投稿を隠す",
|
||||
"max_thumbnails": "投稿に含まれるサムネイルの最大数",
|
||||
"hide_isp": "インスタンス固有パネルを隠す",
|
||||
"preload_images": "画像を先読みする",
|
||||
"use_one_click_nsfw": "NSFWなファイルを1クリックで開く",
|
||||
"hide_post_stats": "投稿の統計を隠す (例: お気に入りの数)",
|
||||
"hide_user_stats": "ユーザーの統計を隠す (例: フォロワーの数)",
|
||||
"hide_filtered_statuses": "フィルターされた投稿を隠す",
|
||||
"import_blocks_from_a_csv_file": "CSVファイルからブロックをインポートする",
|
||||
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
|
||||
"import_theme": "ロード",
|
||||
"inputRadius": "インプットフィールド",
|
||||
|
@ -155,6 +210,7 @@
|
|||
"lock_account_description": "あなたが認めた人だけ、あなたのアカウントをフォローできる",
|
||||
"loop_video": "ビデオを繰り返す",
|
||||
"loop_video_silent_only": "音のないビデオだけ繰り返す",
|
||||
"mutes_tab": "ミュート",
|
||||
"play_videos_in_modal": "ビデオをメディアビューアーで見る",
|
||||
"use_contain_fit": "画像のサムネイルを、切り抜かない",
|
||||
"name": "名前",
|
||||
|
@ -166,6 +222,8 @@
|
|||
"notification_visibility_mentions": "メンション",
|
||||
"notification_visibility_repeats": "リピート",
|
||||
"no_rich_text_description": "リッチテキストを使わない",
|
||||
"no_blocks": "ブロックはありません",
|
||||
"no_mutes": "ミュートはありません",
|
||||
"hide_follows_description": "フォローしている人を見せない",
|
||||
"hide_followers_description": "フォロワーを見せない",
|
||||
"show_admin_badge": "管理者のバッジを見せる",
|
||||
|
@ -188,10 +246,14 @@
|
|||
"reply_visibility_all": "すべてのリプライを見る",
|
||||
"reply_visibility_following": "私に宛てられたリプライと、フォローしている人からのリプライを見る",
|
||||
"reply_visibility_self": "私に宛てられたリプライを見る",
|
||||
"autohide_floating_post_button": "新しい投稿ボタンを自動的に隠す (モバイル)",
|
||||
"saving_err": "設定を保存できませんでした",
|
||||
"saving_ok": "設定を保存しました",
|
||||
"search_user_to_block": "ブロックしたいユーザーを検索",
|
||||
"search_user_to_mute": "ミュートしたいユーザーを検索",
|
||||
"security_tab": "セキュリティ",
|
||||
"scope_copy": "返信するとき、公開範囲をコピーする (DMの公開範囲は、常にコピーされます)",
|
||||
"minimal_scopes_mode": "公開範囲選択オプションを最小にする",
|
||||
"set_new_avatar": "新しいアバターを設定する",
|
||||
"set_new_profile_background": "新しいプロフィールのバックグラウンドを設定する",
|
||||
"set_new_profile_banner": "新しいプロフィールバナーを設定する",
|
||||
|
@ -210,12 +272,20 @@
|
|||
"theme_help_v2_1": "チェックボックスをONにすると、コンポーネントごとに、色と透明度をオーバーライドできます。「すべてクリア」ボタンを押すと、すべてのオーバーライドをやめます。",
|
||||
"theme_help_v2_2": "バックグラウンドとテキストのコントラストを表すアイコンがあります。マウスをホバーすると、詳しい説明が出ます。透明な色を使っているときは、最悪の場合のコントラストが示されます。",
|
||||
"tooltipRadius": "ツールチップとアラート",
|
||||
"upload_a_photo": "画像をアップロード",
|
||||
"user_settings": "ユーザー設定",
|
||||
"values": {
|
||||
"false": "いいえ",
|
||||
"true": "はい"
|
||||
},
|
||||
"notifications": "通知",
|
||||
"notification_setting": "通知を受け取る:",
|
||||
"notification_setting_follows": "あなたがフォローしているユーザーから",
|
||||
"notification_setting_non_follows": "あなたがフォローしていないユーザーから",
|
||||
"notification_setting_followers": "あなたをフォローしているユーザーから",
|
||||
"notification_setting_non_followers": "あなたをフォローしていないユーザーから",
|
||||
"notification_mutes": "特定のユーザーからの通知を止めるには、ミュートしてください。",
|
||||
"notification_blocks": "ブロックしているユーザーからの通知は、すべて止まります。",
|
||||
"enable_web_push_notifications": "ウェブプッシュ通知を許可する",
|
||||
"style": {
|
||||
"switcher": {
|
||||
|
@ -325,6 +395,11 @@
|
|||
"checkbox": "利用規約を読みました",
|
||||
"link": "ハイパーリンク"
|
||||
}
|
||||
},
|
||||
"version": {
|
||||
"title": "バージョン",
|
||||
"backend_version": "バックエンドのバージョン",
|
||||
"frontend_version": "フロントエンドのバージョン"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
|
@ -370,7 +445,19 @@
|
|||
"repeated": "リピート",
|
||||
"show_new": "読み込み",
|
||||
"up_to_date": "最新",
|
||||
"no_more_statuses": "これで終わりです"
|
||||
"no_more_statuses": "これで終わりです",
|
||||
"no_statuses": "ステータスはありません"
|
||||
},
|
||||
"status": {
|
||||
"favorites": "お気に入り",
|
||||
"repeats": "リピート",
|
||||
"delete": "ステータスを削除",
|
||||
"pin": "プロフィールにピン留め",
|
||||
"unpin": "プロフィールのピン留めを外す",
|
||||
"pinned": "ピン留め",
|
||||
"delete_confirm": "本当にこのステータスを削除してもよろしいですか?",
|
||||
"reply_to": "返信",
|
||||
"replies_list": "返信:"
|
||||
},
|
||||
"user_card": {
|
||||
"approve": "受け入れ",
|
||||
|
@ -393,10 +480,47 @@
|
|||
"muted": "ミュートしています!",
|
||||
"per_day": "/日",
|
||||
"remote_follow": "リモートフォロー",
|
||||
"statuses": "ステータス"
|
||||
"report": "通報",
|
||||
"statuses": "ステータス",
|
||||
"unblock": "ブロック解除",
|
||||
"unblock_progress": "ブロックを解除しています...",
|
||||
"block_progress": "ブロックしています...",
|
||||
"unmute": "ミュート解除",
|
||||
"unmute_progress": "ミュートを解除しています...",
|
||||
"mute_progress": "ミュートしています...",
|
||||
"admin_menu": {
|
||||
"moderation": "モデレーション",
|
||||
"grant_admin": "管理者権限を付与",
|
||||
"revoke_admin": "管理者権限を解除",
|
||||
"grant_moderator": "モデレーター権限を付与",
|
||||
"revoke_moderator": "モデレーター権限を解除",
|
||||
"activate_account": "アカウントをアクティブにする",
|
||||
"deactivate_account": "アカウントをアクティブでなくする",
|
||||
"delete_account": "アカウントを削除",
|
||||
"force_nsfw": "すべての投稿をNSFWにする",
|
||||
"strip_media": "投稿からメディアを除去する",
|
||||
"force_unlisted": "投稿を未収載にする",
|
||||
"sandbox": "投稿をフォロワーのみにする",
|
||||
"disable_remote_subscription": "他のインスタンスからフォローされないようにする",
|
||||
"disable_any_subscription": "フォローされないようにする",
|
||||
"quarantine": "他のインスタンスからの投稿を止める",
|
||||
"delete_user": "ユーザーを削除",
|
||||
"delete_user_confirmation": "あなたの精神状態に何か問題はございませんか? この操作を取り消すことはできません。"
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"timeline_title": "ユーザータイムライン"
|
||||
"timeline_title": "ユーザータイムライン",
|
||||
"profile_does_not_exist": "申し訳ない。このプロフィールは存在しません。",
|
||||
"profile_loading_error": "申し訳ない。プロフィールの読み込みがエラーになりました。"
|
||||
},
|
||||
"user_reporting": {
|
||||
"title": "通報する: {0}",
|
||||
"add_comment_description": "この通報は、あなたのインスタンスのモデレーターに送られます。このアカウントを通報する理由を説明することができます:",
|
||||
"additional_comments": "追加のコメント",
|
||||
"forward_description": "このアカウントは他のサーバーに置かれています。この通報のコピーをリモートのサーバーに送りますか?",
|
||||
"forward_to": "転送する: {0}",
|
||||
"submit": "送信",
|
||||
"generic_error": "あなたのリクエストを処理しようとしましたが、エラーになりました。"
|
||||
},
|
||||
"who_to_follow": {
|
||||
"more": "詳細",
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
"general": {
|
||||
"apply": "Применить",
|
||||
"submit": "Отправить",
|
||||
"cancel": "Отмена"
|
||||
"cancel": "Отмена",
|
||||
"disable": "Оключить",
|
||||
"enable": "Включить",
|
||||
"confirm": "Подтвердить",
|
||||
"verify": "Проверить"
|
||||
},
|
||||
"login": {
|
||||
"login": "Войти",
|
||||
|
@ -17,7 +21,15 @@
|
|||
"password": "Пароль",
|
||||
"placeholder": "e.c. lain",
|
||||
"register": "Зарегистрироваться",
|
||||
"username": "Имя пользователя"
|
||||
"username": "Имя пользователя",
|
||||
"authentication_code": "Код аутентификации",
|
||||
"enter_recovery_code": "Ввести код восстановления",
|
||||
"enter_two_factor_code": "Ввести код аутентификации",
|
||||
"recovery_code": "Код восстановления",
|
||||
"heading" : {
|
||||
"TotpForm" : "Двухфакторная аутентификация",
|
||||
"RecoveryForm" : "Two-factor recovery"
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"back": "Назад",
|
||||
|
@ -79,6 +91,28 @@
|
|||
}
|
||||
},
|
||||
"settings": {
|
||||
"enter_current_password_to_confirm": "Введите свой текущий пароль",
|
||||
"mfa": {
|
||||
"otp" : "OTP",
|
||||
"setup_otp" : "Настройка OTP",
|
||||
"wait_pre_setup_otp" : "предварительная настройка OTP",
|
||||
"confirm_and_enable" : "Подтвердить и включить OTP",
|
||||
"title": "Двухфакторная аутентификация",
|
||||
"generate_new_recovery_codes" : "Получить новые коды востановления",
|
||||
"warning_of_generate_new_codes" : "После получения новых кодов восстановления, старые больше не будут работать.",
|
||||
"recovery_codes" : "Коды восстановления.",
|
||||
"waiting_a_recovery_codes": "Получение кодов восстановления ...",
|
||||
"recovery_codes_warning" : "Запишите эти коды и держите в безопасном месте - иначе вы их больше не увидите. Если вы потеряете доступ к OTP приложению - без резервных кодов вы больше не сможете залогиниться.",
|
||||
"authentication_methods" : "Методы аутентификации",
|
||||
"scan": {
|
||||
"title": "Сканирование",
|
||||
"desc": "Используйте приложение для двухэтапной аутентификации для сканирования этого QR-код или введите текстовый ключ:",
|
||||
"secret_code": "Ключ"
|
||||
},
|
||||
"verify": {
|
||||
"desc": "Чтобы включить двухэтапную аутентификации, введите код из вашего приложение для двухэтапной аутентификации:"
|
||||
}
|
||||
},
|
||||
"attachmentRadius": "Прикреплённые файлы",
|
||||
"attachments": "Вложения",
|
||||
"autoload": "Включить автоматическую загрузку при прокрутке вниз",
|
||||
|
|
|
@ -10,6 +10,7 @@ import apiModule from './modules/api.js'
|
|||
import configModule from './modules/config.js'
|
||||
import chatModule from './modules/chat.js'
|
||||
import oauthModule from './modules/oauth.js'
|
||||
import authFlowModule from './modules/auth_flow.js'
|
||||
import mediaViewerModule from './modules/media_viewer.js'
|
||||
import oauthTokensModule from './modules/oauth_tokens.js'
|
||||
import reportsModule from './modules/reports.js'
|
||||
|
@ -23,6 +24,7 @@ import messages from './i18n/messages.js'
|
|||
|
||||
import VueChatScroll from 'vue-chat-scroll'
|
||||
import VueClickOutside from 'v-click-outside'
|
||||
import PortalVue from 'portal-vue'
|
||||
|
||||
import afterStoreSetup from './boot/after_store.js'
|
||||
|
||||
|
@ -33,6 +35,7 @@ Vue.use(VueRouter)
|
|||
Vue.use(VueI18n)
|
||||
Vue.use(VueChatScroll)
|
||||
Vue.use(VueClickOutside)
|
||||
Vue.use(PortalVue)
|
||||
|
||||
const i18n = new VueI18n({
|
||||
// By default, use the browser locale, we will update it if neccessary
|
||||
|
@ -66,6 +69,7 @@ const persistedStateOptions = {
|
|||
config: configModule,
|
||||
chat: chatModule,
|
||||
oauth: oauthModule,
|
||||
authFlow: authFlowModule,
|
||||
mediaViewer: mediaViewerModule,
|
||||
oauthTokens: oauthTokensModule,
|
||||
reports: reportsModule
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
const PASSWORD_STRATEGY = 'password'
|
||||
const TOKEN_STRATEGY = 'token'
|
||||
|
||||
// MFA strategies
|
||||
const TOTP_STRATEGY = 'totp'
|
||||
const RECOVERY_STRATEGY = 'recovery'
|
||||
|
||||
// initial state
|
||||
const state = {
|
||||
app: null,
|
||||
settings: {},
|
||||
strategy: PASSWORD_STRATEGY,
|
||||
initStrategy: PASSWORD_STRATEGY // default strategy from config
|
||||
}
|
||||
|
||||
const resetState = (state) => {
|
||||
state.strategy = state.initStrategy
|
||||
state.settings = {}
|
||||
state.app = null
|
||||
}
|
||||
|
||||
// getters
|
||||
const getters = {
|
||||
app: (state, getters) => {
|
||||
return state.app
|
||||
},
|
||||
settings: (state, getters) => {
|
||||
return state.settings
|
||||
},
|
||||
requiredPassword: (state, getters, rootState) => {
|
||||
return state.strategy === PASSWORD_STRATEGY
|
||||
},
|
||||
requiredToken: (state, getters, rootState) => {
|
||||
return state.strategy === TOKEN_STRATEGY
|
||||
},
|
||||
requiredTOTP: (state, getters, rootState) => {
|
||||
return state.strategy === TOTP_STRATEGY
|
||||
},
|
||||
requiredRecovery: (state, getters, rootState) => {
|
||||
return state.strategy === RECOVERY_STRATEGY
|
||||
}
|
||||
}
|
||||
|
||||
// mutations
|
||||
const mutations = {
|
||||
setInitialStrategy (state, strategy) {
|
||||
if (strategy) {
|
||||
state.initStrategy = strategy
|
||||
state.strategy = strategy
|
||||
}
|
||||
},
|
||||
requirePassword (state) {
|
||||
state.strategy = PASSWORD_STRATEGY
|
||||
},
|
||||
requireToken (state) {
|
||||
state.strategy = TOKEN_STRATEGY
|
||||
},
|
||||
requireMFA (state, {app, settings}) {
|
||||
state.settings = settings
|
||||
state.app = app
|
||||
state.strategy = TOTP_STRATEGY // default strategy of MFA
|
||||
},
|
||||
requireRecovery (state) {
|
||||
state.strategy = RECOVERY_STRATEGY
|
||||
},
|
||||
requireTOTP (state) {
|
||||
state.strategy = TOTP_STRATEGY
|
||||
},
|
||||
abortMFA (state) {
|
||||
resetState(state)
|
||||
}
|
||||
}
|
||||
|
||||
// actions
|
||||
const actions = {
|
||||
async login ({state, dispatch, commit}, {access_token}) {
|
||||
commit('setToken', access_token, { root: true })
|
||||
await dispatch('loginUser', access_token, { root: true })
|
||||
resetState(state)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
}
|
|
@ -27,7 +27,6 @@ const defaultState = {
|
|||
scopeCopy: true,
|
||||
subjectLineBehavior: 'email',
|
||||
postContentType: 'text/plain',
|
||||
loginMethod: 'password',
|
||||
nsfwCensorImage: undefined,
|
||||
vapidPublicKey: undefined,
|
||||
noAttachmentLinks: false,
|
||||
|
|
|
@ -1,16 +1,39 @@
|
|||
const oauth = {
|
||||
state: {
|
||||
client_id: false,
|
||||
client_secret: false,
|
||||
token: false
|
||||
clientId: false,
|
||||
clientSecret: false,
|
||||
/* App token is authentication for app without any user, used mostly for
|
||||
* MastoAPI's registration of new users, stored so that we can fall back to
|
||||
* it on logout
|
||||
*/
|
||||
appToken: false,
|
||||
/* User token is authentication for app with user, this is for every calls
|
||||
* that need authorized user to be successful (i.e. posting, liking etc)
|
||||
*/
|
||||
userToken: false
|
||||
},
|
||||
mutations: {
|
||||
setClientData (state, data) {
|
||||
state.client_id = data.client_id
|
||||
state.client_secret = data.client_secret
|
||||
setClientData (state, { clientId, clientSecret }) {
|
||||
state.clientId = clientId
|
||||
state.clientSecret = clientSecret
|
||||
},
|
||||
setAppToken (state, token) {
|
||||
state.appToken = token
|
||||
},
|
||||
setToken (state, token) {
|
||||
state.token = token
|
||||
state.userToken = token
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
getToken: state => () => {
|
||||
// state.token is userToken with older name, coming from persistent state
|
||||
// added here for smoother transition, otherwise user will be logged out
|
||||
return state.userToken || state.token || state.appToken
|
||||
},
|
||||
getUserToken: state => () => {
|
||||
// state.token is userToken with older name, coming from persistent state
|
||||
// added here for smoother transition, otherwise user will be logged out
|
||||
return state.userToken || state.token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import userSearchApi from '../services/new_api/user_search.js'
|
|||
import { compact, map, each, merge, last, concat, uniq } from 'lodash'
|
||||
import { set } from 'vue'
|
||||
import { registerPushNotifications, unregisterPushNotifications } from '../services/push/push.js'
|
||||
import oauthApi from '../services/new_api/oauth'
|
||||
import { humanizeErrors } from './errors'
|
||||
|
||||
// TODO: Unify with mergeOrAdd in statuses.js
|
||||
|
@ -368,31 +367,21 @@ const users = {
|
|||
|
||||
let rootState = store.rootState
|
||||
|
||||
let response = await rootState.api.backendInteractor.register(userInfo)
|
||||
if (response.ok) {
|
||||
const data = {
|
||||
oauth: rootState.oauth,
|
||||
instance: rootState.instance.server
|
||||
}
|
||||
let app = await oauthApi.getOrCreateApp(data)
|
||||
let result = await oauthApi.getTokenWithCredentials({
|
||||
app,
|
||||
instance: data.instance,
|
||||
username: userInfo.username,
|
||||
password: userInfo.password
|
||||
})
|
||||
try {
|
||||
let data = await rootState.api.backendInteractor.register(userInfo)
|
||||
store.commit('signUpSuccess')
|
||||
store.commit('setToken', result.access_token)
|
||||
store.dispatch('loginUser', result.access_token)
|
||||
} else {
|
||||
const data = await response.json()
|
||||
let errors = JSON.parse(data.error)
|
||||
store.commit('setToken', data.access_token)
|
||||
store.dispatch('loginUser', data.access_token)
|
||||
} catch (e) {
|
||||
let errors = e.message
|
||||
// replace ap_id with username
|
||||
if (errors.ap_id) {
|
||||
errors.username = errors.ap_id
|
||||
delete errors.ap_id
|
||||
if (typeof errors === 'object') {
|
||||
if (errors.ap_id) {
|
||||
errors.username = errors.ap_id
|
||||
delete errors.ap_id
|
||||
}
|
||||
errors = humanizeErrors(errors)
|
||||
}
|
||||
errors = humanizeErrors(errors)
|
||||
store.commit('signUpFailure', errors)
|
||||
throw Error(errors)
|
||||
}
|
||||
|
@ -406,7 +395,7 @@ const users = {
|
|||
store.dispatch('disconnectFromChat')
|
||||
store.commit('setToken', false)
|
||||
store.dispatch('stopFetching', 'friends')
|
||||
store.commit('setBackendInteractor', backendInteractorService())
|
||||
store.commit('setBackendInteractor', backendInteractorService(store.getters.getToken()))
|
||||
store.dispatch('stopFetching', 'notifications')
|
||||
store.commit('clearNotifications')
|
||||
store.commit('resetStatuses')
|
||||
|
|
|
@ -4,8 +4,6 @@ import 'whatwg-fetch'
|
|||
import { StatusCodeError } from '../errors/errors'
|
||||
|
||||
/* eslint-env browser */
|
||||
const LOGIN_URL = '/api/account/verify_credentials.json'
|
||||
const REGISTRATION_URL = '/api/account/register.json'
|
||||
const BG_UPDATE_URL = '/api/qvitter/update_background_image.json'
|
||||
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
|
||||
const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications/read.json'
|
||||
|
@ -23,6 +21,16 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
|
|||
const SUGGESTIONS_URL = '/api/v1/suggestions'
|
||||
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
|
||||
|
||||
const MFA_SETTINGS_URL = '/api/pleroma/profile/mfa'
|
||||
const MFA_BACKUP_CODES_URL = '/api/pleroma/profile/mfa/backup_codes'
|
||||
|
||||
const MFA_SETUP_OTP_URL = '/api/pleroma/profile/mfa/setup/totp'
|
||||
const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp'
|
||||
const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp'
|
||||
|
||||
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
|
||||
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
|
||||
const GET_BACKGROUND_HACK = '/api/account/verify_credentials.json'
|
||||
const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
|
||||
const MASTODON_USER_NOTIFICATIONS_URL = '/api/v1/notifications'
|
||||
const MASTODON_FAVORITE_URL = id => `/api/v1/statuses/${id}/favourite`
|
||||
|
@ -175,19 +183,29 @@ const updateProfile = ({ credentials, params }) => {
|
|||
// homepage
|
||||
// location
|
||||
// token
|
||||
const register = (params) => {
|
||||
const form = new FormData()
|
||||
|
||||
each(params, (value, key) => {
|
||||
if (value) {
|
||||
form.append(key, value)
|
||||
}
|
||||
})
|
||||
|
||||
return fetch(REGISTRATION_URL, {
|
||||
const register = ({ params, credentials }) => {
|
||||
const { nickname, ...rest } = params
|
||||
return fetch(MASTODON_REGISTRATION_URL, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
headers: {
|
||||
...authHeaders(credentials),
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
nickname,
|
||||
locale: 'en_US',
|
||||
agreement: true,
|
||||
...rest
|
||||
})
|
||||
})
|
||||
.then((response) => [response.ok, response])
|
||||
.then(([ok, response]) => {
|
||||
if (ok) {
|
||||
return response.json()
|
||||
} else {
|
||||
return response.json().then((error) => { throw new Error(error) })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getCaptcha = () => fetch('/api/pleroma/captcha').then(resp => resp.json())
|
||||
|
@ -519,8 +537,7 @@ const fetchPinnedStatuses = ({ id, credentials }) => {
|
|||
}
|
||||
|
||||
const verifyCredentials = (user) => {
|
||||
return fetch(LOGIN_URL, {
|
||||
method: 'POST',
|
||||
return fetch(MASTODON_LOGIN_URL, {
|
||||
headers: authHeaders(user)
|
||||
})
|
||||
.then((response) => {
|
||||
|
@ -533,6 +550,26 @@ const verifyCredentials = (user) => {
|
|||
}
|
||||
})
|
||||
.then((data) => data.error ? data : parseUser(data))
|
||||
.then((mastoUser) => {
|
||||
// REMOVE WHEN BE SUPPORTS background_image
|
||||
return fetch(GET_BACKGROUND_HACK, {
|
||||
method: 'POST',
|
||||
headers: authHeaders(user)
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json()
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
})
|
||||
/* eslint-disable camelcase */
|
||||
.then(({ background_image }) => ({
|
||||
...mastoUser,
|
||||
background_image
|
||||
}))
|
||||
/* eslint-enable camelcase */
|
||||
})
|
||||
}
|
||||
|
||||
const favorite = ({ id, credentials }) => {
|
||||
|
@ -679,6 +716,51 @@ const changePassword = ({ credentials, password, newPassword, newPasswordConfirm
|
|||
.then((response) => response.json())
|
||||
}
|
||||
|
||||
const settingsMFA = ({ credentials }) => {
|
||||
return fetch(MFA_SETTINGS_URL, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'GET'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const mfaDisableOTP = ({ credentials, password }) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
|
||||
return fetch(MFA_DISABLE_OTP_URL, {
|
||||
body: form,
|
||||
method: 'DELETE',
|
||||
headers: authHeaders(credentials)
|
||||
})
|
||||
.then((response) => response.json())
|
||||
}
|
||||
|
||||
const mfaConfirmOTP = ({ credentials, password, token }) => {
|
||||
const form = new FormData()
|
||||
|
||||
form.append('password', password)
|
||||
form.append('code', token)
|
||||
|
||||
return fetch(MFA_CONFIRM_OTP_URL, {
|
||||
body: form,
|
||||
headers: authHeaders(credentials),
|
||||
method: 'POST'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const mfaSetupOTP = ({ credentials }) => {
|
||||
return fetch(MFA_SETUP_OTP_URL, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'GET'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const generateMfaBackupCodes = ({ credentials }) => {
|
||||
return fetch(MFA_BACKUP_CODES_URL, {
|
||||
headers: authHeaders(credentials),
|
||||
method: 'GET'
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const fetchMutes = ({ credentials }) => {
|
||||
return promisedRequest({ url: MASTODON_USER_MUTES_URL, credentials })
|
||||
.then((users) => users.map(parseUser))
|
||||
|
@ -830,6 +912,11 @@ const apiService = {
|
|||
importFollows,
|
||||
deleteAccount,
|
||||
changePassword,
|
||||
settingsMFA,
|
||||
mfaDisableOTP,
|
||||
generateMfaBackupCodes,
|
||||
mfaSetupOTP,
|
||||
mfaConfirmOTP,
|
||||
fetchFollowRequests,
|
||||
approveUser,
|
||||
denyUser,
|
||||
|
|
|
@ -117,6 +117,7 @@ const backendInteractorService = credentials => {
|
|||
const updateBanner = ({ banner }) => apiService.updateBanner({ credentials, banner })
|
||||
const updateProfile = ({ params }) => apiService.updateProfile({ credentials, params })
|
||||
|
||||
|
||||
const externalProfile = (profileUrl) => apiService.externalProfile({ profileUrl, credentials })
|
||||
const importBlocks = (file) => apiService.importBlocks({ file, credentials })
|
||||
const importFollows = (file) => apiService.importFollows({ file, credentials })
|
||||
|
@ -128,6 +129,11 @@ const backendInteractorService = credentials => {
|
|||
const fetchFavoritedByUsers = (id) => apiService.fetchFavoritedByUsers({ id })
|
||||
const fetchRebloggedByUsers = (id) => apiService.fetchRebloggedByUsers({ id })
|
||||
const reportUser = (params) => apiService.reportUser({ credentials, ...params })
|
||||
const fetchSettingsMFA = () => apiService.settingsMFA({ credentials })
|
||||
const generateMfaBackupCodes = () => apiService.generateMfaBackupCodes({ credentials })
|
||||
const mfaSetupOTP = () => apiService.mfaSetupOTP({ credentials })
|
||||
const mfaConfirmOTP = ({ password, token }) => apiService.mfaConfirmOTP({ credentials, password, token })
|
||||
const mfaDisableOTP = ({ password }) => apiService.mfaDisableOTP({ credentials, password })
|
||||
|
||||
const favorite = (id) => apiService.favorite({ id, credentials })
|
||||
const unfavorite = (id) => apiService.unfavorite({ id, credentials })
|
||||
|
@ -175,6 +181,11 @@ const backendInteractorService = credentials => {
|
|||
importFollows,
|
||||
deleteAccount,
|
||||
changePassword,
|
||||
fetchSettingsMFA,
|
||||
generateMfaBackupCodes,
|
||||
mfaSetupOTP,
|
||||
mfaConfirmOTP,
|
||||
mfaDisableOTP,
|
||||
fetchFollowRequests,
|
||||
approveUser,
|
||||
denyUser,
|
||||
|
|
|
@ -71,6 +71,23 @@ export const parseUser = (data) => {
|
|||
moderator: data.pleroma.is_moderator,
|
||||
admin: data.pleroma.is_admin
|
||||
}
|
||||
// TODO: Clean up in UI? This is duplication from what BE does for qvitterapi
|
||||
if (output.rights.admin) {
|
||||
output.role = 'admin'
|
||||
} else if (output.rights.moderator) {
|
||||
output.role = 'moderator'
|
||||
} else {
|
||||
output.role = 'member'
|
||||
}
|
||||
}
|
||||
|
||||
if (data.source) {
|
||||
output.description = data.source.note
|
||||
output.default_scope = data.source.privacy
|
||||
if (data.source.pleroma) {
|
||||
output.no_rich_text = data.source.pleroma.no_rich_text
|
||||
output.show_role = data.source.pleroma.show_role
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: handle is_local
|
||||
|
@ -105,8 +122,6 @@ export const parseUser = (data) => {
|
|||
|
||||
output.muted = data.muted
|
||||
|
||||
// QVITTER ONLY FOR NOW
|
||||
// Really only applies to logged in user, really.. I THINK
|
||||
if (data.rights) {
|
||||
output.rights = {
|
||||
moderator: data.rights.delete_others_notice,
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
const verifyOTPCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'totp')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const verifyRecoveryCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'recovery')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const mfa = {
|
||||
verifyOTPCode,
|
||||
verifyRecoveryCode
|
||||
}
|
||||
|
||||
export default mfa
|
|
@ -1,51 +1,57 @@
|
|||
import {reduce} from 'lodash'
|
||||
import { reduce } from 'lodash'
|
||||
|
||||
const REDIRECT_URI = `${window.location.origin}/oauth-callback`
|
||||
|
||||
export const getOrCreateApp = ({ clientId, clientSecret, instance, commit }) => {
|
||||
if (clientId && clientSecret) {
|
||||
return Promise.resolve({ clientId, clientSecret })
|
||||
}
|
||||
|
||||
const getOrCreateApp = ({oauth, instance}) => {
|
||||
const url = `${instance}/api/v1/apps`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_name', `PleromaFE_${Math.random()}`)
|
||||
form.append('redirect_uris', `${window.location.origin}/oauth-callback`)
|
||||
form.append('client_name', `PleromaFE_${window.___pleromafe_commit_hash}_${(new Date()).toISOString()}`)
|
||||
form.append('redirect_uris', REDIRECT_URI)
|
||||
form.append('scopes', 'read write follow')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const login = (args) => {
|
||||
getOrCreateApp(args).then((app) => {
|
||||
args.commit('setClientData', app)
|
||||
|
||||
const data = {
|
||||
response_type: 'code',
|
||||
client_id: app.client_id,
|
||||
redirect_uri: app.redirect_uri,
|
||||
scope: 'read write follow'
|
||||
}
|
||||
|
||||
const dataString = reduce(data, (acc, v, k) => {
|
||||
const encoded = `${k}=${encodeURIComponent(v)}`
|
||||
if (!acc) {
|
||||
return encoded
|
||||
} else {
|
||||
return `${acc}&${encoded}`
|
||||
}
|
||||
}, false)
|
||||
|
||||
// Do the redirect...
|
||||
const url = `${args.instance}/oauth/authorize?${dataString}`
|
||||
|
||||
window.location.href = url
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((app) => ({ clientId: app.client_id, clientSecret: app.client_secret }))
|
||||
.then((app) => commit('setClientData', app) || app)
|
||||
}
|
||||
|
||||
const getTokenWithCredentials = ({app, instance, username, password}) => {
|
||||
const login = ({ instance, clientId }) => {
|
||||
const data = {
|
||||
response_type: 'code',
|
||||
client_id: clientId,
|
||||
redirect_uri: REDIRECT_URI,
|
||||
scope: 'read write follow'
|
||||
}
|
||||
|
||||
const dataString = reduce(data, (acc, v, k) => {
|
||||
const encoded = `${k}=${encodeURIComponent(v)}`
|
||||
if (!acc) {
|
||||
return encoded
|
||||
} else {
|
||||
return `${acc}&${encoded}`
|
||||
}
|
||||
}, false)
|
||||
|
||||
// Do the redirect...
|
||||
const url = `${instance}/oauth/authorize?${dataString}`
|
||||
|
||||
window.location.href = url
|
||||
}
|
||||
|
||||
const getTokenWithCredentials = ({ clientId, clientSecret, instance, username, password }) => {
|
||||
const url = `${instance}/oauth/token`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('client_id', clientId)
|
||||
form.append('client_secret', clientSecret)
|
||||
form.append('grant_type', 'password')
|
||||
form.append('username', username)
|
||||
form.append('password', password)
|
||||
|
@ -56,15 +62,62 @@ const getTokenWithCredentials = ({app, instance, username, password}) => {
|
|||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const getToken = ({app, instance, code}) => {
|
||||
const getToken = ({ clientId, clientSecret, instance, code }) => {
|
||||
const url = `${instance}/oauth/token`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', clientId)
|
||||
form.append('client_secret', clientSecret)
|
||||
form.append('grant_type', 'authorization_code')
|
||||
form.append('code', code)
|
||||
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
})
|
||||
.then((data) => data.json())
|
||||
}
|
||||
|
||||
export const getClientToken = ({ clientId, clientSecret, instance }) => {
|
||||
const url = `${instance}/oauth/token`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', clientId)
|
||||
form.append('client_secret', clientSecret)
|
||||
form.append('grant_type', 'client_credentials')
|
||||
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
const verifyOTPCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('grant_type', 'authorization_code')
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('redirect_uri', `${window.location.origin}/oauth-callback`)
|
||||
form.append('challenge_type', 'totp')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
body: form
|
||||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const verifyRecoveryCode = ({app, instance, mfaToken, code}) => {
|
||||
const url = `${instance}/oauth/mfa/challenge`
|
||||
const form = new window.FormData()
|
||||
|
||||
form.append('client_id', app.client_id)
|
||||
form.append('client_secret', app.client_secret)
|
||||
form.append('mfa_token', mfaToken)
|
||||
form.append('code', code)
|
||||
form.append('challenge_type', 'recovery')
|
||||
|
||||
return window.fetch(url, {
|
||||
method: 'POST',
|
||||
|
@ -76,7 +129,9 @@ const oauth = {
|
|||
login,
|
||||
getToken,
|
||||
getTokenWithCredentials,
|
||||
getOrCreateApp
|
||||
getOrCreateApp,
|
||||
verifyOTPCode,
|
||||
verifyRecoveryCode
|
||||
}
|
||||
|
||||
export default oauth
|
||||
|
|
|
@ -5,9 +5,9 @@ const queryParams = (params) => {
|
|||
}
|
||||
|
||||
const headers = (store) => {
|
||||
const accessToken = store.state.oauth.token
|
||||
const accessToken = store.getters.getToken()
|
||||
if (accessToken) {
|
||||
return {'Authorization': `Bearer ${accessToken}`}
|
||||
return { 'Authorization': `Bearer ${accessToken}` }
|
||||
} else {
|
||||
return {}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue