Some work
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
parent
394057d45e
commit
93a97b0865
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,4 +15,5 @@ erl_crash.dump
|
||||||
# variables.
|
# variables.
|
||||||
/config/*.secret.exs
|
/config/*.secret.exs
|
||||||
|
|
||||||
|
.elixir_ls
|
||||||
/doc
|
/doc
|
|
@ -25,7 +25,10 @@ config :eventos, EventosWeb.Endpoint,
|
||||||
secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM",
|
secret_key_base: "1yOazsoE0Wqu4kXk3uC5gu3jDbShOimTCzyFL3OjCdBmOXMyHX87Qmf3+Tu9s0iM",
|
||||||
render_errors: [view: EventosWeb.ErrorView, accepts: ~w(html json)],
|
render_errors: [view: EventosWeb.ErrorView, accepts: ~w(html json)],
|
||||||
pubsub: [name: Eventos.PubSub,
|
pubsub: [name: Eventos.PubSub,
|
||||||
adapter: Phoenix.PubSub.PG2]
|
adapter: Phoenix.PubSub.PG2],
|
||||||
|
instance: "localhost",
|
||||||
|
email_from: "noreply@localhost",
|
||||||
|
email_to: "noreply@localhost"
|
||||||
|
|
||||||
# Configures Elixir's Logger
|
# Configures Elixir's Logger
|
||||||
config :logger, :console,
|
config :logger, :console,
|
||||||
|
|
|
@ -47,6 +47,9 @@ config :logger, :console, format: "[$level] $message\n", level: :debug
|
||||||
# in production as building large stacktraces may be expensive.
|
# in production as building large stacktraces may be expensive.
|
||||||
config :phoenix, :stacktrace_depth, 20
|
config :phoenix, :stacktrace_depth, 20
|
||||||
|
|
||||||
|
config :eventos, Eventos.Mailer,
|
||||||
|
adapter: Bamboo.LocalAdapter
|
||||||
|
|
||||||
# Configure your database
|
# Configure your database
|
||||||
config :eventos, Eventos.Repo,
|
config :eventos, Eventos.Repo,
|
||||||
adapter: Ecto.Adapters.Postgres,
|
adapter: Ecto.Adapters.Postgres,
|
||||||
|
|
|
@ -18,6 +18,19 @@ config :eventos, EventosWeb.Endpoint,
|
||||||
url: [host: "example.com", port: 80],
|
url: [host: "example.com", port: 80],
|
||||||
cache_static_manifest: "priv/static/cache_manifest.json"
|
cache_static_manifest: "priv/static/cache_manifest.json"
|
||||||
|
|
||||||
|
config :eventos, Eventos.Mailer,
|
||||||
|
adapter: Bamboo.SMTPAdapter,
|
||||||
|
server: "localhost",
|
||||||
|
hostname: "localhost",
|
||||||
|
port: 25,
|
||||||
|
username: nil, # or {:system, "SMTP_USERNAME"}
|
||||||
|
password: nil, # or {:system, "SMTP_PASSWORD"}
|
||||||
|
tls: :if_available, # can be `:always` or `:never`
|
||||||
|
allowed_tls_versions: [:"tlsv1", :"tlsv1.1", :"tlsv1.2"], # or {":system", ALLOWED_TLS_VERSIONS"} w/ comma seprated values (e.g. "tlsv1.1,tlsv1.2")
|
||||||
|
ssl: false, # can be `true`
|
||||||
|
retries: 1,
|
||||||
|
no_mx_lookups: false # can be `true`
|
||||||
|
|
||||||
# Do not print debug messages in production
|
# Do not print debug messages in production
|
||||||
config :logger, level: :info
|
config :logger, level: :info
|
||||||
|
|
||||||
|
|
7755
js/package-lock.json
generated
7755
js/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,28 +11,29 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"material-design-icons": "^3.0.1",
|
"material-design-icons": "^3.0.1",
|
||||||
"moment": "^2.22.1",
|
"moment": "^2.22.2",
|
||||||
"ngeohash": "^0.6.0",
|
"ngeohash": "^0.6.0",
|
||||||
"register-service-worker": "^1.0.0",
|
"register-service-worker": "^1.4.1",
|
||||||
"vue": "^2.5.16",
|
"vue": "^2.5.16",
|
||||||
|
"vue-gravatar": "^1.2.1",
|
||||||
"vue-markdown": "^2.2.4",
|
"vue-markdown": "^2.2.4",
|
||||||
"vue-router": "^3.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vuetify": "^1.0.18",
|
"vuetify": "^1.1.1",
|
||||||
"vuetify-google-autocomplete": "^2.0.0-Alpha.9",
|
"vuetify-google-autocomplete": "^2.0.0-beta.4",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1",
|
||||||
"vuex-i18n": "^1.10.5"
|
"vuex-i18n": "^1.10.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv-webpack": "^1.5.5",
|
"@vue/cli-plugin-babel": "^3.0.0-rc.3",
|
||||||
"@vue/cli-plugin-babel": "^3.0.0-beta.10",
|
"@vue/cli-plugin-e2e-nightwatch": "^3.0.0-rc.3",
|
||||||
"@vue/cli-plugin-e2e-nightwatch": "^3.0.0-beta.10",
|
"@vue/cli-plugin-eslint": "^3.0.0-rc.3",
|
||||||
"@vue/cli-plugin-eslint": "^3.0.0-beta.10",
|
"@vue/cli-plugin-pwa": "^3.0.0-rc.3",
|
||||||
"@vue/cli-plugin-pwa": "^3.0.0-beta.10",
|
"@vue/cli-plugin-unit-mocha": "^3.0.0-rc.3",
|
||||||
"@vue/cli-plugin-unit-mocha": "^3.0.0-beta.10",
|
"@vue/cli-service": "^3.0.0-rc.3",
|
||||||
"@vue/cli-service": "^3.0.0-beta.10",
|
"@vue/eslint-config-airbnb": "^3.0.0-rc.3",
|
||||||
"@vue/eslint-config-airbnb": "^3.0.0-beta.10",
|
"@vue/test-utils": "^1.0.0-beta.20",
|
||||||
"@vue/test-utils": "^1.0.0-beta.10",
|
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
|
"dotenv-webpack": "^1.5.7",
|
||||||
"node-sass": "^4.7.2",
|
"node-sass": "^4.7.2",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"vue-template-compiler": "^2.5.13"
|
"vue-template-compiler": "^2.5.13"
|
||||||
|
|
|
@ -38,28 +38,54 @@
|
||||||
</template>
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-navigation-drawer>
|
</v-navigation-drawer>
|
||||||
<NavBar></NavBar>
|
<NavBar v-bind="{toggleDrawer}"></NavBar>
|
||||||
<v-content>
|
<v-content>
|
||||||
<v-container fluid fill-height>
|
<v-container fluid fill-height>
|
||||||
<v-layout xs-12>
|
<v-layout xs-12>
|
||||||
<transition>
|
<transition name="router">
|
||||||
<router-view></router-view>
|
<router-view></router-view>
|
||||||
</transition>
|
</transition>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-content>
|
</v-content>
|
||||||
<v-btn
|
<v-speed-dial
|
||||||
fixed
|
v-model="fab"
|
||||||
dark
|
|
||||||
fab
|
|
||||||
bottom
|
bottom
|
||||||
|
fixed
|
||||||
right
|
right
|
||||||
color="pink"
|
direction="top"
|
||||||
@click="$router.push({name: 'CreateEvent'})"
|
transition="scale-transition"
|
||||||
v-if="getUser()"
|
v-if="getUser()"
|
||||||
>
|
>
|
||||||
<v-icon>add</v-icon>
|
<v-btn
|
||||||
</v-btn>
|
slot="activator"
|
||||||
|
v-model="fab"
|
||||||
|
color="blue darken-2"
|
||||||
|
dark
|
||||||
|
fab
|
||||||
|
>
|
||||||
|
<v-icon>add</v-icon>
|
||||||
|
<v-icon>close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
fab
|
||||||
|
dark
|
||||||
|
small
|
||||||
|
color="pink"
|
||||||
|
@click="$router.push({name: 'CreateEvent'})"
|
||||||
|
>
|
||||||
|
<v-icon>event</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn
|
||||||
|
fab
|
||||||
|
dark
|
||||||
|
small
|
||||||
|
color="purple"
|
||||||
|
@click="$router.push({name: 'CreateGroup'})"
|
||||||
|
>
|
||||||
|
<v-icon>group</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-speed-dial>
|
||||||
<v-footer class="indigo" app>
|
<v-footer class="indigo" app>
|
||||||
<span class="white--text">© Thomas Citharel {{ new Date().getFullYear() }} - Made with Elixir, Phoenix & <a href="https://vuejs.org/">VueJS</a> & <a href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks</span>
|
<span class="white--text">© Thomas Citharel {{ new Date().getFullYear() }} - Made with Elixir, Phoenix & <a href="https://vuejs.org/">VueJS</a> & <a href="https://www.vuetifyjs.com/">Vuetify</a> with some love and some weeks</span>
|
||||||
</v-footer>
|
</v-footer>
|
||||||
|
@ -85,7 +111,8 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
drawer: true,
|
drawer: false,
|
||||||
|
fab: false,
|
||||||
user: false,
|
user: false,
|
||||||
items: [
|
items: [
|
||||||
{ icon: 'poll', text: 'Events', route: 'EventList', role: null },
|
{ icon: 'poll', text: 'Events', route: 'EventList', role: null },
|
||||||
|
@ -110,10 +137,25 @@ export default {
|
||||||
},
|
},
|
||||||
getUser() {
|
getUser() {
|
||||||
return this.$store.state.user === undefined ? false : this.$store.state.user;
|
return this.$store.state.user === undefined ? false : this.$store.state.user;
|
||||||
|
},
|
||||||
|
toggleDrawer() {
|
||||||
|
this.drawer = !this.drawer;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.router-enter-active, .router-leave-active {
|
||||||
|
transition-property: opacity;
|
||||||
|
transition-duration: .25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.router-enter-active {
|
||||||
|
transition-delay: .25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.router-enter, .router-leave-active {
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
65
js/src/assets/profile.svg
Normal file
65
js/src/assets/profile.svg
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
data-name="Layer 1"
|
||||||
|
viewBox="0 0 100 125"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
id="svg2"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
sodipodi:docname="profile.svg">
|
||||||
|
<metadata
|
||||||
|
id="metadata24">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs22" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1"
|
||||||
|
objecttolerance="10"
|
||||||
|
gridtolerance="10"
|
||||||
|
guidetolerance="10"
|
||||||
|
inkscape:pageopacity="0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:window-width="640"
|
||||||
|
inkscape:window-height="480"
|
||||||
|
id="namedview20"
|
||||||
|
showgrid="false"
|
||||||
|
inkscape:zoom="1.888"
|
||||||
|
inkscape:cx="50"
|
||||||
|
inkscape:cy="62.5"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="36"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg2" />
|
||||||
|
<title
|
||||||
|
id="title4">47 all</title>
|
||||||
|
<g
|
||||||
|
id="g6">"><g
|
||||||
|
id="g8">"><path
|
||||||
|
d="M77.74,83.19H22.26V76.47a24,24,0,0,1,24-24h7.48a24,24,0,0,1,24,24Zm-51.48-4H73.74V76.47a20,20,0,0,0-20-20H46.26a20,20,0,0,0-20,20Z"
|
||||||
|
id="path10" />
|
||||||
|
</g>
|
||||||
|
<g
|
||||||
|
id="g12">"><path
|
||||||
|
d="M50,50.5A16.85,16.85,0,1,1,66.85,33.66,16.87,16.87,0,0,1,50,50.5Zm0-29.7A12.85,12.85,0,1,0,62.85,33.66,12.86,12.86,0,0,0,50,20.81Z"
|
||||||
|
id="path14" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -61,10 +61,11 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
// To log out, we just need to remove the token
|
// To log out, we just need to remove the token
|
||||||
logout() {
|
logout(store) {
|
||||||
localStorage.removeItem('refresh_token');
|
localStorage.removeItem('refresh_token');
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
this.authenticated = false;
|
this.authenticated = false;
|
||||||
|
store.commit('LOGOUT_USER');
|
||||||
},
|
},
|
||||||
|
|
||||||
jwt_decode(token) {
|
jwt_decode(token) {
|
||||||
|
|
|
@ -14,20 +14,30 @@
|
||||||
<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.user.actor.id === actor.id">
|
<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.user.actor.id === actor.id">
|
||||||
<v-icon>edit</v-icon>
|
<v-icon>edit</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn icon>
|
<v-menu bottom left>
|
||||||
<v-icon>more_vert</v-icon>
|
<v-btn icon slot="activator">
|
||||||
</v-btn>
|
<v-icon>more_vert</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<v-list>
|
||||||
|
<v-list-tile @click="logoutUser()" v-if="$store.state.user && $store.state.user.actor.id === actor.id">
|
||||||
|
<v-list-tile-title>User logout</v-list-tile-title>
|
||||||
|
</v-list-tile>
|
||||||
|
<v-list-tile @click="deleteAccount()" v-if="$store.state.user && $store.state.user.actor.id === actor.id">
|
||||||
|
<v-list-tile-title>Delete</v-list-tile-title>
|
||||||
|
</v-list-tile>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<div class="text-xs-center">
|
<div class="text-xs-center">
|
||||||
<v-avatar size="125px">
|
<v-avatar size="125px">
|
||||||
<img v-if="!account.avatar_url"
|
<img v-if="!actor.avatar_url"
|
||||||
class="img-circle elevation-7 mb-1"
|
class="img-circle elevation-7 mb-1"
|
||||||
src="https://picsum.photos/125/125/"
|
src="https://picsum.photos/125/125/"
|
||||||
>
|
>
|
||||||
<img v-else
|
<img v-else
|
||||||
class="img-circle elevation-7 mb-1"
|
class="img-circle elevation-7 mb-1"
|
||||||
:src="account.avatar_url"
|
:src="actor.avatar_url"
|
||||||
>
|
>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,6 +176,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import eventFetch from '@/api/eventFetch';
|
import eventFetch from '@/api/eventFetch';
|
||||||
|
import auth from '@/auth';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Account',
|
name: 'Account',
|
||||||
|
@ -197,6 +208,10 @@ export default {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
console.log(this.actor);
|
console.log(this.actor);
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
logoutUser() {
|
||||||
|
auth.logout(this.$store);
|
||||||
|
this.$router.push({ name: 'Home' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
141
js/src/components/Account/Login.vue
Normal file
141
js/src/components/Account/Login.vue
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
<template>
|
||||||
|
<v-container fluid fill-height>
|
||||||
|
<v-layout align-center justify-center>
|
||||||
|
<v-flex xs12 sm8 md4>
|
||||||
|
<v-card class="elevation-12">
|
||||||
|
<v-toolbar dark color="primary">
|
||||||
|
<v-toolbar-title>Login</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-tooltip bottom>
|
||||||
|
<v-btn
|
||||||
|
slot="activator"
|
||||||
|
:to="{ name: 'Register', params: { email: this.credentials.email, password: this.credentials.password } }"
|
||||||
|
>
|
||||||
|
<!-- <v-icon large>login</v-icon> -->
|
||||||
|
<span>Register</span>
|
||||||
|
</v-btn>
|
||||||
|
<span>Register</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-xs-center">
|
||||||
|
<v-avatar size="80px">
|
||||||
|
<transition name="avatar">
|
||||||
|
<component :is="validEmail()" v-bind="{email: credentials.email}"></component>
|
||||||
|
<!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/>
|
||||||
|
<avatar v-else></avatar> -->
|
||||||
|
</transition>
|
||||||
|
</v-avatar>
|
||||||
|
</div>
|
||||||
|
<v-form @submit="loginAction" v-if="!validationSent">
|
||||||
|
<v-text-field
|
||||||
|
label="Email"
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
v-model="credentials.email"
|
||||||
|
:rules="[rules.required, rules.email]"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
label="password"
|
||||||
|
required
|
||||||
|
type="password"
|
||||||
|
v-model="credentials.password"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn @click="loginAction" color="blue">Login</v-btn>
|
||||||
|
<router-link :to="{ name: 'SendPasswordReset', params: { email: credentials.email } }">Password forgotten ?</router-link>
|
||||||
|
</v-form>
|
||||||
|
<div v-else>
|
||||||
|
<h2>{{ $t('registration.form.validation_sent', { email: credentials.email }) }}</h2>
|
||||||
|
<b-alert show variant="info">{{ $t('registration.form.validation_sent_info') }}</b-alert>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import auth from '@/auth/index';
|
||||||
|
import Gravatar from 'vue-gravatar';
|
||||||
|
import RegisterAvatar from './RegisterAvatar';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
beforeCreate() {
|
||||||
|
if (this.$store.state.user) {
|
||||||
|
this.$router.push('/');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
'v-gravatar': Gravatar,
|
||||||
|
'avatar': RegisterAvatar
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.credentials.email = this.email;
|
||||||
|
this.credentials.password = this.password;
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
credentials: {
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
validationSent: false,
|
||||||
|
error: {
|
||||||
|
show: false,
|
||||||
|
text: '',
|
||||||
|
timeout: 3000,
|
||||||
|
field: {
|
||||||
|
email: false,
|
||||||
|
password: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
required: value => !!value || 'Required.',
|
||||||
|
email: (value) => {
|
||||||
|
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
return pattern.test(value) || 'Invalid e-mail.';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loginAction(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
auth.login(JSON.stringify(this.credentials), (data) => {
|
||||||
|
this.$store.commit('LOGIN_USER', data.user);
|
||||||
|
this.$router.push({ name: 'Home' });
|
||||||
|
}, (error) => {
|
||||||
|
Promise.resolve(error).then((errorMsg) => {
|
||||||
|
console.log(errorMsg);
|
||||||
|
this.error.show = true;
|
||||||
|
this.error.text = this.$t(errorMsg.display_error);
|
||||||
|
}).catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
this.error.show = true;
|
||||||
|
this.error.text = e.message;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
validEmail() {
|
||||||
|
return this.rules.email(this.credentials.email) === true ? 'v-gravatar' : 'avatar';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
128
js/src/components/Account/PasswordReset.vue
Normal file
128
js/src/components/Account/PasswordReset.vue
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<v-container fluid fill-height>
|
||||||
|
<v-layout align-center justify-center>
|
||||||
|
<v-flex xs12 sm8 md4>
|
||||||
|
<v-card class="elevation-12">
|
||||||
|
<v-toolbar dark color="primary">
|
||||||
|
<v-toolbar-title>Password Reset</v-toolbar-title>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
<v-alert type="error" :value="state.token.status === false">{{ state.token.msg }}</v-alert>
|
||||||
|
<v-form @submit="resetAction">
|
||||||
|
<v-text-field
|
||||||
|
label="Password"
|
||||||
|
type="password"
|
||||||
|
v-model="credentials.password"
|
||||||
|
required
|
||||||
|
:error="state.password.status"
|
||||||
|
:rules="[rules.required, rules.password_length]"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
label="Password (confirmation)"
|
||||||
|
type="password"
|
||||||
|
v-model="credentials.password_confirmation"
|
||||||
|
required
|
||||||
|
:rules="[rules.required, rules.password_length, rules.password_equal]"
|
||||||
|
:error="state.password_confirmation.status"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn type="submit" :disabled="!samePasswords" color="blue">Reset my password</v-btn>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import fetchStory from '@/api/eventFetch';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PasswordReset',
|
||||||
|
props: {
|
||||||
|
token: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
samePasswords() {
|
||||||
|
return this.rules.password_length(this.credentials.password) === true &&
|
||||||
|
this.credentials.password === this.credentials.password_confirmation;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
credentials: {
|
||||||
|
password: '',
|
||||||
|
password_confirmation: '',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
state: {
|
||||||
|
token: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
password_confirmation: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
password_length: value => value.length > 6 || 'Password must be at least 6 caracters long',
|
||||||
|
required: value => !!value || 'Required.',
|
||||||
|
password_equal: value => value === this.credentials.password || 'Passwords must be the same',
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resetAction(e) {
|
||||||
|
this.resetState();
|
||||||
|
e.preventDefault();
|
||||||
|
console.log(this.token);
|
||||||
|
fetchStory('/users/password-reset/post', this.$store, { method: 'POST', body: JSON.stringify({ password: this.credentials.password, token: this.token }) }).then((data) => {
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
localStorage.setItem('refresh_token', data.refresh_token);
|
||||||
|
this.$store.commit('LOGIN_USER', data.account);
|
||||||
|
this.$snotify.success(this.$t('registration.success.login', { username: data.account.username }));
|
||||||
|
this.$router.push({ name: 'Home' });
|
||||||
|
}, (error) => {
|
||||||
|
Promise.resolve(error).then((errormsg) => {
|
||||||
|
console.log('errormsg', errormsg);
|
||||||
|
this.error.show = true;
|
||||||
|
Object.entries(JSON.parse(errormsg).errors).forEach(([key, val]) => {
|
||||||
|
console.log('key', key);
|
||||||
|
console.log('val', val[0]);
|
||||||
|
this.state[key] = { status: false, msg: val[0] };
|
||||||
|
console.log('state', this.state);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetState() {
|
||||||
|
this.state = {
|
||||||
|
token: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
password_confirmation: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
198
js/src/components/Account/Register.vue
Normal file
198
js/src/components/Account/Register.vue
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
<template>
|
||||||
|
<v-container fluid fill-height>
|
||||||
|
<v-layout align-center justify-center>
|
||||||
|
<v-flex xs12 sm8 md4>
|
||||||
|
<v-card class="elevation-12">
|
||||||
|
<v-toolbar dark color="primary">
|
||||||
|
<v-toolbar-title>Register</v-toolbar-title>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-tooltip bottom>
|
||||||
|
<v-btn
|
||||||
|
slot="activator"
|
||||||
|
:to="{ name: 'Login', params: { email: this.credentials.email, password: this.credentials.password } }"
|
||||||
|
>
|
||||||
|
<!-- <v-icon large>login</v-icon> -->
|
||||||
|
<span>Login</span>
|
||||||
|
</v-btn>
|
||||||
|
<span>Login</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="text-xs-center">
|
||||||
|
<v-avatar size="80px">
|
||||||
|
<transition name="avatar">
|
||||||
|
<component :is="validEmail()" v-bind="{email: credentials.email}"></component>
|
||||||
|
<!-- <v-gravatar :email="credentials.email" default-img="mp" v-if="validEmail()"/>
|
||||||
|
<avatar v-else></avatar> -->
|
||||||
|
</transition>
|
||||||
|
</v-avatar>
|
||||||
|
</div>
|
||||||
|
<v-form @submit="registerAction" v-if="!validationSent">
|
||||||
|
<v-text-field
|
||||||
|
label="Username"
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
v-model="credentials.username"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
:error="this.state.username.status"
|
||||||
|
:error-messages="this.state.username.msg"
|
||||||
|
:suffix="this.host()"
|
||||||
|
hint="You will be able to create more identities once registered"
|
||||||
|
persistent-hint
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
label="Email"
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
ref="email"
|
||||||
|
v-model="credentials.email"
|
||||||
|
:rules="[rules.required, rules.email]"
|
||||||
|
:error="this.state.email.status"
|
||||||
|
:error-messages="this.state.email.msg"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-text-field
|
||||||
|
label="Password"
|
||||||
|
required
|
||||||
|
:type="showPassword ? 'text' : 'password'"
|
||||||
|
v-model="credentials.password"
|
||||||
|
:rules="[rules.required, rules.password_length]"
|
||||||
|
:error="this.state.password.status"
|
||||||
|
:error-messages="this.state.password.msg"
|
||||||
|
:append-icon="showPassword ? 'visibility_off' : 'visibility'"
|
||||||
|
@click:append="showPassword = !showPassword"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn @click="registerAction" color="primary">Register</v-btn>
|
||||||
|
<router-link :to="{ name: 'ResendConfirmation', params: { email: credentials.email }}">Didn't receive the instructions ?</router-link>
|
||||||
|
</v-form>
|
||||||
|
<div v-else>
|
||||||
|
<h2>{{ $t('registration.form.validation_sent', { email: credentials.email }) }}</h2>
|
||||||
|
<b-alert show variant="info">{{ $t('registration.form.validation_sent_info') }}</b-alert>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import auth from '@/auth/index';
|
||||||
|
import Gravatar from 'vue-gravatar';
|
||||||
|
import RegisterAvatar from './RegisterAvatar';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
'v-gravatar': Gravatar,
|
||||||
|
'avatar': RegisterAvatar
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.credentials.email = this.email;
|
||||||
|
this.credentials.password = this.password;
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
credentials: {
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
showPassword: false,
|
||||||
|
validationSent: false,
|
||||||
|
state: {
|
||||||
|
email: {
|
||||||
|
status: false,
|
||||||
|
msg: [],
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
status: false,
|
||||||
|
msg: [],
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
status: false,
|
||||||
|
msg: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
password_length: value => value.length > 6 || 'Password must be at least 6 caracters long',
|
||||||
|
required: value => !!value || 'Required.',
|
||||||
|
email: (value) => {
|
||||||
|
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
return pattern.test(value) || 'Invalid e-mail.';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
registerAction(e) {
|
||||||
|
this.resetState();
|
||||||
|
e.preventDefault();
|
||||||
|
auth.signup(JSON.stringify(this.credentials), (data) => {
|
||||||
|
console.log(data);
|
||||||
|
this.validationSent = true;
|
||||||
|
}, (error) => {
|
||||||
|
Promise.resolve(error).then((errormsg) => {
|
||||||
|
console.log(errormsg);
|
||||||
|
this.error.show = true;
|
||||||
|
Object.entries(errormsg.errors.user).forEach(([key, val]) => {
|
||||||
|
console.log(key);
|
||||||
|
console.log(val);
|
||||||
|
this.state[key] = { status: true, msg: val };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetState() {
|
||||||
|
this.state = {
|
||||||
|
email: {
|
||||||
|
status: false,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
status: false,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
status: false,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
host() {
|
||||||
|
return `@${window.location.host}`;
|
||||||
|
},
|
||||||
|
validEmail() {
|
||||||
|
return this.rules.email(this.credentials.email) === true ? 'v-gravatar' : 'avatar';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.avatar-enter-active {
|
||||||
|
transition: opacity 1s ease;
|
||||||
|
}
|
||||||
|
.avatar-enter, .avatar-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-leave {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
9
js/src/components/Account/RegisterAvatar.vue
Normal file
9
js/src/components/Account/RegisterAvatar.vue
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<template>
|
||||||
|
<img class="img-circle elevation-7 mb-1" src="@/assets/profile.svg">
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'RegisterAvatar'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
84
js/src/components/Account/ResendConfirmation.vue
Normal file
84
js/src/components/Account/ResendConfirmation.vue
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<v-container fluid fill-height>
|
||||||
|
<v-layout align-center justify-center>
|
||||||
|
<v-flex xs12 sm8 md4>
|
||||||
|
<v-card class="elevation-12">
|
||||||
|
<v-toolbar dark color="primary">
|
||||||
|
<v-toolbar-title>Resend Instructions</v-toolbar-title>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit="resendConfirmationAction" v-if="!validationSent">
|
||||||
|
<v-text-field
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
v-model="credentials.email"
|
||||||
|
required
|
||||||
|
:state="state.email.status"
|
||||||
|
:rules="[rules.required, rules.email]"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn type="submit" color="blue">Send instructions again</v-btn>
|
||||||
|
</v-form>
|
||||||
|
<div v-else>
|
||||||
|
<h2>Validation email sent to {{ credentials.email }}</h2>
|
||||||
|
<v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import fetchStory from '@/api/eventFetch';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ResendConfirmation',
|
||||||
|
props: {
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
credentials: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
validationSent: false,
|
||||||
|
error: false,
|
||||||
|
state: {
|
||||||
|
email: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
required: value => !!value || 'Required.',
|
||||||
|
email: (value) => {
|
||||||
|
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
return pattern.test(value) || 'Invalid e-mail.';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.credentials.email = this.email;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resendConfirmationAction(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchStory('/users/resend', this.$store, { method: 'POST', body: JSON.stringify(this.credentials) }).then(() => {
|
||||||
|
this.validationSent = true;
|
||||||
|
}).catch((err) => {
|
||||||
|
Promise.resolve(err).then(() => {
|
||||||
|
this.error = true;
|
||||||
|
this.validationSent = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
93
js/src/components/Account/SendPasswordReset.vue
Normal file
93
js/src/components/Account/SendPasswordReset.vue
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
<template>
|
||||||
|
<v-container fluid fill-height>
|
||||||
|
<v-layout align-center justify-center>
|
||||||
|
<v-flex xs12 sm8 md4>
|
||||||
|
<v-card class="elevation-12">
|
||||||
|
<v-toolbar dark color="primary">
|
||||||
|
<v-toolbar-title>Password Reset</v-toolbar-title>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form @submit="resendConfirmationAction" v-if="!validationSent">
|
||||||
|
<v-text-field
|
||||||
|
label="Email"
|
||||||
|
type="email"
|
||||||
|
v-model="credentials.email"
|
||||||
|
required
|
||||||
|
:state="state.email.status"
|
||||||
|
:rules="[rules.required, rules.email]"
|
||||||
|
>
|
||||||
|
</v-text-field>
|
||||||
|
<v-btn type="submit" color="blue">Reset my password</v-btn>
|
||||||
|
</v-form>
|
||||||
|
<div v-else>
|
||||||
|
<h2>Validation email sent to {{ credentials.email }}</h2>
|
||||||
|
<v-alert :value="true" type="info">Please check you spam folder if you didn't receive the email.</v-alert>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-flex>
|
||||||
|
</v-layout>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import fetchStory from '@/api/eventFetch';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SendPasswordReset',
|
||||||
|
props: {
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.credentials.email = this.email;
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
credentials: {
|
||||||
|
email: '',
|
||||||
|
},
|
||||||
|
validationSent: false,
|
||||||
|
error: false,
|
||||||
|
state: {
|
||||||
|
email: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
required: value => !!value || 'Required.',
|
||||||
|
email: (value) => {
|
||||||
|
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
return pattern.test(value) || 'Invalid e-mail.';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resendConfirmationAction(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
fetchStory('/users/password-reset/send', this.$store, { method: 'POST', body: JSON.stringify(this.credentials) }).then(() => {
|
||||||
|
this.error = false;
|
||||||
|
this.validationSent = true;
|
||||||
|
}).catch((err) => {
|
||||||
|
Promise.resolve(err).then((data) => {
|
||||||
|
this.error = true;
|
||||||
|
this.state.email = { status: false, msg: data.errors };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resetState() {
|
||||||
|
this.state = {
|
||||||
|
email: {
|
||||||
|
status: null,
|
||||||
|
msg: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
51
js/src/components/Account/Validate.vue
Normal file
51
js/src/components/Account/Validate.vue
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<template>
|
||||||
|
<b-container>
|
||||||
|
<h1 v-if="loading">{{ $t('registration.validation.process') }}</h1>
|
||||||
|
<div v-else>
|
||||||
|
<div v-if="failed">
|
||||||
|
<b-alert show variant="danger">{{ $t('registration.success.validation_failure') }}</b-alert>
|
||||||
|
</div>
|
||||||
|
<h1 v-else>{{ $t('registration.validation.finished') }}</h1>
|
||||||
|
</div>
|
||||||
|
</b-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import fetchStory from '@/api/eventFetch';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Validate',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
failed: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
token: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.validateAction();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
validateAction() {
|
||||||
|
fetchStory(`/users/validate/${this.token}`, this.$store).then((data) => {
|
||||||
|
this.loading = false;
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
localStorage.setItem('refresh_token', data.refresh_token);
|
||||||
|
this.$store.commit('LOGIN_USER', data.account);
|
||||||
|
this.$snotify.success(this.$t('registration.success.login', { username: data.account.username }));
|
||||||
|
this.$router.push({ name: 'Home' });
|
||||||
|
}).catch((err) => {
|
||||||
|
Promise.resolve(err).then(() => {
|
||||||
|
this.failed = true;
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,204 +1,65 @@
|
||||||
<template>
|
<template>
|
||||||
<v-container fluid grid-list-sm>
|
<v-container fluid fill-height>
|
||||||
<h3>Create a new event</h3>
|
<v-layout align-center justify-center>
|
||||||
<v-form>
|
<v-flex xs12 sm8 md4>
|
||||||
<v-stepper v-model="e1">
|
<v-card class="elevation-12">
|
||||||
<v-stepper-header>
|
<v-toolbar dark color="primary">
|
||||||
<v-stepper-step step="1" :complete="e1 > 1" editable>Basic Informations
|
<v-toolbar-title>Create a new event</v-toolbar-title>
|
||||||
<small>Title and description</small>
|
</v-toolbar>
|
||||||
</v-stepper-step>
|
<v-card-text>
|
||||||
<v-divider></v-divider>
|
<v-form>
|
||||||
<v-stepper-step step="2" :complete="e1 > 2" editable>Date and place</v-stepper-step>
|
|
||||||
<v-divider></v-divider>
|
|
||||||
<v-stepper-step step="3" :complete="e1 > 3">Extra informations</v-stepper-step>
|
|
||||||
</v-stepper-header>
|
|
||||||
<v-stepper-items>
|
|
||||||
<v-stepper-content step="1">
|
|
||||||
<v-layout row wrap>
|
|
||||||
<v-flex xs12>
|
|
||||||
<v-text-field
|
|
||||||
label="Title"
|
|
||||||
v-model="event.title"
|
|
||||||
:counter="100"
|
|
||||||
required
|
|
||||||
></v-text-field>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md6>
|
|
||||||
<v-text-field
|
<v-text-field
|
||||||
label="Description"
|
label="Title"
|
||||||
v-model="event.description"
|
v-model="event.title"
|
||||||
multiLine
|
:counter="100"
|
||||||
required
|
required
|
||||||
></v-text-field>
|
></v-text-field>
|
||||||
</v-flex>
|
<v-radio-group v-model="event.location_type" row>
|
||||||
<v-flex md6>
|
<v-radio label="Address" value="physical" off-icon="place"></v-radio>
|
||||||
<vue-markdown class="markdown-render"
|
<v-radio label="Online" value="online" off-icon="link"></v-radio>
|
||||||
:watches="['show','html','breaks','linkify','emoji','typographer','toc']"
|
<v-radio label="Phone" value="phone" off-icon="phone"></v-radio>
|
||||||
:source="event.description"
|
<v-radio label="Other" value="other"></v-radio>
|
||||||
:show="true" :html="false" :breaks="true" :linkify="true"
|
</v-radio-group>
|
||||||
:emoji="true" :typographer="true" :toc="false"
|
<vuetify-google-autocomplete
|
||||||
></vue-markdown>
|
v-if="event.location_type === 'physical'"
|
||||||
</v-flex>
|
id="map"
|
||||||
<v-flex md12>
|
append-icon="search"
|
||||||
<v-select
|
classname="form-control"
|
||||||
v-bind:items="categories"
|
placeholder="Start typing"
|
||||||
v-model="event.category"
|
label="Location"
|
||||||
item-text="title"
|
enable-geolocation
|
||||||
item-value="@id"
|
types="geocode"
|
||||||
label="Categories"
|
v-on:placechanged="getAddressData"
|
||||||
single-line
|
>
|
||||||
bottom
|
</vuetify-google-autocomplete>
|
||||||
></v-select>
|
<v-text-field
|
||||||
</v-flex>
|
v-if="event.location_type === 'online'"
|
||||||
<v-flex md12>
|
label="Meeting adress"
|
||||||
<!--<v-text-field
|
type="url"
|
||||||
v-model="tagsToSend"
|
v-model="event.url"
|
||||||
label="Tags"
|
:required="event.location_type === 'online'"
|
||||||
></v-text-field>-->
|
></v-text-field>
|
||||||
<v-select
|
<v-text-field
|
||||||
v-model="tagsToSend"
|
v-if="event.location_type === 'phone'"
|
||||||
label="Tags"
|
label="Phone number"
|
||||||
chips
|
type="tel"
|
||||||
tags
|
v-model="event.phone"
|
||||||
:items="tagsFetched"
|
:required="event.location_type === 'phone'"
|
||||||
></v-select>
|
></v-text-field>
|
||||||
</v-flex>
|
<v-autocomplete
|
||||||
</v-layout>
|
:items="categories"
|
||||||
<v-btn color="primary" @click.native="e1 = 2">Next</v-btn>
|
v-model="event.category"
|
||||||
</v-stepper-content>
|
item-text="title"
|
||||||
<v-stepper-content step="2">
|
item-value="id"
|
||||||
Event starts at:
|
label="Categories"
|
||||||
<v-text-field type="datetime-local" v-model="event.begins_on"></v-text-field>
|
>
|
||||||
<!--<v-layout row wrap>
|
</v-autocomplete>
|
||||||
<v-flex md6>
|
<v-btn color="primary" @click="create">Create event</v-btn>
|
||||||
<v-dialog
|
</v-form>
|
||||||
persistent
|
</v-card-text>
|
||||||
v-model="modals.beginning.date"
|
</v-card>
|
||||||
lazy
|
</v-flex>
|
||||||
full-width
|
</v-layout>
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
slot="activator"
|
|
||||||
label="Beginning of the event date"
|
|
||||||
v-model="event.startDate.date"
|
|
||||||
prepend-icon="event"
|
|
||||||
readonly
|
|
||||||
></v-text-field>
|
|
||||||
<v-date-picker v-model="event.startDate.date" scrollable dateFormat="val => new Date(val).">
|
|
||||||
<template scope="{ save, cancel }">
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
|
|
||||||
<v-btn flat primary @click.native="save()">Save</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
</v-date-picker>
|
|
||||||
</v-dialog>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md6>
|
|
||||||
<v-dialog
|
|
||||||
persistent
|
|
||||||
v-model="modals.beginning.time"
|
|
||||||
lazy
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
slot="activator"
|
|
||||||
label="Beginning of the event time"
|
|
||||||
v-model="event.startDate.time"
|
|
||||||
prepend-icon="access_time"
|
|
||||||
readonly
|
|
||||||
></v-text-field>
|
|
||||||
<v-time-picker v-model="event.startDate.time" actions format="24h">
|
|
||||||
<template scope="{ save, cancel }">
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
|
|
||||||
<v-btn flat primary @click.native="save()">Save</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
</v-time-picker>
|
|
||||||
</v-dialog>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>-->
|
|
||||||
Event ends at:
|
|
||||||
<v-text-field type="datetime-local" v-model="event.ends_on"></v-text-field>
|
|
||||||
<!--<v-layout row wrap>
|
|
||||||
<v-flex md6>
|
|
||||||
<v-dialog
|
|
||||||
persistent
|
|
||||||
v-model="modals.end.date"
|
|
||||||
lazy
|
|
||||||
full-width
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
slot="activator"
|
|
||||||
label="End of the event date"
|
|
||||||
v-model="event.endDate.date"
|
|
||||||
prepend-icon="event"
|
|
||||||
readonly
|
|
||||||
></v-text-field>
|
|
||||||
<v-date-picker v-model="event.endDate.date" scrollable >
|
|
||||||
<template scope="{ save, cancel }">
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
|
|
||||||
<v-btn flat primary @click.native="save()">Save</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
</v-date-picker>
|
|
||||||
</v-dialog>
|
|
||||||
</v-flex>
|
|
||||||
<v-flex md6>
|
|
||||||
<v-dialog
|
|
||||||
persistent
|
|
||||||
v-model="modals.end.time"
|
|
||||||
lazy
|
|
||||||
>
|
|
||||||
<v-text-field
|
|
||||||
slot="activator"
|
|
||||||
label="End of the event time"
|
|
||||||
v-model="event.endDate.time"
|
|
||||||
prepend-icon="access_time"
|
|
||||||
readonly
|
|
||||||
></v-text-field>
|
|
||||||
<v-time-picker v-model="event.endDate.time" format="24h" actions >
|
|
||||||
<template scope="{ save, cancel }">
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn flat primary @click.native="cancel()">Cancel</v-btn>
|
|
||||||
<v-btn flat primary @click.native="save()">Save</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</template>
|
|
||||||
</v-time-picker>
|
|
||||||
</v-dialog>
|
|
||||||
</v-flex>
|
|
||||||
</v-layout>-->
|
|
||||||
|
|
||||||
<vuetify-google-autocomplete
|
|
||||||
id="map"
|
|
||||||
append-icon="search"
|
|
||||||
classname="form-control"
|
|
||||||
placeholder="Start typing"
|
|
||||||
label="Location"
|
|
||||||
enable-geolocation
|
|
||||||
types="geocode"
|
|
||||||
v-on:placechanged="getAddressData"
|
|
||||||
>
|
|
||||||
</vuetify-google-autocomplete>
|
|
||||||
<v-btn color="primary" @click.native="e1 = 3">Next</v-btn>
|
|
||||||
</v-stepper-content>
|
|
||||||
<v-stepper-content step="3">
|
|
||||||
<v-text-field
|
|
||||||
label="Number of seats"
|
|
||||||
v-model="event.seats"
|
|
||||||
></v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="Price"
|
|
||||||
prefix="$"
|
|
||||||
type="float"
|
|
||||||
v-model="event.price"
|
|
||||||
></v-text-field>
|
|
||||||
</v-stepper-content>
|
|
||||||
</v-stepper-items>
|
|
||||||
</v-stepper>
|
|
||||||
</v-form>
|
|
||||||
<v-btn color="primary" @click="create">Create event</v-btn>
|
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -219,29 +80,18 @@
|
||||||
return {
|
return {
|
||||||
e1: 0,
|
e1: 0,
|
||||||
event: {
|
event: {
|
||||||
title: '',
|
title: null,
|
||||||
description: '',
|
description: null,
|
||||||
begins_on: new Date(),
|
begins_on: new Date(),
|
||||||
ends_on: new Date(),
|
ends_on: new Date(),
|
||||||
seats: 0,
|
seats: null,
|
||||||
address: {
|
physical_address: null,
|
||||||
description: null,
|
location_type: 'physical',
|
||||||
floor: null,
|
online_address: null,
|
||||||
geo: {
|
tel_num: null,
|
||||||
type: null,
|
price: null,
|
||||||
data: {
|
|
||||||
latitude: null,
|
|
||||||
longitude: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
addressCountry: null,
|
|
||||||
addressLocality: null,
|
|
||||||
addressRegion: null,
|
|
||||||
postalCode: null,
|
|
||||||
streetAddress: null,
|
|
||||||
},
|
|
||||||
price: 0,
|
|
||||||
category: null,
|
category: null,
|
||||||
|
category_id: null,
|
||||||
tags: [],
|
tags: [],
|
||||||
participants: [],
|
participants: [],
|
||||||
},
|
},
|
||||||
|
@ -262,31 +112,35 @@
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
create() {
|
create() {
|
||||||
this.event.seats = parseInt(this.event.seats, 10);
|
// this.event.seats = parseInt(this.event.seats, 10);
|
||||||
this.tagsToSend.forEach((tag) => {
|
// this.tagsToSend.forEach((tag) => {
|
||||||
this.event.tags.push({
|
// this.event.tags.push({
|
||||||
title: tag,
|
// title: tag,
|
||||||
// '@type': 'Tag',
|
// // '@type': 'Tag',
|
||||||
});
|
// });
|
||||||
});
|
// });
|
||||||
this.event.category_id = this.event.category.id;
|
this.event.category_id = this.event.category;
|
||||||
this.event.organizer_actor_id = this.$store.state.user.actor.id;
|
this.event.organizer_actor_id = this.$store.state.user.actor.id;
|
||||||
this.event.participants = [this.$store.state.user.actor.id];
|
this.event.participants = [this.$store.state.user.actor.id];
|
||||||
this.event.price = parseFloat(this.event.price);
|
// this.event.price = parseFloat(this.event.price);
|
||||||
|
|
||||||
if (this.id === undefined) {
|
if (this.id === undefined) {
|
||||||
eventFetch('/events', this.$store, {method: 'POST', body: JSON.stringify({ event: this.event })})
|
eventFetch('/events', this.$store, {method: 'POST', body: JSON.stringify({ event: this.event })})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.$router.push({name: 'Event', params: {id: data.id}});
|
this.$router.push({name: 'Event', params: {uuid: data.uuid}});
|
||||||
|
}).catch((err) => {
|
||||||
|
Promise.resolve(err).then((err) => {
|
||||||
|
console.log('err creation', err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
eventFetch(`/events/${this.id}`, this.$store, {method: 'PUT', body: JSON.stringify(this.event)})
|
eventFetch(`/events/${this.uuid}`, this.$store, {method: 'PUT', body: JSON.stringify(this.event)})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.$router.push({name: 'Event', params: {id: data.id}});
|
this.$router.push({name: 'Event', params: {uuid: data.uuid}});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.event.tags = [];
|
this.event.tags = [];
|
||||||
|
|
|
@ -48,10 +48,10 @@
|
||||||
>
|
>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
|
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name ? event.organizer.display_name : event.organizer.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
<vue-markdown :source="event.description" />
|
<vue-markdown :source="event.description" v-if="event.description" />
|
||||||
</p>
|
</p>
|
||||||
<!--<p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey--text">{{ event.organizer.username }}</span></router-link> organises {{ event.title }} <span v-if="event.address.addressLocality">in {{ event.address.addressLocality }}</span> on the {{ event.startDate | formatDate }}.</p>
|
<!--<p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey--text">{{ event.organizer.username }}</span></router-link> organises {{ event.title }} <span v-if="event.address.addressLocality">in {{ event.address.addressLocality }}</span> on the {{ event.startDate | formatDate }}.</p>
|
||||||
<v-card-text v-if="event.description"><vue-markdown :source="event.description"></vue-markdown></v-card-text>-->
|
<v-card-text v-if="event.description"><vue-markdown :source="event.description"></vue-markdown></v-card-text>-->
|
||||||
|
|
|
@ -19,38 +19,38 @@
|
||||||
<v-layout>
|
<v-layout>
|
||||||
<v-flex xs12 sm8 offset-sm2>
|
<v-flex xs12 sm8 offset-sm2>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-layout row wrap>
|
<v-layout row wrap>
|
||||||
<v-flex xs4 v-for="event in events" :key="event.uuid">
|
<v-flex xs4 v-for="event in events" :key="event.uuid">
|
||||||
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
|
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
|
||||||
<v-card-media v-if="!event.image"
|
<v-card-media v-if="!event.image"
|
||||||
class="white--text"
|
class="white--text"
|
||||||
height="200px"
|
height="200px"
|
||||||
src="https://picsum.photos/g/400/200/"
|
src="https://picsum.photos/g/400/200/"
|
||||||
>
|
>
|
||||||
<v-container fill-height fluid>
|
<v-container fill-height fluid>
|
||||||
<v-layout fill-height>
|
<v-layout fill-height>
|
||||||
<v-flex xs12 align-end flexbox>
|
<v-flex xs12 align-end flexbox>
|
||||||
<span class="headline black--text">{{ event.title }}</span>
|
<span class="headline black--text">{{ event.title }}</span>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-container>
|
</v-container>
|
||||||
</v-card-media>
|
</v-card-media>
|
||||||
<v-card-title primary-title>
|
<v-card-title primary-title>
|
||||||
<div>
|
<div>
|
||||||
<span class="grey--text">{{ event.begins_on | formatDate }}</span><br>
|
<span class="grey--text">{{ event.begins_on | formatDate }}</span><br>
|
||||||
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
|
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
|
||||||
<v-avatar size="25px">
|
<v-avatar size="25px">
|
||||||
<img class="img-circle elevation-7 mb-1"
|
<img class="img-circle elevation-7 mb-1"
|
||||||
:src="event.organizer.avatar"
|
:src="event.organizer.avatar"
|
||||||
>
|
>
|
||||||
</v-avatar>
|
</v-avatar>
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
|
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-flex>
|
</v-flex>
|
||||||
</v-layout>
|
</v-layout>
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-form>
|
|
||||||
<v-text-field
|
|
||||||
label="Email"
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
v-model="credentials.email"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="password"
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
v-model="credentials.password"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-btn @click="loginAction" color="blue">Login</v-btn>
|
|
||||||
</v-form>
|
|
||||||
<v-snackbar
|
|
||||||
:timeout="error.timeout"
|
|
||||||
:error="true"
|
|
||||||
v-model="error.show"
|
|
||||||
>
|
|
||||||
{{ error.text }}
|
|
||||||
<v-btn dark flat @click.native="error.show = false">Close</v-btn>
|
|
||||||
</v-snackbar>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import auth from '@/auth/index';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
|
|
||||||
beforeCreate() {
|
|
||||||
if (this.$store.state.user) {
|
|
||||||
this.$router.push('/');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
credentials: {
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
show: false,
|
|
||||||
text: '',
|
|
||||||
timeout: 3000,
|
|
||||||
field: {
|
|
||||||
email: false,
|
|
||||||
password: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
required: value => !!value || 'Required.',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
loginAction(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
auth.login(JSON.stringify(this.credentials), (data) => {
|
|
||||||
this.$store.commit('LOGIN_USER', data.user);
|
|
||||||
this.$router.push({ name: 'Home' });
|
|
||||||
}, (error) => {
|
|
||||||
Promise.resolve(error).then((errorMsg) => {
|
|
||||||
console.log(errorMsg);
|
|
||||||
this.error.show = true;
|
|
||||||
this.error.text = this.$t(errorMsg.display_error);
|
|
||||||
}).catch((e) => {
|
|
||||||
console.log(e);
|
|
||||||
this.error.show = true;
|
|
||||||
this.error.text = e.message;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -3,24 +3,23 @@
|
||||||
class="blue darken-3"
|
class="blue darken-3"
|
||||||
dark
|
dark
|
||||||
app
|
app
|
||||||
clipped-left
|
:clipped-left="$vuetify.breakpoint.lgAndUp"
|
||||||
fixed
|
fixed
|
||||||
>
|
>
|
||||||
<v-toolbar-title style="width: 300px" class="ml-0 pl-3">
|
<v-toolbar-title style="width: 300px" class="ml-0 pl-3 white--text">
|
||||||
<v-toolbar-side-icon @click.stop="drawer = !drawer"></v-toolbar-side-icon>
|
<v-toolbar-side-icon @click.stop="toggleDrawer()"></v-toolbar-side-icon>
|
||||||
<router-link :to="{ name: 'Home' }">
|
<router-link :to="{ name: 'Home' }" class="hidden-sm-and-down white--text">Eventos
|
||||||
Eventos
|
|
||||||
</router-link>
|
</router-link>
|
||||||
</v-toolbar-title>
|
</v-toolbar-title>
|
||||||
<v-select
|
<v-autocomplete
|
||||||
autocomplete
|
|
||||||
:loading="searchElement.loading"
|
:loading="searchElement.loading"
|
||||||
light
|
flat
|
||||||
solo
|
solo-inverted
|
||||||
prepend-icon="search"
|
prepend-icon="search"
|
||||||
placeholder="Search"
|
label="Search"
|
||||||
required
|
required
|
||||||
item-text="displayedText"
|
item-text="displayedText"
|
||||||
|
class="hidden-sm-and-down"
|
||||||
:items="searchElement.items"
|
:items="searchElement.items"
|
||||||
:search-input.sync="search"
|
:search-input.sync="search"
|
||||||
v-model="searchSelect"
|
v-model="searchSelect"
|
||||||
|
@ -39,7 +38,7 @@
|
||||||
</v-list-tile-content>
|
</v-list-tile-content>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</v-select>
|
</v-autocomplete>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-menu
|
<v-menu
|
||||||
offset-y
|
offset-y
|
||||||
|
@ -82,6 +81,12 @@
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'NavBar',
|
name: 'NavBar',
|
||||||
|
props: {
|
||||||
|
toggleDrawer: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
notificationMenu: false,
|
notificationMenu: false,
|
||||||
|
@ -165,3 +170,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
nav.v-toolbar .v-input__slot {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,87 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<v-form>
|
|
||||||
<v-text-field
|
|
||||||
label="Username"
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
v-model="credentials.username"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="email"
|
|
||||||
required
|
|
||||||
type="email"
|
|
||||||
v-model="credentials.email"
|
|
||||||
:rules="[rules.required, rules.email]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-text-field
|
|
||||||
label="password"
|
|
||||||
required
|
|
||||||
type="password"
|
|
||||||
v-model="credentials.password"
|
|
||||||
:rules="[rules.required]"
|
|
||||||
>
|
|
||||||
</v-text-field>
|
|
||||||
<v-btn @click="registerAction" color="primary">Register</v-btn>
|
|
||||||
</v-form>
|
|
||||||
<v-snackbar
|
|
||||||
:timeout="error.timeout"
|
|
||||||
:error="true"
|
|
||||||
v-model="error.show"
|
|
||||||
>
|
|
||||||
{{ error.text }}
|
|
||||||
<v-btn dark flat @click.native="error.show = false">Close</v-btn>
|
|
||||||
</v-snackbar>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
|
|
||||||
import auth from '@/auth/index';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
credentials: {
|
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
},
|
|
||||||
error: {
|
|
||||||
show: false,
|
|
||||||
text: '',
|
|
||||||
timeout: 3000,
|
|
||||||
field: {
|
|
||||||
username: false,
|
|
||||||
email: false,
|
|
||||||
password: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
required: value => !!value || 'Required.',
|
|
||||||
email: (value) => {
|
|
||||||
const pattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
|
||||||
return pattern.test(value) || 'Invalid e-mail.';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
registerAction(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
auth.signup(JSON.stringify(this.credentials), (response) => {
|
|
||||||
console.log(response);
|
|
||||||
this.$store.commit('LOGIN_USER', response.user);
|
|
||||||
this.$router.push({ name: 'Home' });
|
|
||||||
}, (error) => {
|
|
||||||
this.error.show = true;
|
|
||||||
this.error.text = error.message;
|
|
||||||
this.error.field[error.field] = true;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
|
@ -8,8 +8,12 @@ import Location from '@/components/Location';
|
||||||
import CreateEvent from '@/components/Event/Create';
|
import CreateEvent from '@/components/Event/Create';
|
||||||
import CategoryList from '@/components/Category/List';
|
import CategoryList from '@/components/Category/List';
|
||||||
import CreateCategory from '@/components/Category/Create';
|
import CreateCategory from '@/components/Category/Create';
|
||||||
import Register from '@/components/Register';
|
import Register from '@/components/Account/Register';
|
||||||
import Login from '@/components/Login';
|
import Login from '@/components/Account/Login';
|
||||||
|
import Validate from '@/components/Account/Validate';
|
||||||
|
import ResendConfirmation from '@/components/Account/ResendConfirmation';
|
||||||
|
import SendPasswordReset from '@/components/Account/SendPasswordReset';
|
||||||
|
import PasswordReset from '@/components/Account/PasswordReset';
|
||||||
import Account from '@/components/Account/Account';
|
import Account from '@/components/Account/Account';
|
||||||
import CreateGroup from '@/components/Group/Create';
|
import CreateGroup from '@/components/Group/Create';
|
||||||
import Group from '@/components/Group/Group';
|
import Group from '@/components/Group/Group';
|
||||||
|
@ -68,12 +72,42 @@ const router = new Router({
|
||||||
path: '/register',
|
path: '/register',
|
||||||
name: 'Register',
|
name: 'Register',
|
||||||
component: Register,
|
component: Register,
|
||||||
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/resend-instructions',
|
||||||
|
name: 'ResendConfirmation',
|
||||||
|
component: ResendConfirmation,
|
||||||
|
props: true,
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/password-reset/send',
|
||||||
|
name: 'SendPasswordReset',
|
||||||
|
component: SendPasswordReset,
|
||||||
|
props: true,
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/password-reset/:token',
|
||||||
|
name: 'PasswordReset',
|
||||||
|
component: PasswordReset,
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/validate/:token',
|
||||||
|
name: 'Validate',
|
||||||
|
component: Validate,
|
||||||
|
props: true,
|
||||||
|
meta: { requiresAuth: false },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/login',
|
path: '/login',
|
||||||
name: 'Login',
|
name: 'Login',
|
||||||
component: Login,
|
component: Login,
|
||||||
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -109,7 +143,8 @@ const router = new Router({
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false },
|
||||||
},
|
},
|
||||||
{ path: "*",
|
{
|
||||||
|
path: '*',
|
||||||
name: 'PageNotFound',
|
name: 'PageNotFound',
|
||||||
component: PageNotFound,
|
component: PageNotFound,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false },
|
||||||
|
|
|
@ -2,7 +2,7 @@ const Dotenv = require('dotenv-webpack');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
lintOnSave: false,
|
lintOnSave: false,
|
||||||
compiler: true,
|
runtimeCompiler: true,
|
||||||
configureWebpack: {
|
configureWebpack: {
|
||||||
plugins: [
|
plugins: [
|
||||||
new Dotenv(),
|
new Dotenv(),
|
||||||
|
|
|
@ -312,8 +312,12 @@ defmodule Eventos.Actors do
|
||||||
Get an user by email
|
Get an user by email
|
||||||
"""
|
"""
|
||||||
def find_by_email(email) do
|
def find_by_email(email) do
|
||||||
user = Repo.get_by(User, email: email)
|
case Repo.get_by(User, email: email) |> Repo.preload(:actor) do
|
||||||
Repo.preload(user, :actor)
|
nil ->
|
||||||
|
{:error, nil}
|
||||||
|
user ->
|
||||||
|
{:ok, user}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -366,8 +370,7 @@ defmodule Eventos.Actors do
|
||||||
|
|
||||||
try do
|
try do
|
||||||
Eventos.Repo.insert!(actor_with_user)
|
Eventos.Repo.insert!(actor_with_user)
|
||||||
user = find_by_email(email)
|
find_by_email(email)
|
||||||
{:ok, user}
|
|
||||||
rescue
|
rescue
|
||||||
e in Ecto.InvalidChangesetError ->
|
e in Ecto.InvalidChangesetError ->
|
||||||
{:error, e.changeset}
|
{:error, e.changeset}
|
||||||
|
|
29
lib/eventos/actors/service/activation.ex
Normal file
29
lib/eventos/actors/service/activation.ex
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
defmodule Eventos.Actors.Service.Activation do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
alias Eventos.{Mailer, Repo, Actors.User, Actors}
|
||||||
|
alias Eventos.Email.User, as: UserEmail
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def check_confirmation_token(token) when is_binary(token) do
|
||||||
|
with %User{} = user <- Repo.get_by(User, confirmation_token: token) do
|
||||||
|
Actors.update_user(user, %{"confirmed_at" => DateTime.utc_now(), "confirmation_sent_at" => nil, "confirmation_token" => nil})
|
||||||
|
else
|
||||||
|
_err ->
|
||||||
|
{:error, "Invalid token"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resend_confirmation_email(%User{} = user, locale \\ "en") do
|
||||||
|
{:ok, user} = Actors.update_user(user, %{"confirmation_sent_at" => DateTime.utc_now()})
|
||||||
|
send_confirmation_email(user, locale)
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_confirmation_email(%User{} = user, locale \\ "en") do
|
||||||
|
user
|
||||||
|
|> UserEmail.confirmation_email(locale)
|
||||||
|
|> Mailer.deliver_later()
|
||||||
|
end
|
||||||
|
end
|
57
lib/eventos/actors/service/reset_password.ex
Normal file
57
lib/eventos/actors/service/reset_password.ex
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
defmodule Eventos.Actors.Service.ResetPassword do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
alias Eventos.{Mailer, Repo, Actors.User}
|
||||||
|
alias Eventos.Email.User, as: UserEmail
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Check that the provided token is correct and update provided password
|
||||||
|
"""
|
||||||
|
@spec check_reset_password_token(String.t, String.t) :: tuple
|
||||||
|
def check_reset_password_token(password, token) do
|
||||||
|
with %User{} = user <- Repo.get_by(User, reset_password_token: token) do
|
||||||
|
User.password_reset_changeset(user, %{"password" => password, "reset_password_sent_at" => nil, "reset_password_token" => nil}) |> Repo.update()
|
||||||
|
else
|
||||||
|
_err ->
|
||||||
|
{:error, :invalid_token}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Send the email reset password, if it's not too soon since the last send
|
||||||
|
"""
|
||||||
|
@spec send_password_reset_email(User.t, String.t) :: tuple
|
||||||
|
def send_password_reset_email(%User{} = user, locale \\ "en") do
|
||||||
|
with :ok <- we_can_send_email(user),
|
||||||
|
{:ok, %User{} = user_updated} <- User.send_password_reset_changeset(user, %{"reset_password_token" => random_string(30), "reset_password_sent_at" => DateTime.utc_now()}) |> Repo.update() do
|
||||||
|
mail = user_updated
|
||||||
|
|> UserEmail.reset_password_email(locale)
|
||||||
|
|> Mailer.deliver_later()
|
||||||
|
{:ok, mail}
|
||||||
|
else
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec random_string(integer) :: String.t
|
||||||
|
defp random_string(length) do
|
||||||
|
:crypto.strong_rand_bytes(length) |> Base.url_encode64
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec we_can_send_email(User.t) :: boolean
|
||||||
|
defp we_can_send_email(%User{} = user) do
|
||||||
|
case user.reset_password_sent_at do
|
||||||
|
nil ->
|
||||||
|
:ok
|
||||||
|
_ ->
|
||||||
|
case Timex.before?(Timex.shift(user.reset_password_sent_at, hours: 1), DateTime.utc_now()) do
|
||||||
|
true ->
|
||||||
|
:ok
|
||||||
|
false ->
|
||||||
|
{:error, :email_too_soon}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -12,6 +12,11 @@ defmodule Eventos.Actors.User do
|
||||||
field :password, :string, virtual: true
|
field :password, :string, virtual: true
|
||||||
field :role, :integer, default: 0
|
field :role, :integer, default: 0
|
||||||
belongs_to :actor, Actor
|
belongs_to :actor, Actor
|
||||||
|
field :confirmed_at, :utc_datetime
|
||||||
|
field :confirmation_sent_at, :utc_datetime
|
||||||
|
field :confirmation_token, :string
|
||||||
|
field :reset_password_sent_at, :utc_datetime
|
||||||
|
field :reset_password_token, :string
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
@ -19,18 +24,49 @@ defmodule Eventos.Actors.User do
|
||||||
@doc false
|
@doc false
|
||||||
def changeset(%User{} = user, attrs) do
|
def changeset(%User{} = user, attrs) do
|
||||||
user
|
user
|
||||||
|> cast(attrs, [:email, :role, :password_hash, :actor_id])
|
|> cast(attrs, [:email, :role, :password_hash, :actor_id, :confirmed_at, :confirmation_sent_at, :confirmation_token, :reset_password_sent_at, :reset_password_token])
|
||||||
|> validate_required([:email])
|
|> validate_required([:email])
|
||||||
|> unique_constraint(:email)
|
|> unique_constraint(:email, [message: "registration.error.email_already_used"])
|
||||||
|> validate_format(:email, ~r/@/)
|
|> validate_format(:email, ~r/@/)
|
||||||
|
|> validate_length(:password, min: 6, max: 100, message: "registration.error.password_too_short")
|
||||||
end
|
end
|
||||||
|
|
||||||
def registration_changeset(struct, params) do
|
def registration_changeset(struct, params) do
|
||||||
struct
|
struct
|
||||||
|> changeset(params)
|
|> changeset(params)
|
||||||
|> cast(params, ~w(password)a, [])
|
|> cast(params, ~w(password)a, [])
|
||||||
|> validate_length(:password, min: 6, max: 100)
|
|> validate_required([:email, :password])
|
||||||
|
|> validate_length(:password, min: 6, max: 100, message: "registration.error.password_too_short")
|
||||||
|> hash_password()
|
|> hash_password()
|
||||||
|
|> save_confirmation_token()
|
||||||
|
|> unique_constraint(:confirmation_token, [message: "regisration.error.confirmation_token_already_in_use"])
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_password_reset_changeset(%User{} = user, attrs) do
|
||||||
|
user
|
||||||
|
|> cast(attrs, [:reset_password_token, :reset_password_sent_at])
|
||||||
|
end
|
||||||
|
|
||||||
|
def password_reset_changeset(%User{} = user, attrs) do
|
||||||
|
user
|
||||||
|
|> cast(attrs, [:password, :reset_password_token, :reset_password_sent_at])
|
||||||
|
|> validate_length(:password, min: 6, max: 100, message: "registration.error.password_too_short")
|
||||||
|
|> hash_password()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp save_confirmation_token(changeset) do
|
||||||
|
case changeset do
|
||||||
|
%Ecto.Changeset{valid?: true,
|
||||||
|
changes: %{email: _email}} ->
|
||||||
|
changeset = put_change(changeset, :confirmation_token, random_string(30))
|
||||||
|
put_change(changeset, :confirmation_sent_at, DateTime.utc_now())
|
||||||
|
_ ->
|
||||||
|
changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp random_string(length) do
|
||||||
|
:crypto.strong_rand_bytes(length) |> Base.url_encode64
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -47,4 +83,12 @@ defmodule Eventos.Actors.User do
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_confirmed(%User{confirmed_at: nil} = _user) do
|
||||||
|
{:error, :unconfirmed}
|
||||||
|
end
|
||||||
|
|
||||||
|
def is_confirmed(%User{} = user) do
|
||||||
|
{:ok, user}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,6 +26,5 @@ defmodule Eventos.Addresses.Address do
|
||||||
def changeset(%Address{} = address, attrs) do
|
def changeset(%Address{} = address, attrs) do
|
||||||
address
|
address
|
||||||
|> cast(attrs, [:description, :floor, :geom, :addressCountry, :addressLocality, :addressRegion, :postalCode, :streetAddress])
|
|> cast(attrs, [:description, :floor, :geom, :addressCountry, :addressLocality, :addressRegion, :postalCode, :streetAddress])
|
||||||
|> validate_required([:streetAddress])
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
46
lib/eventos/email/user.ex
Normal file
46
lib/eventos/email/user.ex
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
defmodule Eventos.Email.User do
|
||||||
|
|
||||||
|
alias Eventos.Actors.User
|
||||||
|
|
||||||
|
import Bamboo.Email
|
||||||
|
import Bamboo.Phoenix
|
||||||
|
use Bamboo.Phoenix, view: Eventos.EmailView
|
||||||
|
import EventosWeb.Gettext
|
||||||
|
|
||||||
|
def confirmation_email(%User{} = user, locale \\ "en") do
|
||||||
|
Gettext.put_locale(locale)
|
||||||
|
instance_url = get_config(:instance)
|
||||||
|
base_email()
|
||||||
|
|> to(user.email)
|
||||||
|
|> subject(gettext "Peakweaver: Confirmation instructions for %{instance}", instance: instance_url)
|
||||||
|
|> put_header("Reply-To", get_config(:reply_to))
|
||||||
|
|> assign(:token, user.confirmation_token)
|
||||||
|
|> assign(:instance, instance_url)
|
||||||
|
|> render(:registration_confirmation)
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_password_email(%User{} = user, locale \\ "en") do
|
||||||
|
Gettext.put_locale(locale)
|
||||||
|
instance_url = get_config(:instance)
|
||||||
|
base_email()
|
||||||
|
|> to(user.email)
|
||||||
|
|> subject(gettext "Peakweaver: Reset your password on %{instance} instructions", instance: instance_url)
|
||||||
|
|> put_header("Reply-To", get_config(:reply_to))
|
||||||
|
|> assign(:token, user.reset_password_token)
|
||||||
|
|> assign(:instance, instance_url)
|
||||||
|
|> render(:password_reset)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp base_email do
|
||||||
|
# Here you can set a default from, default headers, etc.
|
||||||
|
new_email()
|
||||||
|
|> from(Application.get_env(:eventos, EventosWeb.Endpoint)[:email_from])
|
||||||
|
|> put_html_layout({Eventos.EmailView, "email.html"})
|
||||||
|
|> put_text_layout({Eventos.EmailView, "email.text"})
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec get_config(atom()) :: any()
|
||||||
|
defp get_config(key) do
|
||||||
|
_config = Application.get_env(:eventos, EventosWeb.Endpoint) |> Keyword.get(key)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,30 +1,5 @@
|
||||||
defmodule Eventos.Events.Event.TitleSlug do
|
import EctoEnum
|
||||||
@moduledoc """
|
defenum AddressTypeEnum, :address_type, [:physical, :url, :phone, :other]
|
||||||
Generates a slug for an event title
|
|
||||||
"""
|
|
||||||
alias Eventos.Events.Event
|
|
||||||
import Ecto.Query
|
|
||||||
alias Eventos.Repo
|
|
||||||
use EctoAutoslugField.Slug, from: :title, to: :slug
|
|
||||||
|
|
||||||
def build_slug(sources, changeset) do
|
|
||||||
slug = super(sources, changeset)
|
|
||||||
build_unique_slug(slug, changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp build_unique_slug(slug, changeset) do
|
|
||||||
query = from e in Event,
|
|
||||||
where: e.slug == ^slug
|
|
||||||
|
|
||||||
case Repo.one(query) do
|
|
||||||
nil -> slug
|
|
||||||
_event ->
|
|
||||||
slug
|
|
||||||
|> Eventos.Slug.increment_slug()
|
|
||||||
|> build_unique_slug(changeset)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defmodule Eventos.Events.Event do
|
defmodule Eventos.Events.Event do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
|
@ -33,7 +8,6 @@ defmodule Eventos.Events.Event do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
alias Eventos.Events.{Event, Participant, Tag, Category, Session, Track}
|
alias Eventos.Events.{Event, Participant, Tag, Category, Session, Track}
|
||||||
alias Eventos.Events.Event.TitleSlug
|
|
||||||
alias Eventos.Actors.Actor
|
alias Eventos.Actors.Actor
|
||||||
alias Eventos.Addresses.Address
|
alias Eventos.Addresses.Address
|
||||||
|
|
||||||
|
@ -44,7 +18,6 @@ defmodule Eventos.Events.Event do
|
||||||
field :description, :string
|
field :description, :string
|
||||||
field :ends_on, Timex.Ecto.DateTimeWithTimezone
|
field :ends_on, Timex.Ecto.DateTimeWithTimezone
|
||||||
field :title, :string
|
field :title, :string
|
||||||
field :slug, TitleSlug.Type
|
|
||||||
field :state, :integer, default: 0
|
field :state, :integer, default: 0
|
||||||
field :status, :integer, default: 0
|
field :status, :integer, default: 0
|
||||||
field :public, :boolean, default: true
|
field :public, :boolean, default: true
|
||||||
|
@ -52,6 +25,9 @@ defmodule Eventos.Events.Event do
|
||||||
field :large_image, :string
|
field :large_image, :string
|
||||||
field :publish_at, Timex.Ecto.DateTimeWithTimezone
|
field :publish_at, Timex.Ecto.DateTimeWithTimezone
|
||||||
field :uuid, Ecto.UUID, default: Ecto.UUID.generate()
|
field :uuid, Ecto.UUID, default: Ecto.UUID.generate()
|
||||||
|
field :address_type, AddressTypeEnum, default: :physical
|
||||||
|
field :online_address, :string
|
||||||
|
field :phone, :string
|
||||||
belongs_to :organizer_actor, Actor, [foreign_key: :organizer_actor_id]
|
belongs_to :organizer_actor, Actor, [foreign_key: :organizer_actor_id]
|
||||||
belongs_to :attributed_to, Actor, [foreign_key: :attributed_to_id]
|
belongs_to :attributed_to, Actor, [foreign_key: :attributed_to_id]
|
||||||
many_to_many :tags, Tag, join_through: "events_tags"
|
many_to_many :tags, Tag, join_through: "events_tags"
|
||||||
|
@ -59,7 +35,7 @@ defmodule Eventos.Events.Event do
|
||||||
many_to_many :participants, Actor, join_through: Participant
|
many_to_many :participants, Actor, join_through: Participant
|
||||||
has_many :tracks, Track
|
has_many :tracks, Track
|
||||||
has_many :sessions, Session
|
has_many :sessions, Session
|
||||||
belongs_to :address, Address
|
belongs_to :physical_address, Address
|
||||||
|
|
||||||
timestamps(type: :utc_datetime)
|
timestamps(type: :utc_datetime)
|
||||||
end
|
end
|
||||||
|
@ -75,13 +51,28 @@ defmodule Eventos.Events.Event do
|
||||||
""
|
""
|
||||||
end
|
end
|
||||||
event
|
event
|
||||||
|> cast(attrs, [:title, :description, :url, :begins_on, :ends_on, :organizer_actor_id, :category_id, :state, :status, :public, :thumbnail, :large_image, :publish_at])
|
|> Ecto.Changeset.cast(attrs, [
|
||||||
|
:title,
|
||||||
|
:description,
|
||||||
|
:url,
|
||||||
|
:begins_on,
|
||||||
|
:ends_on,
|
||||||
|
:organizer_actor_id,
|
||||||
|
:category_id,
|
||||||
|
:state,
|
||||||
|
:status,
|
||||||
|
:public,
|
||||||
|
:thumbnail,
|
||||||
|
:large_image,
|
||||||
|
:publish_at,
|
||||||
|
:address_type,
|
||||||
|
:online_address,
|
||||||
|
:phone,
|
||||||
|
])
|
||||||
|> cast_assoc(:tags)
|
|> cast_assoc(:tags)
|
||||||
|> cast_assoc(:address)
|
|> cast_assoc(:physical_address)
|
||||||
|> TitleSlug.maybe_generate_slug()
|
|
||||||
|> TitleSlug.unique_constraint()
|
|
||||||
|> put_change(:uuid, uuid)
|
|> put_change(:uuid, uuid)
|
||||||
|> put_change(:url, "#{EventosWeb.Endpoint.url()}/@#{actor_url}/#{uuid}")
|
|> put_change(:url, "#{EventosWeb.Endpoint.url()}/@#{actor_url}/#{uuid}")
|
||||||
|> validate_required([:title, :description, :begins_on, :ends_on, :organizer_actor_id, :category_id, :url, :uuid])
|
|> validate_required([:title, :begins_on, :ends_on, :organizer_actor_id, :category_id, :url, :uuid, :address_type])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,7 +32,7 @@ defmodule Eventos.Events do
|
||||||
limit: ^limit,
|
limit: ^limit,
|
||||||
order_by: [desc: :id],
|
order_by: [desc: :id],
|
||||||
offset: ^start,
|
offset: ^start,
|
||||||
preload: [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address]
|
preload: [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :physical_address]
|
||||||
events = Repo.all(query)
|
events = Repo.all(query)
|
||||||
count_events = Repo.one(from e in Event, select: count(e.id), where: e.organizer_actor_id == ^actor_id)
|
count_events = Repo.one(from e in Event, select: count(e.id), where: e.organizer_actor_id == ^actor_id)
|
||||||
{:ok, events, count_events}
|
{:ok, events, count_events}
|
||||||
|
@ -89,7 +89,7 @@ defmodule Eventos.Events do
|
||||||
"""
|
"""
|
||||||
def get_event_full!(id) do
|
def get_event_full!(id) do
|
||||||
event = Repo.get!(Event, id)
|
event = Repo.get!(Event, id)
|
||||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :physical_address])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -97,7 +97,7 @@ defmodule Eventos.Events do
|
||||||
"""
|
"""
|
||||||
def get_event_full_by_url!(url) do
|
def get_event_full_by_url!(url) do
|
||||||
event = Repo.get_by(Event, url: url)
|
event = Repo.get_by(Event, url: url)
|
||||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :physical_address])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -105,23 +105,7 @@ defmodule Eventos.Events do
|
||||||
"""
|
"""
|
||||||
def get_event_full_by_uuid(uuid) do
|
def get_event_full_by_uuid(uuid) do
|
||||||
event = Repo.get_by(Event, uuid: uuid)
|
event = Repo.get_by(Event, uuid: uuid)
|
||||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :physical_address])
|
||||||
end
|
|
||||||
|
|
||||||
@spec get_event_full_by_name_and_slug!(String.t, String.t) :: Event.t
|
|
||||||
def get_event_full_by_name_and_slug!(name, slug) do
|
|
||||||
query = case String.split(name, "@") do
|
|
||||||
[name, domain] -> from e in Event,
|
|
||||||
join: a in Actor,
|
|
||||||
on: a.id == e.organizer_actor_id and a.preferred_username == ^name and a.domain == ^domain,
|
|
||||||
where: e.slug == ^slug
|
|
||||||
[name] -> from e in Event,
|
|
||||||
join: a in Actor,
|
|
||||||
on: a.id == e.organizer_actor_id and a.preferred_username == ^name and is_nil(a.domain),
|
|
||||||
where: e.slug == ^slug
|
|
||||||
end
|
|
||||||
event = Repo.one(query)
|
|
||||||
Repo.preload(event, [:organizer_actor, :category, :sessions, :tracks, :tags, :participants, :address])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
3
lib/eventos/mailer.ex
Normal file
3
lib/eventos/mailer.ex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule Eventos.Mailer do
|
||||||
|
use Bamboo.Mailer, otp_app: :eventos
|
||||||
|
end
|
|
@ -19,12 +19,9 @@ defmodule EventosWeb.EventController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"event" => event_params}) do
|
def create(conn, %{"event" => event_params}) do
|
||||||
address = process_address(event_params["address"])
|
event_params = process_event_address(event_params)
|
||||||
event_params = if is_nil address do
|
Logger.debug("creating event with")
|
||||||
event_params
|
Logger.debug(inspect event_params)
|
||||||
else
|
|
||||||
%{event_params | "address" => address}
|
|
||||||
end
|
|
||||||
with {:ok, %Event{} = event} <- Events.create_event(event_params) do
|
with {:ok, %Event{} = event} <- Events.create_event(event_params) do
|
||||||
conn
|
conn
|
||||||
|> put_status(:created)
|
|> put_status(:created)
|
||||||
|
@ -33,15 +30,19 @@ defmodule EventosWeb.EventController do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp process_address(address) do
|
defp process_event_address(event) do
|
||||||
geom = EventosWeb.AddressController.process_geom(address["geom"])
|
if Map.has_key?(event, "address_type") && event["address_type"] === :physical do
|
||||||
case geom do
|
address = event["physical_address"]
|
||||||
nil ->
|
geom = EventosWeb.AddressController.process_geom(address["geom"])
|
||||||
address
|
address = case geom do
|
||||||
_ ->
|
nil ->
|
||||||
%{address | "geom" => geom}
|
address
|
||||||
_ ->
|
_ ->
|
||||||
address
|
%{address | "geom" => geom}
|
||||||
|
end
|
||||||
|
%{event | "physical_address" => address}
|
||||||
|
else
|
||||||
|
event
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule EventosWeb.UserController do
|
||||||
alias Eventos.Actors
|
alias Eventos.Actors
|
||||||
alias Eventos.Actors.User
|
alias Eventos.Actors.User
|
||||||
alias Eventos.Repo
|
alias Eventos.Repo
|
||||||
|
alias Eventos.Actors.Service.{Activation, ResetPassword}
|
||||||
|
|
||||||
action_fallback EventosWeb.FallbackController
|
action_fallback EventosWeb.FallbackController
|
||||||
|
|
||||||
|
@ -16,18 +17,80 @@ defmodule EventosWeb.UserController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def register(conn, %{"username" => username, "email" => email, "password" => password}) do
|
def register(conn, %{"username" => username, "email" => email, "password" => password}) do
|
||||||
with {:ok, %User{} = user} <- Actors.register(%{email: email, password: password, username: username}),
|
with {:ok, %User{} = user} <- Actors.register(%{email: email, password: password, username: username}) do
|
||||||
{:ok, token, _claims} <- EventosWeb.Guardian.encode_and_sign(user) do
|
Activation.send_confirmation_email(user, "locale")
|
||||||
conn
|
conn
|
||||||
|> put_status(:created)
|
|> put_status(:created)
|
||||||
|> render("show_with_token.json", %{token: token, user: user})
|
|> render("confirmation.json", %{user: user})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate(conn, %{"token" => token}) do
|
||||||
|
with {:ok, %User{} = user} <- Activation.check_confirmation_token(token) do
|
||||||
|
{:ok, token, _claims} = EventosWeb.Guardian.encode_and_sign(user)
|
||||||
|
conn
|
||||||
|
|> put_resp_header("location", user_path(conn, :show_current_actor))
|
||||||
|
|> render("show_with_token.json", %{user: user, token: token})
|
||||||
|
else
|
||||||
|
{:error, msg} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"error" => msg})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resend_confirmation(conn, %{"email" => email}) do
|
||||||
|
with {:ok, %User{} = user} <- Actors.find_by_email(email),
|
||||||
|
false <- is_nil(user.confirmation_token),
|
||||||
|
true <- Timex.before?(Timex.shift(user.confirmation_sent_at, hours: 1), DateTime.utc_now()) do
|
||||||
|
Activation.resend_confirmation_email(user)
|
||||||
|
render(conn, "confirmation.json", %{user: user})
|
||||||
|
else
|
||||||
|
{:error, :not_found} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"error" => "Unable to find an user with this email"})
|
||||||
|
_ ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"error" => "Unable to resend the validation token"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_reset_password(conn, %{"email" => email}) do
|
||||||
|
with {:ok, %User{} = user} <- Actors.find_by_email(email),
|
||||||
|
{:ok, _} <- ResetPassword.send_password_reset_email(user) do
|
||||||
|
render(conn, "password_reset.json", %{user: user})
|
||||||
|
else
|
||||||
|
{:error, :not_found} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"errors" => "Unable to find an user with this email"})
|
||||||
|
{:error, :email_too_soon} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"errors" => "You requested a new reset password too early"})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def reset_password(conn, %{"password" => password, "token" => token}) do
|
||||||
|
with {:ok, %User{} = user} <- ResetPassword.check_reset_password_token(password, token) do
|
||||||
|
{:ok, token, _claims} = EventosWeb.Guardian.encode_and_sign(user)
|
||||||
|
render(conn, "show_with_token.json", %{user: user, token: token})
|
||||||
|
else
|
||||||
|
{:error, :invalid_token} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:not_found)
|
||||||
|
|> json(%{"errors" => %{"token" => ["Wrong token for password reset"]}})
|
||||||
|
{:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
conn
|
||||||
|
|> put_status(:unprocessable_entity)
|
||||||
|
|> render(EventosWeb.ChangesetView, "error.json", changeset: changeset)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_current_actor(conn, _params) do
|
def show_current_actor(conn, _params) do
|
||||||
user = Guardian.Plug.current_resource(conn)
|
user = Guardian.Plug.current_resource(conn) |> Repo.preload(:actor)
|
||||||
user
|
|
||||||
|> Repo.preload(:actor)
|
|
||||||
render(conn, "show_simple.json", user: user)
|
render(conn, "show_simple.json", user: user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,18 +7,24 @@ defmodule EventosWeb.UserSessionController do
|
||||||
alias Eventos.Actors
|
alias Eventos.Actors
|
||||||
|
|
||||||
def sign_in(conn, %{"email" => email, "password" => password}) do
|
def sign_in(conn, %{"email" => email, "password" => password}) do
|
||||||
case Actors.find_by_email(email) do
|
with {:ok, %User{} = user} <- Actors.find_by_email(email),
|
||||||
%User{} = user ->
|
{:ok, %User{} = _user} <- User.is_confirmed(user),
|
||||||
# Attempt to authenticate the user
|
{:ok, token, _claims} <- Actors.authenticate(%{user: user, password: password}) do
|
||||||
case Actors.authenticate(%{user: user, password: password}) do
|
|
||||||
{:ok, token, _claims} ->
|
|
||||||
# Render the token
|
# Render the token
|
||||||
render conn, "token.json", %{token: token, user: user}
|
render conn, "token.json", %{token: token, user: user}
|
||||||
_ ->
|
else
|
||||||
send_resp(conn, 400, Poison.encode!(%{"error_msg" => "Bad login", "display_error" => "session.error.bad_login", "error_code" => 400}))
|
{:error, :not_found} ->
|
||||||
end
|
conn
|
||||||
_ ->
|
|> put_status(401)
|
||||||
send_resp(conn, 400, Poison.encode!(%{"error_msg" => "No such user", "display_error" => "session.error.bad_login", "error_code" => 400}))
|
|> json(%{"error_msg" => "No such user", "display_error" => "session.error.bad_login"})
|
||||||
|
{:error, :unconfirmed} ->
|
||||||
|
conn
|
||||||
|
|> put_status(401)
|
||||||
|
|> json(%{"error_msg" => "User is not activated", "display_error" => "session.error.not_activated"})
|
||||||
|
{:error, :unauthorized} ->
|
||||||
|
conn
|
||||||
|
|> put_status(401)
|
||||||
|
|> json(%{"error_msg" => "Bad login", "display_error" => "session.error.bad_login"})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,12 @@ defmodule EventosWeb.Router do
|
||||||
scope "/v1" do
|
scope "/v1" do
|
||||||
|
|
||||||
post "/users", UserController, :register
|
post "/users", UserController, :register
|
||||||
|
get "/users/validate/:token", UserController, :validate
|
||||||
|
post "/users/resend", UserController, :resend_confirmation
|
||||||
|
|
||||||
|
post "/users/password-reset/send", UserController, :send_reset_password
|
||||||
|
post "/users/password-reset/post", UserController, :reset_password
|
||||||
|
|
||||||
post "/login", UserSessionController, :sign_in
|
post "/login", UserSessionController, :sign_in
|
||||||
get "/groups", GroupController, :index
|
get "/groups", GroupController, :index
|
||||||
get "/events", EventController, :index
|
get "/events", EventController, :index
|
||||||
|
@ -119,6 +125,11 @@ defmodule EventosWeb.Router do
|
||||||
post "/inbox", ActivityPubController, :inbox
|
post "/inbox", ActivityPubController, :inbox
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if Mix.env == :dev do
|
||||||
|
# If using Phoenix
|
||||||
|
forward "/sent_emails", Bamboo.SentEmailViewerPlug
|
||||||
|
end
|
||||||
|
|
||||||
scope "/", EventosWeb do
|
scope "/", EventosWeb do
|
||||||
pipe_through :browser
|
pipe_through :browser
|
||||||
|
|
||||||
|
|
10
lib/eventos_web/templates/email/email.html.eex
Normal file
10
lib/eventos_web/templates/email/email.html.eex
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="<%= static_url(EventosWeb.Endpoint, "/css/email.css") %>">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%= render @view_module, @view_template, assigns %>
|
||||||
|
|
||||||
|
<p><%= gettext "An email sent by Eventos on %{instance}.", instance: @instance %></p>
|
||||||
|
</body>
|
||||||
|
</html>
|
3
lib/eventos_web/templates/email/email.text.eex
Normal file
3
lib/eventos_web/templates/email/email.text.eex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<%= render @view_module, @view_template, assigns %>
|
||||||
|
|
||||||
|
<%= gettext "An email sent by Eventos on %{instance}.", instance: @instance %>
|
5
lib/eventos_web/templates/email/password_reset.html.eex
Normal file
5
lib/eventos_web/templates/email/password_reset.html.eex
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<h1><%= gettext "Password reset" %></h1>
|
||||||
|
<p><%= gettext "You requested a new password for your account on %{host}.", host: @instance %></p>
|
||||||
|
<p><%= gettext "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one." %></p>
|
||||||
|
|
||||||
|
<p><%= link "Change password", to: EventosWeb.Endpoint.url() <> "/password-reset/#{@token}", target: "_blank" %></p>
|
11
lib/eventos_web/templates/email/password_reset.text.eex
Normal file
11
lib/eventos_web/templates/email/password_reset.text.eex
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<%= gettext "Password reset" %>
|
||||||
|
|
||||||
|
==
|
||||||
|
|
||||||
|
<%= gettext "You requested a new password for your account on %{host}.", host: @instance %>
|
||||||
|
|
||||||
|
<%= gettext "If you didn't request this, please ignore this email. Your password won't change until you access the link below and create a new one." %>
|
||||||
|
|
||||||
|
<%= EventosWeb.Endpoint.url() <> "/password-reset/#{@token}" %>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<h1><%= gettext "Confirm the email address" %></h1>
|
||||||
|
<p><%= gettext "You created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email.", host: @instance %></p>
|
||||||
|
|
||||||
|
<p><%= link "Confirm your email address", to: EventosWeb.Endpoint.url() <> "/validate/#{@token}", target: "_blank" %></p>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<%= gettext "Confirm the email address" %>
|
||||||
|
|
||||||
|
==
|
||||||
|
|
||||||
|
<%= gettext "You created an account on %{host} with this email address. You are one click away from activating it. If this wasn't you, please ignore this email.", host: @instance %>
|
||||||
|
|
||||||
|
<%= EventosWeb.Endpoint.url() <> "/validate/#{@token}" %>
|
||||||
|
|
||||||
|
|
3
lib/eventos_web/views/email_view.ex
Normal file
3
lib/eventos_web/views/email_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule Eventos.EmailView do
|
||||||
|
use EventosWeb, :view
|
||||||
|
end
|
|
@ -20,7 +20,6 @@ defmodule EventosWeb.EventView do
|
||||||
def render("event_for_actor.json", %{event: event}) do
|
def render("event_for_actor.json", %{event: event}) do
|
||||||
%{id: event.id,
|
%{id: event.id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
slug: event.slug,
|
|
||||||
uuid: event.uuid,
|
uuid: event.uuid,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
@ -28,7 +27,6 @@ defmodule EventosWeb.EventView do
|
||||||
def render("event_simple.json", %{event: event}) do
|
def render("event_simple.json", %{event: event}) do
|
||||||
%{id: event.id,
|
%{id: event.id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
slug: event.slug,
|
|
||||||
description: event.description,
|
description: event.description,
|
||||||
begins_on: event.begins_on,
|
begins_on: event.begins_on,
|
||||||
ends_on: event.ends_on,
|
ends_on: event.ends_on,
|
||||||
|
@ -39,6 +37,7 @@ defmodule EventosWeb.EventView do
|
||||||
avatar: event.organizer_actor.avatar_url,
|
avatar: event.organizer_actor.avatar_url,
|
||||||
},
|
},
|
||||||
type: "Event",
|
type: "Event",
|
||||||
|
address_type: event.address_type,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -51,8 +50,9 @@ defmodule EventosWeb.EventView do
|
||||||
uuid: event.uuid,
|
uuid: event.uuid,
|
||||||
organizer: render_one(event.organizer_actor, ActorView, "acccount_basic.json"),
|
organizer: render_one(event.organizer_actor, ActorView, "acccount_basic.json"),
|
||||||
participants: render_many(event.participants, ActorView, "show_basic.json"),
|
participants: render_many(event.participants, ActorView, "show_basic.json"),
|
||||||
address: render_one(event.address, AddressView, "address.json"),
|
physical_address: render_one(event.physical_address, AddressView, "address.json"),
|
||||||
type: "Event",
|
type: "Event",
|
||||||
|
address_type: event.address_type,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -45,4 +45,16 @@ defmodule EventosWeb.UserView do
|
||||||
role: user.role,
|
role: user.role,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def render("confirmation.json", %{user: user}) do
|
||||||
|
%{
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render("password_reset.json", %{user: user}) do
|
||||||
|
%{
|
||||||
|
email: user.email,
|
||||||
|
}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,13 +7,14 @@ defmodule Mix.Tasks.CreateBot do
|
||||||
alias Eventos.Actors
|
alias Eventos.Actors
|
||||||
alias Eventos.Actors.Bot
|
alias Eventos.Actors.Bot
|
||||||
alias Eventos.Repo
|
alias Eventos.Repo
|
||||||
|
alias Eventos.Actors.User
|
||||||
import Logger
|
import Logger
|
||||||
|
|
||||||
@shortdoc "Register user"
|
@shortdoc "Register user"
|
||||||
def run([email, name, summary, type, url]) do
|
def run([email, name, summary, type, url]) do
|
||||||
Mix.Task.run("app.start")
|
Mix.Task.run("app.start")
|
||||||
|
|
||||||
with user <- Actors.find_by_email(email),
|
with {:ok, %User{} = user} <- Actors.find_by_email(email),
|
||||||
actor <- Actors.register_bot_account(%{name: name, summary: summary}),
|
actor <- Actors.register_bot_account(%{name: name, summary: summary}),
|
||||||
{:ok, %Bot{} = bot} <- Actors.create_bot(%{"type" => type, "source" => url, "actor_id" => actor.id, "user_id" => user.id}) do
|
{:ok, %Bot{} = bot} <- Actors.create_bot(%{"type" => type, "source" => url, "actor_id" => actor.id, "user_id" => user.id}) do
|
||||||
bot
|
bot
|
||||||
|
|
6
mix.exs
6
mix.exs
|
@ -26,7 +26,7 @@ defmodule Eventos.Mixfile do
|
||||||
def application do
|
def application do
|
||||||
[
|
[
|
||||||
mod: {Eventos.Application, []},
|
mod: {Eventos.Application, []},
|
||||||
extra_applications: [:logger, :runtime_tools, :guardian]
|
extra_applications: [:logger, :runtime_tools, :guardian, :bamboo]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -66,7 +66,9 @@ defmodule Eventos.Mixfile do
|
||||||
{:ex_crypto, "~> 0.9.0"},
|
{:ex_crypto, "~> 0.9.0"},
|
||||||
{:http_sign, "~> 0.1.1"},
|
{:http_sign, "~> 0.1.1"},
|
||||||
{:ecto_enum, "~> 1.0"},
|
{:ecto_enum, "~> 1.0"},
|
||||||
{:ex_ical, github: "tcitworld/ex_ical", branch: "usable"},
|
{:ex_ical, github: "fazibear/ex_ical"},
|
||||||
|
{:bamboo, "~> 1.0"},
|
||||||
|
{:bamboo_smtp, "~> 1.5.0"},
|
||||||
# Dev and test dependencies
|
# Dev and test dependencies
|
||||||
{:phoenix_live_reload, "~> 1.0", only: :dev},
|
{:phoenix_live_reload, "~> 1.0", only: :dev},
|
||||||
{:ex_machina, "~> 2.1", only: :test},
|
{:ex_machina, "~> 2.1", only: :test},
|
||||||
|
|
5
mix.lock
5
mix.lock
|
@ -1,5 +1,7 @@
|
||||||
%{
|
%{
|
||||||
"argon2_elixir": {:hex, :argon2_elixir, "1.3.0", "fbc521ca54e8802eeaf571caf1cf385827db3b02cae30d9aa591b83ea79785c2", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
"argon2_elixir": {:hex, :argon2_elixir, "1.3.0", "fbc521ca54e8802eeaf571caf1cf385827db3b02cae30d9aa591b83ea79785c2", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"bamboo": {:hex, :bamboo, "1.0.0", "446525f74eb59022ef58bc82f6c91c8e4c5a1469ab42a7f9b37c17262f872ef0", [:mix], [{:hackney, "~> 1.12.1", [hex: :hackney, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, ">= 1.5.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
"bamboo_smtp": {:hex, :bamboo_smtp, "1.5.0", "ebc4deb64a0ff88d05edc1e5f6fd77aea563cdbeac3fcb277666af96dff309e3", [:mix], [{:bamboo, "~> 1.0.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 0.12.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
"base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"},
|
||||||
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
|
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
|
||||||
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
@ -19,13 +21,14 @@
|
||||||
"elixir_make": {:hex, :elixir_make, "0.4.1", "6628b86053190a80b9072382bb9756a6c78624f208ec0ff22cb94c8977d80060", [:mix], [], "hexpm"},
|
"elixir_make": {:hex, :elixir_make, "0.4.1", "6628b86053190a80b9072382bb9756a6c78624f208ec0ff22cb94c8977d80060", [:mix], [], "hexpm"},
|
||||||
"ex_crypto": {:hex, :ex_crypto, "0.9.0", "e04a831034c4d0a43fb2858f696d6b5ae0f87f07dedca3452912fd3cb5ee3ca2", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
|
"ex_crypto": {:hex, :ex_crypto, "0.9.0", "e04a831034c4d0a43fb2858f696d6b5ae0f87f07dedca3452912fd3cb5ee3ca2", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
|
"ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"ex_ical": {:git, "https://github.com/tcitworld/ex_ical.git", "e2facc514ee2ce99331b6a351c00667a9b28dd01", [branch: "usable"]},
|
"ex_ical": {:git, "https://github.com/fazibear/ex_ical.git", "7aec48272760cb52c237b316faadfc533d48e45c", []},
|
||||||
"ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
"ex_machina": {:hex, :ex_machina, "2.2.0", "fec496331e04fc2db2a1a24fe317c12c0c4a50d2beb8ebb3531ed1f0d84be0ed", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
"ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm"},
|
"ex_unit_notifier": {:hex, :ex_unit_notifier, "0.1.4", "36a2dcab829f506e01bf17816590680dd1474407926d43e64c1263e627c364b8", [:mix], [], "hexpm"},
|
||||||
"excoveralls": {:hex, :excoveralls, "0.8.2", "b941a08a1842d7aa629e0bbc969186a4cefdd035bad9fe15d43aaaaaeb8fae36", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
"excoveralls": {:hex, :excoveralls, "0.8.2", "b941a08a1842d7aa629e0bbc969186a4cefdd035bad9fe15d43aaaaaeb8fae36", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"exgravatar": {:hex, :exgravatar, "2.0.1", "66d595c7d63dd6bbac442c5542a724375ae29144059c6fe093e61553850aace4", [:mix], [], "hexpm"},
|
"exgravatar": {:hex, :exgravatar, "2.0.1", "66d595c7d63dd6bbac442c5542a724375ae29144059c6fe093e61553850aace4", [:mix], [], "hexpm"},
|
||||||
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
|
"exjsx": {:hex, :exjsx, "4.0.0", "60548841e0212df401e38e63c0078ec57b33e7ea49b032c796ccad8cde794b5c", [:mix], [{:jsx, "~> 2.8.0", [hex: :jsx, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"file_system": {:hex, :file_system, "0.2.5", "a3060f063b116daf56c044c273f65202e36f75ec42e678dc10653056d3366054", [:mix], [], "hexpm"},
|
"file_system": {:hex, :file_system, "0.2.5", "a3060f063b116daf56c044c273f65202e36f75ec42e678dc10653056d3366054", [:mix], [], "hexpm"},
|
||||||
|
"gen_smtp": {:hex, :gen_smtp, "0.12.0", "97d44903f5ca18ca85cb39aee7d9c77e98d79804bbdef56078adcf905cb2ef00", [:rebar3], [], "hexpm"},
|
||||||
"geo": {:hex, :geo, "2.1.0", "f9a7a1403dde669c4e3f1885aeb4f3b3fb4e51cd28ada6d9f97463e5da65c04a", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
|
"geo": {:hex, :geo, "2.1.0", "f9a7a1403dde669c4e3f1885aeb4f3b3fb4e51cd28ada6d9f97463e5da65c04a", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
"geo_postgis": {:hex, :geo_postgis, "1.1.0", "4c9efc082a8b625c335967fec9f5671c2bc8a0a686f9c5130445ebbcca989740", [:mix], [{:geo, "~> 2.0", [hex: :geo, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"},
|
"geo_postgis": {:hex, :geo_postgis, "1.1.0", "4c9efc082a8b625c335967fec9f5671c2bc8a0a686f9c5130445ebbcca989740", [:mix], [{:geo, "~> 2.0", [hex: :geo, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
|
"gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"},
|
||||||
|
|
|
@ -10,8 +10,8 @@ defmodule Eventos.Repo.Migrations.AlterEventTimestampsToDateTimeWithTimeZone do
|
||||||
|
|
||||||
def down do
|
def down do
|
||||||
alter table("events") do
|
alter table("events") do
|
||||||
modify :inserted_at, :naive_datetime
|
modify :inserted_at, :timestamptz
|
||||||
modify :updated_at, :naive_datetime
|
modify :updated_at, :timestamptz
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
defmodule :"Elixir.Eventos.Repo.Migrations.Add-user-confirm-email-fields" do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:users) do
|
||||||
|
add :confirmed_at, :utc_datetime
|
||||||
|
add :confirmation_sent_at, :utc_datetime
|
||||||
|
add :confirmation_token, :string
|
||||||
|
end
|
||||||
|
create unique_index(:users, [:confirmation_token], name: "index_unique_users_confirmation_token")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:users) do
|
||||||
|
remove :confirmed_at
|
||||||
|
remove :confirmation_sent_at
|
||||||
|
remove :confirmation_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule :"Elixir.Eventos.Repo.Migrations.Add-user-password-reset-fields" do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:users) do
|
||||||
|
add :reset_password_sent_at, :utc_datetime
|
||||||
|
add :reset_password_token, :string
|
||||||
|
end
|
||||||
|
create unique_index(:users, [:reset_password_token], name: "index_unique_users_reset_password_token")
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:users) do
|
||||||
|
remove :reset_password_sent_at
|
||||||
|
remove :reset_password_token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,17 @@
|
||||||
|
defmodule Eventos.Repo.Migrations.AlterEventTimestampsToDateTimeWithTimeZone do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table("events") do
|
||||||
|
modify :inserted_at, :timestamptz
|
||||||
|
modify :updated_at, :timestamptz
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table("events") do
|
||||||
|
modify :inserted_at, :utc_datetime
|
||||||
|
modify :updated_at, :utc_datetime
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
31
priv/repo/migrations/20180702150922_add_address_type.exs
Normal file
31
priv/repo/migrations/20180702150922_add_address_type.exs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
defmodule Eventos.Repo.Migrations.AddAddressType do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
AddressTypeEnum.create_type
|
||||||
|
alter table(:events) do
|
||||||
|
add :address_type, :address_type
|
||||||
|
add :online_address, :string
|
||||||
|
add :phone, :string
|
||||||
|
end
|
||||||
|
drop constraint(:events, "events_address_id_fkey")
|
||||||
|
rename table(:events), :address_id, to: :physical_address_id
|
||||||
|
alter table(:events) do
|
||||||
|
modify :physical_address_id, references(:addresses, on_delete: :nothing)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:events) do
|
||||||
|
remove :address_type
|
||||||
|
remove :online_address
|
||||||
|
remove :phone
|
||||||
|
end
|
||||||
|
AddressTypeEnum.drop_type
|
||||||
|
drop constraint(:events, "events_physical_address_id_fkey")
|
||||||
|
rename table(:events), :physical_address_id, to: :address_id
|
||||||
|
alter table(:events) do
|
||||||
|
modify :address_id, references(:addresses, on_delete: :nothing)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,15 @@
|
||||||
|
defmodule Eventos.Repo.Migrations.RemoveSlugForEvent do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def up do
|
||||||
|
alter table(:events) do
|
||||||
|
remove(:slug)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down do
|
||||||
|
alter table(:events) do
|
||||||
|
add :slug, :string, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue