Merge branch 'feature/registration' into 'develop'

Add a registration form.

See merge request !76
This commit is contained in:
lambadalambda 2017-06-21 12:22:28 -04:00
commit 937705ccb0
13 changed files with 357 additions and 10 deletions

View file

@ -4,7 +4,8 @@ const LoginForm = {
authError: false authError: false
}), }),
computed: { computed: {
loggingIn () { return this.$store.state.users.loggingIn } loggingIn () { return this.$store.state.users.loggingIn },
registrationOpen () { return this.$store.state.config.registrationOpen }
}, },
methods: { methods: {
submit () { submit () {

View file

@ -15,7 +15,10 @@
<input :disabled="loggingIn" v-model='user.password' class='form-control' id='password' type='password'> <input :disabled="loggingIn" v-model='user.password' class='form-control' id='password' type='password'>
</div> </div>
<div class='form-group'> <div class='form-group'>
<button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Submit</button> <div class='login-bottom'>
<div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>Register</router-link></div>
<button :disabled="loggingIn" type='submit' class='btn btn-default base05 base01-background'>Log in</button>
</div>
</div> </div>
<div v-if="authError" class='form-group'> <div v-if="authError" class='form-group'>
<div class='error base05'>{{authError}}</div> <div class='error base05'>{{authError}}</div>
@ -39,8 +42,8 @@
} }
.btn { .btn {
margin-top: 1.0em;
min-height: 28px; min-height: 28px;
width: 10em;
} }
.error { .error {
@ -50,6 +53,18 @@
min-height: 28px; min-height: 28px;
line-height: 28px; line-height: 28px;
} }
.register {
flex: 1 1;
}
.login-bottom {
margin-top: 1.0em;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
} }
</style> </style>

View file

@ -80,6 +80,18 @@
} }
} }
.btn {
cursor: pointer;
}
.btn[disabled] {
cursor: not-allowed;
}
.icon-cancel {
cursor: pointer;
}
form { form {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -0,0 +1,37 @@
const registration = {
data: () => ({
user: {},
error: false,
registering: false
}),
created () {
if (!this.$store.state.config.registrationOpen || !!this.$store.state.users.currentUser) {
this.$router.push('/main/all')
}
},
computed: {
termsofservice () { return this.$store.state.config.tos }
},
methods: {
submit () {
this.registering = true
this.user.nickname = this.user.username
this.$store.state.api.backendInteractor.register(this.user).then(
(response) => {
if (response.ok) {
this.$store.dispatch('loginUser', this.user)
this.$router.push('/main/all')
this.registering = false
} else {
this.registering = false
response.json().then((data) => {
this.error = data.error
})
}
}
)
}
}
}
export default registration

View file

@ -0,0 +1,134 @@
<template>
<div class="settings panel panel-default base00-background">
<div class="panel-heading base01-background base04">
Registration
</div>
<div class="panel-body">
<form v-on:submit.prevent='submit(user)' class='registration-form'>
<div class='container'>
<div class='text-fields'>
<div class='form-group'>
<label for='username'>Username</label>
<input :disabled="registering" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'>
</div>
<div class='form-group'>
<label for='fullname'>Fullname</label>
<input :disabled="registering" v-model='user.fullname' class='form-control' id='fullname' placeholder='e.g. Lain Iwakura'>
</div>
<div class='form-group'>
<label for='email'>Email</label>
<input :disabled="registering" v-model='user.email' class='form-control' id='email' type="email">
</div>
<div class='form-group'>
<label for='bio'>Bio</label>
<input :disabled="registering" v-model='user.bio' class='form-control' id='bio'>
</div>
<div class='form-group'>
<label for='password'>Password</label>
<input :disabled="registering" v-model='user.password' class='form-control' id='password' type='password'>
</div>
<div class='form-group'>
<label for='password_confirmation'>Password confirmation</label>
<input :disabled="registering" v-model='user.confirm' class='form-control' id='password_confirmation' type='password'>
</div>
<!--
<div class='form-group'>
<label for='captcha'>Captcha</label>
<img src='/qvittersimplesecurity/captcha.jpg' alt='captcha' class='captcha'>
<input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
</div>
-->
<div class='form-group'>
<button :disabled="registering" type='submit' class='btn btn-default base05 base01-background'>Submit</button>
</div>
</div>
<div class='terms-of-service' v-html="termsofservice">
</div>
</div>
<div v-if="error" class='form-group'>
<div class='error base05'>{{error}}</div>
</div>
</form>
</div>
</div>
</template>
<script src="./registration.js"></script>
<style lang="scss">
.registration-form {
display: flex;
flex-direction: column;
margin: 0.6em;
.container {
display: flex;
flex-direction: row;
//margin-bottom: 1em;
}
.terms-of-service {
flex: 0 1 50%;
margin: 0.8em;
}
.text-fields {
margin-top: 0.6em;
flex: 1 0;
display: flex;
flex-direction: column;
}
.form-group {
display: flex;
flex-direction: column;
padding: 0.3em 0.0em 0.3em;
line-height:24px;
}
form textarea {
border: solid;
border-width: 1px;
border-color: silver;
border-radius: 5px;
line-height:16px;
padding: 5px;
resize: vertical;
}
input {
border-width: 1px;
border-style: solid;
border-color: silver;
border-radius: 5px;
padding: 0.1em 0.2em 0.2em 0.2em;
}
.captcha {
max-width: 350px;
margin-bottom: 0.4em;
}
.btn {
//align-self: flex-start;
//width: 10em;
margin-top: 0.6em;
height: 28px;
}
.error {
border-radius: 5px;
text-align: center;
margin: 0.5em 0.6em 0;
background-color: rgba(255, 48, 16, 0.65);
min-height: 28px;
line-height: 28px;
}
}
@media all and (max-width: 959px) {
.registration-form .container {
flex-direction: column-reverse;
}
}
</style>

View file

@ -7,14 +7,58 @@ const settings = {
hideAttachmentsLocal: this.$store.state.config.hideAttachments, hideAttachmentsLocal: this.$store.state.config.hideAttachments,
hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv, hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
hideNsfwLocal: this.$store.state.config.hideNsfw, hideNsfwLocal: this.$store.state.config.hideNsfw,
muteWordsString: this.$store.state.config.muteWords.join('\n'),
autoLoadLocal: this.$store.state.config.autoLoad, autoLoadLocal: this.$store.state.config.autoLoad,
hoverPreviewLocal: this.$store.state.config.hoverPreview, hoverPreviewLocal: this.$store.state.config.hoverPreview,
muteWordsString: this.$store.state.config.muteWords.join('\n') previewfile: null
} }
}, },
components: { components: {
StyleSwitcher StyleSwitcher
}, },
computed: {
user () {
return this.$store.state.users.currentUser
}
},
methods: {
uploadAvatar ({target}) {
const file = target.files[0]
// eslint-disable-next-line no-undef
const reader = new FileReader()
reader.onload = ({target}) => {
const img = target.result
this.previewfile = img
}
reader.readAsDataURL(file)
},
submitAvatar () {
if (!this.previewfile) { return }
const img = this.previewfile
// eslint-disable-next-line no-undef
let imginfo = new Image()
let cropX, cropY, cropW, cropH
imginfo.src = this.previewfile
if (imginfo.height > imginfo.width) {
cropX = 0
cropW = imginfo.width
cropY = Math.floor((imginfo.height - imginfo.width) / 2)
cropH = imginfo.width
} else {
cropY = 0
cropH = imginfo.height
cropX = Math.floor((imginfo.width - imginfo.height) / 2)
cropW = imginfo.height
}
this.$store.state.api.backendInteractor.updateAvatar({params: {img, cropX, cropY, cropW, cropH}}).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
}
})
}
},
watch: { watch: {
hideAttachmentsLocal (value) { hideAttachmentsLocal (value) {
this.$store.dispatch('setOption', { name: 'hideAttachments', value }) this.$store.dispatch('setOption', { name: 'hideAttachments', value })

View file

@ -8,6 +8,18 @@
<h2>Theme</h2> <h2>Theme</h2>
<style-switcher></style-switcher> <style-switcher></style-switcher>
</div> </div>
<div class="setting-item" v-if="user">
<h2>Avatar</h2>
<p>Your current avatar:</p>
<img :src="user.profile_image_url_original" class="old-avatar"></img>
<p>Set new avatar:</p>
<img class="new-avatar" v-bind:src="previewfile" v-if="previewfile">
</img>
<div>
<input name="avatar-upload" id="avatar-upload" type="file" @change="uploadAvatar" ></input>
</div>
<button class="btn btn-default base05 base01-background" v-if="previewfile" @click="submitAvatar">Submit</button>
</div>
<div class="setting-item"> <div class="setting-item">
<h2>Filtering</h2> <h2>Filtering</h2>
<p>All notices containing these words will be muted, one per line</p> <p>All notices containing these words will be muted, one per line</p>
@ -52,6 +64,24 @@
width: 100%; width: 100%;
height: 100px; height: 100px;
} }
.old-avatar {
width: 128px;
border-radius: 5px;
}
.new-avatar {
object-fit: cover;
width: 128px;
height: 128px;
border-radius: 5px;
}
.btn {
margin-top: 1em;
min-height: 28px;
width: 10em;
}
} }
.setting-list { .setting-list {
list-style-type: none; list-style-type: none;

View file

@ -9,6 +9,7 @@ import ConversationPage from './components/conversation-page/conversation-page.v
import Mentions from './components/mentions/mentions.vue' import Mentions from './components/mentions/mentions.vue'
import UserProfile from './components/user_profile/user_profile.vue' import UserProfile from './components/user_profile/user_profile.vue'
import Settings from './components/settings/settings.vue' import Settings from './components/settings/settings.vue'
import Registration from './components/registration/registration.vue'
import statusesModule from './modules/statuses.js' import statusesModule from './modules/statuses.js'
import usersModule from './modules/users.js' import usersModule from './modules/users.js'
@ -60,7 +61,8 @@ const routes = [
{ name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
{ name: 'user-profile', path: '/users/:id', component: UserProfile }, { name: 'user-profile', path: '/users/:id', component: UserProfile },
{ name: 'mentions', path: '/:username/mentions', component: Mentions }, { name: 'mentions', path: '/:username/mentions', component: Mentions },
{ name: 'settings', path: '/settings', component: Settings } { name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }
] ]
const router = new VueRouter({ const router = new VueRouter({
@ -84,9 +86,16 @@ new Vue({
window.fetch('/static/config.json') window.fetch('/static/config.json')
.then((res) => res.json()) .then((res) => res.json())
.then(({name, theme, background, logo}) => { .then(({name, theme, background, logo, registrationOpen}) => {
store.dispatch('setOption', { name: 'name', value: name }) store.dispatch('setOption', { name: 'name', value: name })
store.dispatch('setOption', { name: 'theme', value: theme }) store.dispatch('setOption', { name: 'theme', value: theme })
store.dispatch('setOption', { name: 'background', value: background }) store.dispatch('setOption', { name: 'background', value: background })
store.dispatch('setOption', { name: 'logo', value: logo }) store.dispatch('setOption', { name: 'logo', value: logo })
store.dispatch('setOption', { name: 'registrationOpen', value: registrationOpen })
})
window.fetch('/static/terms-of-service.html')
.then((res) => res.text())
.then((html) => {
store.dispatch('setOption', { name: 'tos', value: html })
}) })

View file

@ -24,7 +24,7 @@ export const mutations = {
set(user, 'muted', muted) set(user, 'muted', muted)
}, },
setCurrentUser (state, user) { setCurrentUser (state, user) {
state.currentUser = user state.currentUser = merge(state.currentUser || {}, user)
}, },
beginLogin (state) { beginLogin (state) {
state.loggingIn = true state.loggingIn = true

View file

@ -17,13 +17,15 @@ const FRIENDS_URL = '/api/statuses/friends.json'
const FOLLOWING_URL = '/api/friendships/create.json' const FOLLOWING_URL = '/api/friendships/create.json'
const UNFOLLOWING_URL = '/api/friendships/destroy.json' const UNFOLLOWING_URL = '/api/friendships/destroy.json'
const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json' const QVITTER_USER_PREF_URL = '/api/qvitter/set_profile_pref.json'
const REGISTRATION_URL = '/api/account/register.json'
const AVATAR_UPDATE_URL = '/api/qvitter/update_avatar.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json' const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json' const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
// const USER_URL = '/api/users/show.json' // const USER_URL = '/api/users/show.json'
const oldfetch = window.fetch import { each, map } from 'lodash'
import { map } from 'lodash' const oldfetch = window.fetch
let fetch = (url, options) => { let fetch = (url, options) => {
const baseUrl = '' const baseUrl = ''
@ -31,6 +33,55 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, options) return oldfetch(fullUrl, options)
} }
// Params
// cropH
// cropW
// cropX
// cropY
// img (base 64 encodend data url)
const updateAvatar = ({credentials, params}) => {
let url = AVATAR_UPDATE_URL
const form = new FormData()
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
body: form
}).then((data) => data.json())
}
// Params needed:
// nickname
// email
// fullname
// password
// password_confirm
//
// Optional
// bio
// homepage
// location
const register = (params) => {
const form = new FormData()
each(params, (value, key) => {
if (value) {
form.append(key, value)
}
})
return fetch(REGISTRATION_URL, {
method: 'POST',
body: form
})
}
const authHeaders = (user) => { const authHeaders = (user) => {
if (user && user.username && user.password) { if (user && user.username && user.password) {
return { 'Authorization': `Basic ${btoa(`${user.username}:${user.password}`)}` } return { 'Authorization': `Basic ${btoa(`${user.username}:${user.password}`)}` }
@ -220,6 +271,8 @@ const apiService = {
fetchAllFollowing, fetchAllFollowing,
setUserMute, setUserMute,
fetchMutes, fetchMutes,
register,
updateAvatar,
externalProfile externalProfile
} }

View file

@ -36,6 +36,8 @@ const backendInteractorService = (credentials) => {
const fetchMutes = () => apiService.fetchMutes({credentials}) const fetchMutes = () => apiService.fetchMutes({credentials})
const register = (params) => apiService.register(params)
const updateAvatar = ({params}) => apiService.updateAvatar({credentials, params})
const externalProfile = (profileUrl) => apiService.externalProfile(profileUrl) const externalProfile = (profileUrl) => apiService.externalProfile(profileUrl)
const backendInteractorServiceInstance = { const backendInteractorServiceInstance = {
@ -49,6 +51,8 @@ const backendInteractorService = (credentials) => {
startFetching, startFetching,
setUserMute, setUserMute,
fetchMutes, fetchMutes,
register,
updateAvatar,
externalProfile externalProfile
} }

View file

@ -2,5 +2,6 @@
"name": "Pleroma FE", "name": "Pleroma FE",
"theme": "base16-pleroma-dark.css", "theme": "base16-pleroma-dark.css",
"background": "/static/bg.jpg", "background": "/static/bg.jpg",
"logo": "/static/logo.png" "logo": "/static/logo.png",
"registrationOpen": false
} }

View file

@ -0,0 +1,7 @@
<h4>Terms of Service</h4>
<p>This is a placeholder ToS.</p>
<p>Edit <code>"/static/terms-of-service.html"</code> to make it fit the needs of your instance.</p>
<br>
<img src="/static/logo.png"/ style="display: block; margin: auto;">