Browse Source

Move to GraphQL

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
osm-theme
Thomas Citharel 2 years ago
parent
commit
b54dae7e15
149 changed files with 5600 additions and 4660 deletions
  1. +3
    -1
      .gitignore
  2. +2
    -3
      .gitlab-ci.yml
  3. +7
    -1
      config/config.exs
  4. +5
    -0
      docker/tests/Dockerfile
  5. +4
    -4
      js/.eslintrc.js
  6. +3
    -3
      js/.postcssrc.js
  7. +58
    -0
      js/Makefile
  8. +38
    -0
      js/get_union_json.js
  9. +2081
    -99
      js/package-lock.json
  10. +13
    -5
      js/package.json
  11. +31
    -17
      js/src/App.vue
  12. +0
    -5
      js/src/actions/login.js
  13. +0
    -29
      js/src/api/eventFetch.js
  14. +0
    -120
      js/src/auth/index.js
  15. +180
    -186
      js/src/components/Account/Account.vue
  16. +16
    -19
      js/src/components/Account/Identities.vue
  17. +77
    -72
      js/src/components/Account/Login.vue
  18. +1
    -3
      js/src/components/Account/PasswordReset.vue
  19. +103
    -112
      js/src/components/Account/Register.vue
  20. +2
    -2
      js/src/components/Account/RegisterAvatar.vue
  21. +0
    -2
      js/src/components/Account/ResendConfirmation.vue
  22. +2
    -4
      js/src/components/Account/SendPasswordReset.vue
  23. +22
    -16
      js/src/components/Account/Validate.vue
  24. +77
    -32
      js/src/components/Category/Create.vue
  25. +31
    -36
      js/src/components/Category/List.vue
  26. +122
    -122
      js/src/components/Event/Create.vue
  27. +22
    -24
      js/src/components/Event/Edit.vue
  28. +214
    -216
      js/src/components/Event/Event.vue
  29. +71
    -72
      js/src/components/Event/EventList.vue
  30. +53
    -54
      js/src/components/Group/Create.vue
  31. +33
    -35
      js/src/components/Group/Group.vue
  32. +38
    -40
      js/src/components/Group/GroupList.vue
  33. +34
    -39
      js/src/components/Home.vue
  34. +20
    -20
      js/src/components/Location.vue
  35. +92
    -106
      js/src/components/NavBar.vue
  36. +3
    -0
      js/src/constants.js
  37. +39
    -0
      js/src/graphql/actor.js
  38. +16
    -0
      js/src/graphql/auth.js
  39. +29
    -0
      js/src/graphql/category.js
  40. +109
    -0
      js/src/graphql/event.js
  41. +1
    -0
      js/src/graphql/fragmentTypes.json
  42. +17
    -0
      js/src/graphql/search.js
  43. +10
    -0
      js/src/graphql/upload.js
  44. +28
    -0
      js/src/graphql/user.js
  45. +0
    -15
      js/src/i18n/en.js
  46. +0
    -20
      js/src/i18n/fr.js
  47. +0
    -6
      js/src/i18n/index.js
  48. +30
    -0
      js/src/i18n/locale/en_US/LC_MESSAGES/app.po
  49. +30
    -0
      js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po
  50. +30
    -0
      js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po~
  51. +1
    -0
      js/src/i18n/translations.json
  52. +12
    -35
      js/src/main.js
  53. +0
    -44
      js/src/store/index.js
  54. +0
    -4
      js/src/store/mutation-types.js
  55. +135
    -0
      js/src/vue-apollo.js
  56. +4
    -4
      js/tests/unit/.eslintrc.js
  57. +1
    -1
      lib/mix/tasks/create_bot.ex
  58. +91
    -43
      lib/mobilizon/actors/actors.ex
  59. +9
    -3
      lib/mobilizon/actors/service/activation.ex
  60. +4
    -27
      lib/mobilizon/actors/service/reset_password.ex
  61. +27
    -0
      lib/mobilizon/actors/service/tools.ex
  62. +17
    -1
      lib/mobilizon/actors/user.ex
  63. +2
    -2
      lib/mobilizon/email/user.ex
  64. +4
    -2
      lib/mobilizon/events/category.ex
  65. +0
    -1
      lib/mobilizon/events/event.ex
  66. +73
    -20
      lib/mobilizon/events/events.ex
  67. +3
    -3
      lib/mobilizon_web/auth_pipeline.ex
  68. +20
    -0
      lib/mobilizon_web/context.ex
  69. +0
    -79
      lib/mobilizon_web/controllers/actor_controller.ex
  70. +0
    -78
      lib/mobilizon_web/controllers/address_controller.ex
  71. +0
    -48
      lib/mobilizon_web/controllers/bot_controller.ex
  72. +0
    -46
      lib/mobilizon_web/controllers/category_controller.ex
  73. +0
    -43
      lib/mobilizon_web/controllers/comment_controller.ex
  74. +0
    -125
      lib/mobilizon_web/controllers/event_controller.ex
  75. +0
    -52
      lib/mobilizon_web/controllers/event_request_controller.ex
  76. +0
    -43
      lib/mobilizon_web/controllers/follower_controller.ex
  77. +0
    -53
      lib/mobilizon_web/controllers/group_controller.ex
  78. +0
    -6
      lib/mobilizon_web/controllers/inboxes_controller.ex
  79. +2
    -2
      lib/mobilizon_web/controllers/node_info_controller.ex
  80. +0
    -10
      lib/mobilizon_web/controllers/outboxes_controller.ex
  81. +0
    -18
      lib/mobilizon_web/controllers/participant_controller.ex
  82. +0
    -23
      lib/mobilizon_web/controllers/search_controller.ex
  83. +0
    -56
      lib/mobilizon_web/controllers/session_controller.ex
  84. +0
    -46
      lib/mobilizon_web/controllers/tag_controller.ex
  85. +0
    -46
      lib/mobilizon_web/controllers/track_controller.ex
  86. +0
    -148
      lib/mobilizon_web/controllers/user_controller.ex
  87. +0
    -41
      lib/mobilizon_web/controllers/user_session_controller.ex
  88. +4
    -0
      lib/mobilizon_web/controllers/web_finger_controller.ex
  89. +8
    -0
      lib/mobilizon_web/endpoint.ex
  90. +26
    -0
      lib/mobilizon_web/resolvers/actor.ex
  91. +37
    -0
      lib/mobilizon_web/resolvers/category.ex
  92. +54
    -0
      lib/mobilizon_web/resolvers/event.ex
  93. +2
    -0
      lib/mobilizon_web/resolvers/upload.ex
  94. +118
    -0
      lib/mobilizon_web/resolvers/user.ex
  95. +9
    -81
      lib/mobilizon_web/router.ex
  96. +318
    -0
      lib/mobilizon_web/schema.ex
  97. +36
    -0
      lib/mobilizon_web/schema/custom/UUID4.ex
  98. +15
    -0
      lib/mobilizon_web/upload_plug.ex
  99. +50
    -0
      lib/mobilizon_web/uploaders/avatar.ex
  100. +49
    -0
      lib/mobilizon_web/uploaders/category.ex

+ 3
- 1
.gitignore View File

@ -22,4 +22,6 @@ priv/static/*
priv/data/*
!priv/data/.gitkeep
.vscode/
cover/
cover/
uploads/*
!uploads/.gitkeep

+ 2
- 3
.gitlab-ci.yml View File

@ -1,4 +1,4 @@
image: elixir:1.7
image: tcitworld/mobilizon-ci
services:
- name: mdillon/postgis:10
@ -20,8 +20,7 @@ cache:
- .rebar3
before_script:
- apt-get update
- apt-get install -y build-essential postgresql-client git
- cd js && npm install && npm run build && cd ../
- mix local.rebar --force
- mix local.hex --force
- mix deps.get


+ 7
- 1
config/config.exs View File

@ -47,7 +47,7 @@ config :guardian, Guardian.DB,
# default
schema_name: "guardian_tokens",
# store all token types if not set
token_types: ["refresh_token"],
# token_types: ["refresh_token"],
# default: 60 minutes
sweep_interval: 60
@ -59,3 +59,9 @@ config :geolix,
source: System.get_env("GEOLITE_CITIES_PATH") || "priv/data/GeoLite2-City.mmdb"
}
]
config :arc,
storage: Arc.Storage.Local
config :email_checker,
validations: [EmailChecker.Check.Format]

+ 5
- 0
docker/tests/Dockerfile View File

@ -0,0 +1,5 @@
FROM elixir:latest
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg
RUN curl -sL https://deb.nodesource.com/setup_8.x | bash && apt-get install nodejs -yq
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

+ 4
- 4
js/.eslintrc.js View File

@ -1,7 +1,7 @@
module.exports = {
root: true,
'extends': [
extends: [
'plugin:vue/essential',
'@vue/airbnb'
]
}
'@vue/airbnb',
],
};

+ 3
- 3
js/.postcssrc.js View File

@ -1,5 +1,5 @@
module.exports = {
plugins: {
autoprefixer: {}
}
}
autoprefixer: {},
},
};

+ 58
- 0
js/Makefile View File

@ -0,0 +1,58 @@
# On OSX the PATH variable isn't exported unless "SHELL" is also set, see: http://stackoverflow.com/a/25506676
SHELL = /bin/bash
NODE_BINDIR = ./node_modules/.bin
export PATH := $(NODE_BINDIR):$(PATH)
# Where to find input files (it can be multiple paths).
INPUT_FILES = ./src
# Where to write the files generated by this makefile.
OUTPUT_DIR = ./src/i18n
# Available locales for the app.
LOCALES = en_US fr_FR
# Name of the generated .po files for each available locale.
LOCALE_FILES ?= $(patsubst %,$(OUTPUT_DIR)/locale/%/LC_MESSAGES/app.po,$(LOCALES))
GETTEXT_HTML_SOURCES = $(shell find $(INPUT_FILES) -name '*.vue' -o -name '*.html' 2> /dev/null)
GETTEXT_JS_SOURCES = $(shell find $(INPUT_FILES) -name '*.vue' -o -name '*.js')
# Makefile Targets
.PHONY: clean makemessages translations
clean:
rm -f /tmp/template.pot $(OUTPUT_DIR)/translations.json
makemessages: /tmp/template.pot
translations: ./$(OUTPUT_DIR)/translations.json
# Create a main .pot template, then generate .po files for each available language.
# Thanx to Systematic: https://github.com/Polyconseil/systematic/blob/866d5a/mk/main.mk#L167-L183
/tmp/template.pot: $(GETTEXT_HTML_SOURCES)
# `dir` is a Makefile built-in expansion function which extracts the directory-part of `$@`.
# `$@` is a Makefile automatic variable: the file name of the target of the rule.
# => `mkdir -p /tmp/`
mkdir -p $(dir $@)
which gettext-extract
# Extract gettext strings from templates files and create a POT dictionary template.
gettext-extract --attribute v-translate --quiet --output $@ $(GETTEXT_HTML_SOURCES)
# Extract gettext strings from JavaScript files.
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
--from-code=utf-8 --join-existing --no-wrap \
--package-name=$(shell node -e "console.log(require('./package.json').name);") \
--package-version=$(shell node -e "console.log(require('./package.json').version);") \
--output $@ $(GETTEXT_JS_SOURCES)
# Generate .po files for each available language.
@for lang in $(LOCALES); do \
export PO_FILE=$(OUTPUT_DIR)/locale/$$lang/LC_MESSAGES/app.po; \
echo "msgmerge --update $$PO_FILE $@"; \
mkdir -p $$(dirname $$PO_FILE); \
[ -f $$PO_FILE ] && msgmerge --lang=$$lang --update $$PO_FILE $@ || msginit --no-translator --locale=$$lang --input=$@ --output-file=$$PO_FILE; \
msgattrib --no-wrap --no-obsolete -o $$PO_FILE $$PO_FILE; \
done;
$(OUTPUT_DIR)/translations.json: clean /tmp/template.pot
mkdir -p $(OUTPUT_DIR)
gettext-compile --output $@ $(LOCALE_FILES)

+ 38
- 0
js/get_union_json.js View File

@ -0,0 +1,38 @@
const fetch = require('node-fetch');
const fs = require('fs');
fetch(`http://localhost:4000/graphiql`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
variables: {},
query: `
{
__schema {
types {
kind
name
possibleTypes {
name
}
}
}
}
`,
}),
})
.then(result => result.json())
.then(result => {
// here we're filtering out any type information unrelated to unions or interfaces
const filteredData = result.data.__schema.types.filter(
type => type.possibleTypes !== null,
);
result.data.__schema.types = filteredData;
fs.writeFile('./fragmentTypes.json', JSON.stringify(result.data), err => {
if (err) {
console.error('Error writing fragmentTypes file', err);
} else {
console.log('Fragment types successfully extracted!');
}
});
});

+ 2081
- 99
js/package-lock.json
File diff suppressed because it is too large
View File


+ 13
- 5
js/package.json View File

@ -6,22 +6,29 @@
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit",
"test:e2e": "vue-cli-service test:e2e"
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"apollo-absinthe-upload-link": "^1.4.0",
"apollo-cache-inmemory": "^1.3.6",
"apollo-link": "^1.2.3",
"apollo-link-http": "^1.5.5",
"easygettext": "^2.7.0",
"graphql-tag": "^2.9.0",
"material-design-icons": "^3.0.1",
"moment": "^2.22.2",
"ngeohash": "^0.6.0",
"register-service-worker": "^1.4.1",
"vue": "^2.5.17",
"vue-apollo": "^3.0.0-beta.25",
"vue-gettext": "^2.1.1",
"vue-gravatar": "^1.2.1",
"vue-markdown": "^2.2.4",
"vue-router": "^3.0.1",
"vuetify": "^1.2.7",
"vuetify": "^1.3.1",
"vuetify-google-autocomplete": "^2.0.0-beta.5",
"vuex": "^3.0.1",
"vuex-i18n": "^1.10.5"
"vuex": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.5",
@ -36,6 +43,7 @@
"dotenv-webpack": "^1.5.7",
"node-sass": "^4.9.3",
"sass-loader": "^7.1.0",
"vue-cli-plugin-apollo": "^0.17.1",
"vue-template-compiler": "^2.5.17"
},
"browserslist": [


+ 31
- 17
js/src/App.vue View File

@ -12,24 +12,24 @@
<v-list-group
value="false"
>
<v-list-tile avatar v-if="$store.state.actor" slot="activator">
<v-list-tile avatar v-if="actor" slot="activator">
<v-list-tile-avatar>
<img v-if="!$store.state.actor.avatar"
<img v-if="!actor.avatar"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="$store.state.actor.avatar"
:src="actor.avatar"
>
</v-list-tile-avatar>
<v-list-tile-content @click="$router.push({name: 'Account', params: { name: $store.state.actor.username }})">
<v-list-tile-content @click="$router.push({name: 'Account', params: { name: actor.username }})">
<v-list-tile-title>{{ this.displayed_name }}</v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
<v-list-tile avatar v-if="$store.state.actor">
<v-list-tile avatar v-if="actor">
<v-list-tile-avatar>
<img
class="img-circle elevation-7 mb-1"
@ -93,11 +93,12 @@
<v-speed-dial
v-model="fab"
bottom
fixed
right
fixed
direction="top"
open-on-hover
transition="scale-transition"
v-if="getUser()"
v-if="user"
>
<v-btn
slot="activator"
@ -129,7 +130,12 @@
</v-btn>
</v-speed-dial>
<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"
v-translate="{
date: new Date().getFullYear(),
}">© The Mobilizon Contributors %{date} - 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-snackbar
:timeout="error.timeout"
@ -143,8 +149,9 @@
</template>
<script>
import gql from 'graphql-tag';
import NavBar from '@/components/NavBar';
import { AUTH_USER_ACTOR, AUTH_USER_ID } from '@/constants';
export default {
name: 'app',
@ -155,11 +162,17 @@ export default {
return {
drawer: false,
fab: false,
user: false,
user: localStorage.getItem(AUTH_USER_ID),
items: [
{ icon: 'poll', text: 'Events', route: 'EventList', role: null },
{ icon: 'group', text: 'Groups', route: 'GroupList', role: null },
{ icon: 'content_copy', text: 'Categories', route: 'CategoryList', role: 'ROLE_ADMIN' },
{
icon: 'poll', text: 'Events', route: 'EventList', role: null,
},
{
icon: 'group', text: 'Groups', route: 'GroupList', role: null,
},
{
icon: 'content_copy', text: 'Categories', route: 'CategoryList', role: 'ROLE_ADMIN',
},
{ icon: 'settings', text: 'Settings', role: 'ROLE_USER' },
{ icon: 'chat_bubble', text: 'Send feedback', role: 'ROLE_USER' },
{ icon: 'help', text: 'Help', role: null },
@ -171,14 +184,15 @@ export default {
text: '',
},
show_new_event_button: false,
actor: localStorage.getItem(AUTH_USER_ACTOR),
};
},
methods: {
showMenuItem(elem) {
return elem !== null && this.$store.state.user && this.$store.state.user.roles !== undefined ? this.$store.state.user.roles.includes(elem) : true;
return elem !== null && this.user && this.user.roles !== undefined ? this.user.roles.includes(elem) : true;
},
getUser() {
return this.$store.state.user === undefined ? false : this.$store.state.user;
return this.user === undefined ? false : this.user;
},
toggleDrawer() {
this.drawer = !this.drawer;
@ -186,9 +200,9 @@ export default {
},
computed: {
displayed_name() {
return this.$store.state.actor.display_name === null ? this.$store.state.actor.username : this.$store.state.actor.display_name
return this.actor.display_name === null ? this.actor.username : this.actor.display_name;
},
}
},
};
</script>


+ 0
- 5
js/src/actions/login.js View File

@ -1,5 +0,0 @@
export default {
login(state, user) {
state.user = user.user;
},
};

+ 0
- 29
js/src/api/eventFetch.js View File

@ -1,29 +0,0 @@
import { API_ORIGIN, API_PATH } from './_entrypoint';
const jsonLdMimeType = 'application/json';
export default function eventFetch(url, store, optionsarg = {}) {
const options = optionsarg;
if (typeof options.headers === 'undefined') {
options.headers = new Headers();
}
if (options.headers.get('Accept') === null) {
options.headers.set('Accept', jsonLdMimeType);
}
if (options.body !== 'undefined' && !(options.body instanceof FormData) && options.headers.get('Content-Type') === null) {
options.headers.set('Content-Type', jsonLdMimeType);
}
if (store.state.user) {
options.headers.set('Authorization', `Bearer ${localStorage.getItem('token')}`);
}
const link = url.includes(API_PATH) ? API_ORIGIN + url : API_ORIGIN + API_PATH + url;
return fetch(link, options).then((response) => {
if (response.ok) return response;
throw response.text();
});
}

+ 0
- 120
js/src/auth/index.js View File

@ -1,120 +0,0 @@
import { API_ORIGIN, API_PATH } from '../api/_entrypoint';
import { LOGIN_USER, LOAD_USER, CHANGE_ACTOR } from '../store/mutation-types';
// URL and endpoint constants
const LOGIN_URL = `${API_ORIGIN}${API_PATH}/login`;
const SIGNUP_URL = `${API_ORIGIN}${API_PATH}/users/`;
const CHECK_AUTH = `${API_ORIGIN}${API_PATH}/user/`;
const REFRESH_TOKEN = `${API_ORIGIN}${API_PATH}/token/refresh`;
export default {
// Send a request to the login URL and save the returned JWT
login(creds, success, error) {
fetch(LOGIN_URL, { method: 'POST', body: creds, headers: { 'Content-Type': 'application/json' } })
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw response.json();
})
.then((data) => {
localStorage.setItem('token', data.token);
// localStorage.setItem('refresh_token', data.refresh_token);
return success(data);
})
.catch(err => error(err));
},
signup(creds, success, error) {
fetch(SIGNUP_URL, { method: 'POST', body: creds, headers: { 'Content-Type': 'application/json' } })
.then((response) => {
if (response.status === 200) {
return response.json();
}
throw response.json();
})
.then((data) => {
localStorage.setItem('token', data.token);
// localStorage.setItem('refresh_token', data.refresh_token);
return success(data);
}).catch(err => error(err));
},
refreshToken(store, successHandler, errorHandler) {
const refreshToken = localStorage.getItem('refresh_token');
console.log('We are refreshing the jwt token');
fetch(REFRESH_TOKEN, { method: 'POST', body: JSON.stringify({ refresh_token: refreshToken }), headers: { 'Content-Type': 'application/json' } })
.then((response) => {
if (response.ok) {
return response.json();
}
return errorHandler('Error while authenticating');
})
.then((response) => {
console.log('We have a new token');
this.authenticated = true;
store.commit(LOGIN_USER, response);
localStorage.setItem('token', response.token);
console.log("Let's try to auth again");
successHandler();
});
},
// To log out, we just need to remove the token
logout(store) {
localStorage.removeItem('refresh_token');
localStorage.removeItem('token');
this.authenticated = false;
store.commit('LOGOUT_USER');
},
jwt_decode(token) {
const base64Url = token.split('.')[1];
const base64 = base64Url.replace('-', '+').replace('_', '/');
return JSON.parse(window.atob(base64));
},
getTokenExpirationDate(encodedToken) {
const token = this.jwt_decode(encodedToken);
if (!token.exp) { return null; }
const date = new Date(0);
date.setUTCSeconds(token.exp);
return date;
},
isTokenExpired(token) {
const expirationDate = this.getTokenExpirationDate(token);
return expirationDate < new Date();
},
getUser(store, successHandler, errorHandler) {
console.log('We are checking the auth');
this.token = localStorage.getItem('token');
const options = {};
options.headers = new Headers();
options.headers.set('Authorization', `Bearer ${this.token}`);
fetch(CHECK_AUTH, options)
.then((response) => {
if (response.ok) {
return response.json();
}
return errorHandler('Error while authenticating');
}).then((response) => {
this.authenticated = true;
console.log(response);
store.commit(LOAD_USER, response.data);
store.commit(CHANGE_ACTOR, response.data.actors[0]);
return successHandler();
});
},
// The object to be passed as a header for authenticated requests
getAuthHeader() {
return {
Authorization: `Bearer ${localStorage.getItem('access_token')}`,
};
},
};

+ 180
- 186
js/src/components/Account/Account.vue View File

@ -1,216 +1,210 @@
<template>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-card v-if="!loading">
<v-img :src="actor.banner || 'https://picsum.photos/400/'" height="300px">
<v-layout column class="media">
<v-card-title>
<v-btn icon @click="$router.go(-1)">
<v-icon>chevron_left</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon class="mr-3" v-if="$store.state.user && $store.state.actor.id === actor.id">
<v-icon>edit</v-icon>
</v-btn>
<v-menu bottom left>
<v-btn icon slot="activator">
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<v-list-tile @click="logoutUser()" v-if="$store.state.user && $store.state.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.actor.id === actor.id">
<v-list-tile-title>Delete</v-list-tile-title>
</v-list-tile>
</v-list>
</v-menu>
</v-card-title>
<v-spacer></v-spacer>
<div class="text-xs-center">
<v-avatar size="125px">
<img v-if="!actor.avatar"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
<ApolloQuery :query="FETCH_ACTOR" :variables="{ name }">
<template slot-scope="{ result: { loading, error, data } }">
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-card v-if="data">
<v-img :src="data.actor.banner || 'https://picsum.photos/400/'" height="300px">
<v-layout column class="media">
<v-card-title>
<v-btn icon @click="$router.go(-1)">
<v-icon>chevron_left</v-icon>
</v-btn>
<v-spacer></v-spacer>
<!-- <v-btn icon class="mr-3" v-if="actor.id === data.actor.id">
<v-icon>edit</v-icon>
</v-btn> -->
<v-menu bottom left>
<v-btn icon slot="activator">
<v-icon>more_vert</v-icon>
</v-btn>
<v-list>
<!-- <v-list-tile @click="logoutUser()" v-if="actor.id === data.actor.id">
<v-list-tile-title>User logout</v-list-tile-title>
</v-list-tile>
<v-list-tile @click="deleteAccount()" v-if="actor.id === data.actor.id">
<v-list-tile-title>Delete</v-list-tile-title>
</v-list-tile> -->
</v-list>
</v-menu>
</v-card-title>
<v-spacer></v-spacer>
<div class="text-xs-center">
<v-avatar size="125px">
<img v-if="!data.actor.avatarUrl"
class="img-circle elevation-7 mb-1"
:src="actor.avatar"
>
</v-avatar>
</div>
<v-container fluid grid-list-lg>
<v-layout row>
<v-flex xs7>
<div class="headline">{{ actor.display_name }}</div>
<div><span class="subheading">@{{ actor.username }}<span v-if="actor.domain">@{{ actor.domain }}</span></span></div>
<v-card-text v-if="actor.description" v-html="actor.description"></v-card-text>
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="data.actor.avatarUrl"
>
</v-avatar>
</div>
<v-container fluid grid-list-lg>
<v-layout row>
<v-flex xs7>
<div class="headline">{{ data.actor.name }}</div>
<div><span class="subheading">@{{ data.actor.preferredUsername }}<span v-if="data.actor.domain">@{{ data.actor.domain }}</span></span></div>
<v-card-text v-if="data.actor.description" v-html="data.actor.description"></v-card-text>
</v-flex>
</v-layout>
</v-container>
</v-layout>
</v-img>
<v-list three-line>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">phone</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>(323) 555-6789</v-list-tile-title>
<v-list-tile-sub-title>Work</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-icon dark>chat</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">mail</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>ali_connors@example.com</v-list-tile-title>
<v-list-tile-sub-title>Work</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">location_on</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>1400 Main Street</v-list-tile-title>
<v-list-tile-sub-title>Orlando, FL 79938</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-container fluid grid-list-md v-if="data.actor.participatingEvents && data.actor.participatingEvents.length > 0">
<v-subheader>Participated at</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in data.actor.participatingEvents" :key="event.id">
<v-card>
<v-img
class="black--text"
height="200px"
src="https://picsum.photos/400/200/"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<span class="headline">{{ event.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-img>
<v-card-title>
<div>
<span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br>
<p>{{ event.description }}</p>
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
</div>
</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>favorite</v-icon>
</v-btn>
<v-btn icon>
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn icon>
<v-icon>share</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-layout>
</v-img>
<v-list three-line>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">phone</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>(323) 555-6789</v-list-tile-title>
<v-list-tile-sub-title>Work</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action>
<v-icon dark>chat</v-icon>
</v-list-tile-action>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">mail</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>ali_connors@example.com</v-list-tile-title>
<v-list-tile-sub-title>Work</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
<v-divider inset></v-divider>
<v-list-tile>
<v-list-tile-action>
<v-icon color="indigo">location_on</v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title>1400 Main Street</v-list-tile-title>
<v-list-tile-sub-title>Orlando, FL 79938</v-list-tile-sub-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-container fluid grid-list-md v-if="actor.participatingEvents && actor.participatingEvents.length > 0">
<v-subheader>Participated at</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in actor.participatingEvents" :key="event.id">
<v-card>
<v-card-media
class="black--text"
height="200px"
src="https://picsum.photos/400/200/"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<span class="headline">{{ event.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title>
<div>
<span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br>
<p>{{ event.description }}</p>
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
</div>
</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>favorite</v-icon>
</v-btn>
<v-btn icon>
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn icon>
<v-icon>share</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
<v-container fluid grid-list-md v-if="actor.organized_events && actor.organized_events.length > 0">
<v-subheader>Organized events</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in actor.organized_events" :key="event.id">
<v-card>
<v-card-media
class="black--text"
height="200px"
src="https://picsum.photos/400/200/"
>
<v-container fill-height fluid>
<v-layout fill-height>
<v-flex xs12 align-end flexbox>
<span class="headline">{{ event.title }}</span>
</v-flex>
</v-layout>
</v-container>
</v-card-media>
<v-card-title>
<div>
<span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br>
<p>{{ event.description }}</p>
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
</div>
</v-card-title>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>favorite</v-icon>
</v-btn>
<v-btn icon>
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn icon>
<v-icon>share</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-card>
<v-container fluid grid-list-md v-if="data.actor.organizedEvents && data.actor.organizedEvents.length > 0">
<v-subheader>Organized events</v-subheader>
<v-layout row wrap>
<v-flex v-for="event in data.actor.organizedEvents" :key="event.id" md6>
<v-card>
<v-img
height="200px"
src="https://picsum.photos/400/200/"
/>
<v-card-title primary-title>
<div>
<router-link :to="{name: 'Event', params: {uuid: event.uuid}}">
<div class="headline">{{ event.title }}</div>
</router-link>
<span class="grey--text" v-html="nl2br(event.description)"></span>
</div>
</v-card-title>
<!-- <v-card-title>
<div>
<span class="grey--text" v-if="event.addressType === 'physical'">{{ event.startDate }} à {{ event.location }}</span><br>
<p v-if="event.organizer">Organisé par <router-link :to="{name: 'Account', params: {'id': event.organizer.id}}">{{ event.organizer.username }}</router-link></p>
</div>
</v-card-title> -->
<v-card-actions>
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>favorite</v-icon>
</v-btn>
<v-btn icon>
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn icon>
<v-icon>share</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</v-flex>
</v-layout>
</v-container>
</v-card>
</template>
</ApolloQuery>
</v-flex>
</v-layout>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import auth from '@/auth';
import { FETCH_ACTOR } from '@/graphql/actor';
export default {
name: 'Account',
data() {
return {
actor: null,
loading: true,
}
loading: true,
};
},
props: {
name: {
type: String,
required: true,
}
name: {
type: String,
required: true,
},
},
created() {
this.fetchData();
},
watch: {
// call again the method if the route changes
'$route': 'fetchData'
$route: 'fetchData',
},
methods: {
fetchData() {
eventFetch(`/actors/${this.name}`, this.$store)
.then(response => response.json())
.then((response) => {
this.actor = response.data;
this.loading = false;
console.log('actor', this.actor);
})
},
logoutUser() {
auth.logout(this.$store);
// TODO : implement logout
this.$router.push({ name: 'Home' });
},
}
}
nl2br: function(text) {
return text.replace(/(?:\r\n|\r|\n)/g, '<br>');
}
},
};
</script>

+ 16
- 19
js/src/components/Account/Identities.vue View File

@ -15,7 +15,7 @@
@click="$router.push({ name: 'Account', params: { name: actor.username } })"
>
<v-list-tile-action>
<v-icon v-if="$store.state.defaultActor === actor.username" color="pink">star</v-icon>
<v-icon v-if="defaultActor === actor.username" color="pink">star</v-icon>
</v-list-tile-action>
<v-list-tile-content>
@ -67,29 +67,26 @@
</template>
<script>
import eventFetch from "@/api/eventFetch";
import auth from "@/auth";
export default {
name: "Identities",
name: 'Identities',
data() {
return {
actors: [],
newActor: {
preferred_username: "",
summary: ""
preferred_username: '',
summary: '',
},
loading: true,
showForm: false,
rules: {
required: value => !!value || "Required."
required: value => !!value || 'Required.',
},
state: {
username: {
status: false,
msg: []
}
}
msg: [],
},
},
};
},
created() {
@ -97,9 +94,9 @@ export default {
},
methods: {
fetchData() {
eventFetch(`/user`, this.$store)
eventFetch('/user', this.$store)
.then(response => response.json())
.then(response => {
.then((response) => {
this.actors = response.data.actors;
this.loading = false;
});
@ -107,12 +104,12 @@ export default {
sendData() {
this.loading = true;
this.showForm = false;
eventFetch(`/actors`, this.$store, {
method: "POST",
body: JSON.stringify({ actor: this.newActor })
eventFetch('/actors', this.$store, {
method: 'POST',
body: JSON.stringify({ actor: this.newActor }),
})
.then(response => response.json())
.then(response => {
.then((response) => {
this.actors.push(response.data);
this.loading = false;
});
@ -126,7 +123,7 @@ export default {
},
host() {
return `@${window.location.host}`;
}
}
},
},
};
</script>

+ 77
- 72
js/src/components/Account/Login.vue View File

@ -60,83 +60,88 @@
<script>
import { LOGIN_USER } from '@/store/mutation-types';
import auth from '@/auth/index';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import { AUTH_TOKEN, AUTH_USER_ID, AUTH_USER_ACTOR } from '@/constants';
import { LOGIN } from '@/graphql/auth';
export default {
props: {
email: {
type: String,
required: false,
default: '',
},
password: {
type: String,
required: false,
default: '',
},
},
beforeCreate() {
if (this.$store.state.user) {
this.$router.push('/');
}
export default {
props: {
email: {
type: String,
required: false,
default: '',
},
components: {
'v-gravatar': Gravatar,
'avatar': RegisterAvatar
password: {
type: String,
required: false,
default: '',
},
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.';
},
},
beforeCreate() {
if (this.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,
},
};
},
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';
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();
this.$apollo.mutate({
mutation: LOGIN,
variables: {
email: this.credentials.email,
password: this.credentials.password
}
}).then((result) => {
this.saveUserData(result.data);
this.$router.push({name: 'Home'});
}).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';
},
};
saveUserData({login: login}) {
localStorage.setItem(AUTH_USER_ID, login.user.id);
localStorage.setItem(AUTH_USER_ACTOR, JSON.stringify(login.actor));
localStorage.setItem(AUTH_TOKEN, login.token);
}
},
};
</script>

+ 1
- 3
js/src/components/Account/PasswordReset.vue View File

@ -37,8 +37,6 @@
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'PasswordReset',
props: {
@ -80,7 +78,7 @@ export default {
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: {


+ 103
- 112
js/src/components/Account/Register.vue View File

@ -9,7 +9,7 @@
<v-tooltip bottom>
<v-btn
slot="activator"
:to="{ name: 'Login', params: { email: this.credentials.email, password: this.credentials.password } }"
:to="{ name: 'Login', params: { email, password } }"
>
<!-- <v-icon large>login</v-icon> -->
<span>Login</span>
@ -21,22 +21,22 @@
<div class="text-xs-center">
<v-avatar size="80px">
<transition name="avatar">
<component :is="validEmail()" v-bind="{email: credentials.email}"></component>
<component :is="validEmail()" v-bind="{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-form @submit="submit()" v-if="!validationSent">
<v-text-field
label="Username"
required
type="text"
v-model="credentials.username"
v-model="username"
:rules="[rules.required]"
:error="this.state.username.status"
:error-messages="this.state.username.msg"
:suffix="this.host()"
:error="state.username.status"
:error-messages="state.username.msg"
:suffix="host()"
hint="You will be able to create more identities once registered"
persistent-hint
>
@ -46,30 +46,30 @@
required
type="email"
ref="email"
v-model="credentials.email"
v-model="email"
:rules="[rules.required, rules.email]"
:error="this.state.email.status"
:error-messages="this.state.email.msg"
:error="state.email.status"
:error-messages="state.email.msg"
>
</v-text-field>
<v-text-field
label="Password"
required
:type="showPassword ? 'text' : 'password'"
v-model="credentials.password"
v-model="password"
:rules="[rules.required, rules.password_length]"
:error="this.state.password.status"
:error-messages="this.state.password.msg"
:error="state.password.status"
:error-messages="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-btn @click="submit()" color="primary">Register</v-btn>
<router-link :to="{ name: 'ResendConfirmation', params: { 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-if="validationSent">
<h2><translate>A validation email was sent to %{email}</translate></h2>
<v-alert :value="true" type="info"><translate>Before you can login, you need to click on the link inside it to validate your account</translate></v-alert>
</div>
</v-card-text>
</v-card>
@ -79,110 +79,101 @@
</template>
<script>
import auth from '@/auth/index';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import Gravatar from 'vue-gravatar';
import RegisterAvatar from './RegisterAvatar';
import { CREATE_USER } from '@/graphql/user';
export default {
props: {
email: {
type: String,
required: false,
default: '',
},
password: {
type: String,
required: false,
default: '',
},
export default {
props: {
default_email: {
type: String,
required: false,
default: '',
},
components: {
'v-gravatar': Gravatar,
'avatar': RegisterAvatar
default_password: {
type: String,
required: false,
default: '',
},
mounted() {
this.credentials.email = this.email;
this.credentials.password = this.password;
},
data() {
return {
credentials: {
username: '',
email: '',
password: '',
},
components: {
'v-gravatar': Gravatar,
avatar: RegisterAvatar,
},
data() {
return {
username: '',
email: this.default_email,
password: this.default_password,
error: {
show: false,
},
showPassword: false,
validationSent: false,
state: {
email: {
status: false,
msg: [],
},
error: {
show: false,
username: {
status: false,
msg: [],
},
showPassword: false,
validationSent: false,
state: {
email: {
status: false,
msg: [],
},
username: {
status: false,
msg: [],
},
password: {
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: {
resetState() {
this.state = {
email: {
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.';
},
username: {
status: false,
msg: '',
},
password: {
status: false,
msg: '',
},
};
},
methods: {