Move to GraphQL

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2018-11-06 10:30:27 +01:00
parent 4bd2dd8a0e
commit 4441521994
149 changed files with 5605 additions and 4665 deletions

2
.gitignore vendored
View File

@ -23,3 +23,5 @@ priv/data/*
!priv/data/.gitkeep
.vscode/
cover/
uploads/*
!uploads/.gitkeep

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

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
docker/tests/Dockerfile Normal file
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/*

View File

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

View File

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

58
js/Makefile Normal file
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
js/get_union_json.js Normal file
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!');
}
});
});

2182
js/package-lock.json generated

File diff suppressed because it is too large Load Diff

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": [

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>

View File

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

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();
});
}

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')}`,
};
},
};

View File

@ -1,51 +1,53 @@
<template>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<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="!loading">
<v-img :src="actor.banner || 'https://picsum.photos/400/'" height="300px">
<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="$store.state.user && $store.state.actor.id === actor.id">
<!-- <v-btn icon class="mr-3" v-if="actor.id === data.actor.id">
<v-icon>edit</v-icon>
</v-btn>
</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 @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="$store.state.user && $store.state.actor.id === actor.id">
<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-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"
<img v-if="!data.actor.avatarUrl"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="actor.avatar"
:src="data.actor.avatarUrl"
>
</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>
<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>
@ -85,12 +87,12 @@
</v-list-tile-content>
</v-list-tile>
</v-list>
<v-container fluid grid-list-md v-if="actor.participatingEvents && actor.participatingEvents.length > 0">
<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 actor.participatingEvents" :key="event.id">
<v-flex v-for="event in data.actor.participatingEvents" :key="event.id">
<v-card>
<v-card-media
<v-img
class="black--text"
height="200px"
src="https://picsum.photos/400/200/"
@ -102,7 +104,7 @@
</v-flex>
</v-layout>
</v-container>
</v-card-media>
</v-img>
<v-card-title>
<div>
<span class="grey--text">{{ event.startDate | formatDate }} à {{ event.location }}</span><br>
@ -126,31 +128,30 @@
</v-flex>
</v-layout>
</v-container>
<v-container fluid grid-list-md v-if="actor.organized_events && actor.organized_events.length > 0">
<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 actor.organized_events" :key="event.id">
<v-flex v-for="event in data.actor.organizedEvents" :key="event.id" md6>
<v-card>
<v-card-media
class="black--text"
<v-img
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>
/>
<v-card-title primary-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>
<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>
@ -168,49 +169,42 @@
</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,
}
};
},
props: {
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>

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>

View File

@ -60,12 +60,12 @@
<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 {
export default {
props: {
email: {
type: String,
@ -79,13 +79,13 @@ import { LOGIN_USER } from '@/store/mutation-types';
},
},
beforeCreate() {
if (this.$store.state.user) {
if (this.user) {
this.$router.push('/');
}
},
components: {
'v-gravatar': Gravatar,
'avatar': RegisterAvatar
avatar: RegisterAvatar,
},
mounted() {
this.credentials.email = this.email;
@ -119,24 +119,29 @@ import { LOGIN_USER } from '@/store/mutation-types';
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);
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>

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: {

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,18 +79,18 @@
</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 {
export default {
props: {
email: {
default_email: {
type: String,
required: false,
default: '',
},
password: {
default_password: {
type: String,
required: false,
default: '',
@ -98,19 +98,13 @@
},
components: {
'v-gravatar': Gravatar,
'avatar': RegisterAvatar
},
mounted() {
this.credentials.email = this.email;
this.credentials.password = this.password;
avatar: RegisterAvatar,
},
data() {
return {
credentials: {
username: '',
email: '',
password: '',
},
email: this.default_email,
password: this.default_password,
error: {
show: false,
},
@ -141,24 +135,6 @@
};
},
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: {
@ -179,10 +155,25 @@
return `@${window.location.host}`;
},
validEmail() {
return this.rules.email(this.credentials.email) === true ? 'v-gravatar' : 'avatar';
}
return this.rules.email(this.email) === true ? 'v-gravatar' : 'avatar';
},
};
submit() {
this.$apollo.mutate({
mutation: CREATE_USER,
variables: {
email: this.email,
password: this.password,
username: this.username,
},
}).then((data) => {
console.log(data);
this.validationSent = true;
}).catch((error) => {
console.error(error);
});
},
},
};
</script>
<style lang="scss">
.avatar-enter-active {

View File

@ -3,7 +3,7 @@
</template>
<script>
export default {
name: 'RegisterAvatar'
}
name: 'RegisterAvatar',
};
</script>

View File

@ -31,8 +31,6 @@
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'ResendConfirmation',
props: {

View File

@ -31,8 +31,6 @@
</template>
<script>
import fetchStory from '@/api/eventFetch';
export default {
name: 'SendPasswordReset',
props: {

View File

@ -1,18 +1,17 @@
<template>
<v-container>
<h1 v-if="loading">{{ $t('registration.validation.process') }}</h1>
<h1 v-if="loading"><translate>Your account is being validated</translate></h1>
<div v-else>
<div v-if="failed">
<v-alert :value="true" variant="danger">Error while validating account</v-alert>
<v-alert :value="true" variant="danger"><translate>Error while validating account</translate></v-alert>
</div>
<h1 v-else>{{ $t('registration.validation.finished') }}</h1>
<h1 v-else><translate>Your account has been validated</translate></h1>
</div>
</v-container>
</template>
<script>
import fetchStory from '@/api/eventFetch';
import { LOGIN_USER } from '@/store/mutation-types';
import { VALIDATE_USER } from '@/graphql/user';
export default {
name: 'Validate',
@ -33,20 +32,27 @@ export default {
},
methods: {
validateAction() {
fetchStory(`/users/validate/${this.token}`, this.$store).then((data) => {
this.$apollo.mutate({
mutation: VALIDATE_USER,
variables: {
token: this.token,
},
}).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(() => {
console.log(data);
this.saveUserData(data.data);
this.$router.push({name: 'Home'});
}).catch((error) => {
this.loading = false;
console.log(error);
this.failed = true;
this.loading = false;
});
});
},
saveUserData({validateUser: 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>

View File

@ -1,42 +1,87 @@
<template>
<div>
<h3>Create a new category</h3>
<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><translate>Create a new category</translate></v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-form>
<v-text-field
label="Name of the category"
v-model="category.title"
:label="$gettext('Name of the category')"
v-model="title"
:counter="100"
required
></v-text-field>
<v-textarea
:label="$gettext('Description')"
v-model="description"
></v-textarea>
<v-flex xs12 class="text-xs-center text-sm-center text-md-center text-lg-center">
<v-img :src="image.url" height="150" v-if="image.url" aspect-ratio="1" contain/>
<v-text-field label="Select Image" @click='pickFile' v-model='image.name' prepend-icon='attach_file'></v-text-field>
<input
type="file"
style="display: none"
ref="image"
accept="image/*"
@change="onFilePicked"
>
</v-flex>
<v-btn color="primary" @click="create"><translate>Create category</translate></v-btn>
</v-form>
<v-btn color="primary" @click="create">Create category</v-btn>
</div>
</v-card-text>
</v-card>
</v-flex>
</v-layout>
</v-container>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import { UPLOAD_PICTURE } from '@/graphql/upload';
import { CREATE_CATEGORY } from '@/graphql/category';
export default {
export default {
name: 'create-category',
data() {
return {
category: {
title: '',
description: '',
image: {
url: '',
name: '',
file: '',
},
};
},
methods: {
create() {
const router = this.$router;
eventFetch('/categories', this.$store, { method: 'POST', body: JSON.stringify({ category: this.category }) })
.then(response => response.json())
.then(() => {
this.loading = false;
router.push('/category')
this.$apollo.mutate({
mutation: CREATE_CATEGORY,
variables: {
title: this.title,
description: this.description,
picture: this.$refs.image.files[0],
}
}).then((data) => {
console.log(data);
}).catch((error) => {
console.error(error);
});
},
pickFile () {
this.$refs.image.click ()
},
};
onFilePicked(e) {
const files = e.target.files;
if(files[0] === undefined || files[0].name.lastIndexOf('.') <= 0) {
console.error("File is incorrect")
}
this.image.name = files[0].name;
},
},
};
</script>
<style>

View File

@ -1,14 +1,13 @@
<template>
<v-container>
<h1>Category List</h1>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-container fluid grid-list-md class="grey lighten-4">
<v-layout row wrap v-if="!loading">
<v-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular>
<v-layout row wrap v-else>
<v-flex xs12 sm6 md3 v-for="category in categories" :key="category.id">
<v-card>
<v-card-media v-if="category.image" :src="'/images/categories/' + category.image.name" height="200px">
</v-card-media>
<v-img v-if="category.picture.url" :src="HTTP_ENDPOINT + category.picture.url" height="200px">
</v-img>
<v-card-title primary-title>
<div>
<h3 class="headline mb-0">{{ category.title }}</h3>
@ -16,8 +15,8 @@
</div>
</v-card-title>
<v-card-actions>
<v-btn flat class="orange--text">Explore</v-btn>
<v-btn flat class="red--text" v-on:click="deleteCategory(category.id)">Delete</v-btn>
<v-btn flat class="orange--text"><translate>Explore</translate></v-btn>
<v-btn flat class="red--text" v-on:click="deleteCategory(category.id)"><translate>Delete</translate></v-btn>
</v-card-actions>
</v-card>
</v-flex>
@ -32,40 +31,36 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
import { FETCH_CATEGORIES } from '@/graphql/category';
export default {
// TODO : remove this hardcode
export default {
name: 'Home',
data() {
return {
categories: [],
loading: true,
HTTP_ENDPOINT: 'http://localhost:4000',
};
},
created() {
this.fetchData();
apollo: {
categories: {
query: FETCH_CATEGORIES,
},
},
methods: {
fetchData() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
this.categories = response.data;
});
},
deleteCategory(categoryId) {
const router = this.$router;
eventFetch('/categories/' + categoryId, this.$store, {method: 'DELETE'})
eventFetch(`/categories/${categoryId}`, this.$store, { method: 'DELETE' })
.then(() => {
this.categories = this.categories.filter((category) => {
return category.id !== categoryId;
});
this.categories = this.categories.filter(category => category.id !== categoryId);
router.push('/category');
});
}
},
};
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -14,6 +14,8 @@
:counter="100"
required
></v-text-field>
<v-date-picker v-model="event.begins_on">
</v-date-picker>
<v-radio-group v-model="event.location_type" row>
<v-radio label="Address" value="physical" off-icon="place"></v-radio>
<v-radio label="Online" value="online" off-icon="link"></v-radio>
@ -64,16 +66,22 @@
</template>
<script>
// import Location from '@/components/Location';
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
// import Location from '@/components/Location';
import VueMarkdown from 'vue-markdown';
import { CREATE_EVENT, EDIT_EVENT } from '@/graphql/event';
import { FETCH_CATEGORIES } from '@/graphql/category';
import { AUTH_USER_ACTOR } from '@/constants';
export default {
export default {
name: 'create-event',
props: ['id'],
props: {
uuid: {
required: false,
type: String,
},
},
components: {
/* Location,*/
/* Location, */
VueMarkdown,
},
data() {
@ -81,8 +89,8 @@
e1: 0,
event: {
title: null,
description: null,
begins_on: new Date(),
description: '',
begins_on: (new Date()).toISOString().substr(0, 10),
ends_on: new Date(),
seats: null,
physical_address: null,
@ -101,14 +109,15 @@
tagsFetched: [],
};
},
created() {
if (this.id) {
this.fetchEvent();
}
// created() {
// if (this.uuid) {
// this.fetchEvent();
// }
// },
apollo: {
categories: {
query: FETCH_CATEGORIES,
},
mounted() {
this.fetchCategories();
this.fetchTags();
},
methods: {
create() {
@ -119,60 +128,51 @@
// // '@type': 'Tag',
// });
// });
const actor = JSON.parse(localStorage.getItem(AUTH_USER_ACTOR));
this.event.category_id = this.event.category;
this.event.organizer_actor_id = this.$store.state.actor.id;
this.event.participants = [this.$store.state.actor.id];
this.event.organizer_actor_id = actor.id;
this.event.participants = [actor.id];
// this.event.price = parseFloat(this.event.price);
if (this.id === undefined) {
eventFetch('/events', this.$store, {method: 'POST', body: JSON.stringify({ event: this.event })})
.then(response => response.json())
.then((data) => {
if (this.uuid === undefined) {
this.$apollo.mutate({
mutation: CREATE_EVENT,
variables: {
title: this.event.title,
description: this.event.description,
organizerActorId: this.event.organizer_actor_id,
categoryId: this.event.category_id,
beginsOn: this.event.begins_on,
addressType: this.event.location_type,
}
}).then((data) => {
this.loading = false;
this.$router.push({name: 'Event', params: {uuid: data.data.uuid}});
}).catch((err) => {
Promise.resolve(err).then((err) => {
console.log('err creation', err);
});
this.$router.push({ name: 'Event', params: { uuid: data.data.uuid } });
}).catch((error) => {
console.log(error);
});
} else {
eventFetch(`/events/${this.uuid}`, this.$store, {method: 'PUT', body: JSON.stringify(this.event)})
.then(response => response.json())
.then((data) => {
this.$apollo.mutate({
mutation: EDIT_EVENT,
}).then((data) => {
this.loading = false;
this.$router.push({name: 'Event', params: {uuid: data.uuid}});
this.$router.push({ name: 'Event', params: { uuid: data.data.uuid } });
}).catch((error) => {
console.log(error);
});
}
this.event.tags = [];
},
fetchCategories() {
eventFetch('/categories', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
this.categories = response.data;
});
},
fetchTags() {
eventFetch('/tags', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
response.data.forEach((tag) => {
this.tagsFetched.push(tag.name);
});
});
},
fetchEvent() {
eventFetch(`/events/${this.id}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.event = data;
console.log(this.event);
});
},
getAddressData: function (addressData) {
// fetchEvent() {
// eventFetch(`/events/${this.id}`, this.$store)
// .then(response => response.json())
// .then((data) => {
// this.loading = false;
// this.event = data;
// console.log(this.event);
// });
// },
getAddressData(addressData) {
if (addressData !== null) {
this.event.address = {
geom: {
@ -180,7 +180,7 @@
latitude: addressData.latitude,
longitude: addressData.longitude,
},
type: "point",
type: 'point',
},
addressCountry: addressData.country,
addressLocality: addressData.locality,
@ -191,7 +191,7 @@
}
},
},
};
};
</script>
<style>

View File

@ -98,9 +98,7 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
export default {
props: ['id'],
data() {
return {
@ -121,6 +119,6 @@
console.log(this.event);
});
},
}
}
},
};
</script>

View File

@ -1,10 +1,10 @@
<template>
<v-layout row>
<v-flex xs12 sm6 offset-sm3>
<span v-if="error">Error : event not found</span>
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
<v-card v-if="!loading && !error">
<v-img
<v-progress-circular v-if="$apollo.loading" indeterminate color="primary"></v-progress-circular>
<div>{{ event }}</div>
<v-card v-if="event">
<!-- <v-img
src="https://picsum.photos/600/400/"
height="200px"
>
@ -16,7 +16,7 @@
<v-icon>chevron_left</v-icon>
</v-btn>
<v-spacer></v-spacer>
<v-btn icon class="mr-3 white--text" v-if="actorIsOrganizer()" :to="{ name: 'EditEvent', params: {id: event.id}}">
<v-btn icon class="mr-3 white--text" v-if="actorIsOrganizer()" :to="{ name: 'EditEvent', params: {uuid: event.uuid}}">
<v-icon>edit</v-icon>
</v-btn>
<v-menu bottom left>
@ -36,7 +36,7 @@
</v-flex>
</v-layout>
</v-container>
</v-img>
</v-img> -->
<v-container grid-list-md>
<v-layout row wrap>
<v-flex md10>
@ -44,19 +44,19 @@
<span class="subheading grey--text">{{ event.begins_on | formatDay }}</span>
<h1 class="display-1">{{ event.title }}</h1>
<div>
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
<!-- <router-link :to="{name: 'Account', params: { name: event.organizerActor.preferredUsername } }">
<v-avatar size="25px">
<img class="img-circle elevation-7 mb-1"
:src="event.organizer.avatar"
:src="event.organizer_actor.avatarUrl"
>
</v-avatar>
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name ? event.organizer.display_name : event.organizer.username }}</span>
</router-link> -->
<!-- <span v-if="event.organizerActor">Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span> -->
</div>
<!--<p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey&#45;&#45;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>-->
<!-- <p><router-link :to="{ name: 'Account', params: {id: event.organizer.id} }"><span class="grey&#45;&#45;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-flex>
<v-flex md2>
<!-- <v-flex md2>
<p v-if="actorIsOrganizer()">
Vous êtes organisateur de cet événement.
</p>
@ -72,7 +72,7 @@
<v-btn v-if="!actorIsParticipant()" @click="joinEvent" color="success"><v-icon>check</v-icon> Join</v-btn>
<v-btn v-if="actorIsParticipant()" @click="leaveEvent" color="error">Leave</v-btn>
</v-card-actions>
</v-flex>
</v-flex> -->
</v-layout>
</v-container>
<v-divider></v-divider>
@ -115,24 +115,28 @@
<v-flex md8 xs12>
<p>
<h2>Details</h2>
<vue-markdown :source="event.description" v-if="event.description" :toc-first-level="3" />
<vue-markdown :source="event.description" v-if="event.description" :toc-first-level="3"></vue-markdown>
</p>
<v-subheader>Participants</v-subheader>
<v-flex md2 v-for="actor in event.participants" :key="actor.uuid">
<router-link :to="{name: 'Account', params: { name: actor.username }}">
<!-- <v-flex md2 v-for="participant in event.participants" :key="participant.actor.uuid">
<router-link :to="{name: 'Account', params: { name: participant.actor.preferredUsername }}">
<v-card>
<v-avatar size="75px">
<img v-if="!actor.avatar"
<img v-if="!participant.actor.avatarUrl"
class="img-circle elevation-7 mb-1"
src="https://picsum.photos/125/125/"
>
<img v-else
class="img-circle elevation-7 mb-1"
:src="actor.avatar"
:src="participant.actor.avatarUrl"
>
</v-avatar>
<v-card-title>
<span>{{ participant.actor.preferredUsername }}</span>
</v-card-title>
</v-card>
</router-link>
<span>{{ actor.username }}</span>
</v-flex>
</v-flex> -->
</v-flex>
<span v-if="event.participants.length === 0">No participants yet.</span>
</v-layout>
@ -143,18 +147,17 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
import VueMarkdown from 'vue-markdown';
import { FETCH_EVENT } from '@/graphql/event';
import { LOGGED_ACTOR } from '@/graphql/actor';
export default {
export default {
name: 'Home',
components: {
VueMarkdown,
},
data() {
return {
loading: true,
error: false,
event: {
name: '',
slug: '',
@ -169,26 +172,24 @@
},
};
},
apollo: {
event: {
query: FETCH_EVENT,
variables() {
return {
uuid: this.uuid,
};
},
},
// loggedActor: {
// query: LOGGED_ACTOR,
// }
},
methods: {
deleteEvent() {
const router = this.$router;
eventFetch(`/events/${this.uuid}`, this.$store, { method: 'DELETE' })
.then(() => router.push({'name': 'EventList'}));
},
fetchData() {
eventFetch(`/events/${this.uuid}`, this.$store)
.then(response => response.json())
.then((data) => {
this.loading = false;
this.event = data.data;
console.log('event', this.event);
}).catch((res) => {
Promise.resolve(res).then((data) => {
console.log(data);
this.error = true;
this.loading = false;
});
});
.then(() => router.push({ name: 'EventList' }));
},
joinEvent() {
eventFetch(`/events/${this.uuid}/join`, this.$store, { method: 'POST' })
@ -205,24 +206,24 @@
});
},
downloadIcsEvent() {
eventFetch(`/events/${this.uuid}/ics`, this.$store, {responseType: 'arraybuffer'})
.then((response) => response.text())
.then(response => {
const blob = new Blob([response],{type: 'text/calendar'});
eventFetch(`/events/${this.uuid}/ics`, this.$store, { responseType: 'arraybuffer' })
.then(response => response.text())
.then((response) => {
const blob = new Blob([response], { type: 'text/calendar' });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `${this.event.title}.ics`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
});
},
actorIsParticipant() {
return this.$store.state.actor && this.event.participants.map(participant => participant.id).includes(this.$store.state.actor.id) || this.actorIsOrganizer();
},
actorIsOrganizer() {
return this.$store.state.actor && this.$store.state.actor.id === this.event.organizer.id;
}
// actorIsParticipant() {
// return this.loggedActor && this.event.participants.map(participant => participant.actor.preferredUsername).includes(this.loggedActor.preferredUsername) || this.actorIsOrganizer();
// },
// actorIsOrganizer() {
// return this.loggedActor && this.loggedActor.preferredUsername === this.event.organizer.preferredUsername;
// },
},
props: {
uuid: {
@ -230,10 +231,7 @@
required: true,
},
},
created() {
this.fetchData();
},
};
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -54,16 +54,15 @@
</template>
<script>
import ngeohash from 'ngeohash';
import VueMarkdown from 'vue-markdown';
import eventFetch from '@/api/eventFetch';
import VCardTitle from "vuetify/es5/components/VCard/VCardTitle";
import ngeohash from 'ngeohash';
import VueMarkdown from 'vue-markdown';
import VCardTitle from 'vuetify/es5/components/VCard/VCardTitle';
export default {
export default {
name: 'EventList',
components: {
VCardTitle,
VueMarkdown
VueMarkdown,
},
data() {
return {
@ -80,26 +79,26 @@
watch: {
locationChip(val) {
if (val === false) {
this.$router.push({name: 'EventList'});
}
this.$router.push({ name: 'EventList' });
}
},
},
beforeRouteUpdate(to, from, next) {
this.fetchData(to.params.location);
next();
},
methods: {
geocode(lat, lon) {
console.log({lat, lon});
console.log({ lat, lon });
console.log(ngeohash.encode(lat, lon, 10));
return ngeohash.encode(lat, lon, 10);
},
fetchData(location) {
let queryString = '/events';
if (location) {
queryString += ('?geohash=' + location);
queryString += (`?geohash=${location}`);
const { latitude, longitude } = ngeohash.decode(location);
this.locationText = latitude.toString() + ' : ' + longitude.toString();
this.locationText = `${latitude.toString()} : ${longitude.toString()}`;
}
this.locationChip = true;
eventFetch(queryString, this.$store)
@ -112,27 +111,27 @@
},
deleteEvent(event) {
const router = this.$router;
eventFetch(`/events/${event.uuid}`, this.$store, {'method': 'DELETE'})
eventFetch(`/events/${event.uuid}`, this.$store, { method: 'DELETE' })
.then(() => router.push('/events'));
},
viewEvent(event) {
this.$router.push({ name: 'Event', params: { uuid: event.uuid } })
this.$router.push({ name: 'Event', params: { uuid: event.uuid } });
},
downloadIcsEvent(event) {
eventFetch(`/events/${event.uuid}/ics`, this.$store, {responseType: 'arraybuffer'})
.then((response) => response.text())
.then(response => {
const blob = new Blob([response],{type: 'text/calendar'});
eventFetch(`/events/${event.uuid}/ics`, this.$store, { responseType: 'arraybuffer' })
.then(response => response.text())
.then((response) => {
const blob = new Blob([response], { type: 'text/calendar' });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = `${event.title}.ics`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
});
},
},
};
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -65,11 +65,10 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
import VueMarkdown from 'vue-markdown';
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
import VueMarkdown from 'vue-markdown';
import VuetifyGoogleAutocomplete from 'vuetify-google-autocomplete';
export default {
export default {
name: 'create-group',
components: {
@ -95,7 +94,7 @@
create() {
// this.group.organizer = "/accounts/" + this.$store.state.user.id;
eventFetch('/groups', this.$store, { method: 'POST', body: JSON.stringify({group: this.group}) })
eventFetch('/groups', this.$store, { method: 'POST', body: JSON.stringify({ group: this.group }) })
.then(response => response.json())
.then((data) => {
this.loading = false;
@ -110,7 +109,7 @@
this.categories = data.data;
});
},
getAddressData: function (addressData) {
getAddressData(addressData) {
this.group.address = {
geo: {
latitude: addressData.latitude,
@ -124,7 +123,7 @@
};
},
},
};
};
</script>
<style>

View File

@ -202,28 +202,26 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
export default {
name: 'Group',
data() {
return {
group: null,
loading: true,
}
};
},
props: {
name: {
type: String,
required: true,
}
},
},
created() {
this.fetchData();
},
watch: {
// call again the method if the route changes
'$route': 'fetchData'
$route: 'fetchData',
},
methods: {
fetchData() {
@ -233,8 +231,8 @@
this.group = response.data;
this.loading = false;
console.log(this.group);
})
}
}
}
});
},
},
};
</script>

View File

@ -38,9 +38,7 @@
</template>
<script>
import eventFetch from '@/api/eventFetch';
export default {
export default {
name: 'GroupList',
data() {
return {
@ -53,7 +51,7 @@
},
methods: {
username_with_domain(actor) {
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`)
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
},
fetchData() {
eventFetch('/groups', this.$store)
@ -66,21 +64,21 @@
},
deleteGroup(group) {
const router = this.$router;
eventFetch(`/groups/${this.username_with_domain(group)}`, this.$store, {'method': 'DELETE'})
eventFetch(`/groups/${this.username_with_domain(group)}`, this.$store, { method: 'DELETE' })
.then(response => response.json())
.then(() => router.push('/groups'));
},
viewActor(actor) {
this.$router.push({ name: 'Group', params: { name: this.username_with_domain(actor) } })
this.$router.push({ name: 'Group', params: { name: this.username_with_domain(actor) } });
},
joinGroup(group) {
const router = this.$router;
eventFetch(`/groups/${this.username_with_domain(group)}/join`, this.$store, { method: 'POST' })
.then(response => response.json())
.then(() => router.push({ name: 'Group', params: { name: this.username_with_domain(group) } }));
}
},
};
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->

View File

@ -5,14 +5,14 @@
src="https://picsum.photos/1200/900"
dark
height="300"
v-if="$store.state.user === false"
v-if="!user"
>
<v-container fill-height>
<v-layout align-center>
<v-flex text-xs-center>
<h1 class="display-3">Find events you like</h1>
<h2>Share it with Mobilizon</h2>
<v-btn :to="{ name: 'Register' }">{{ $t("home.register") }}</v-btn>
<v-btn :to="{ name: 'Register' }"><translate>Register</translate></v-btn>
</v-flex>
</v-layout>
</v-container>
@ -21,7 +21,7 @@
<v-flex xs12 sm8 offset-sm2>
<v-layout row wrap>
<v-flex xs12 sm6>
<h1>Welcome back {{ $store.state.actor.username }}</h1>
<h1><translate :translate-params="{username: actor.preferredUsername}">Welcome back %{username}</translate></h1>
</v-flex>
<v-flex xs12 sm6>
<v-layout align-center>
@ -33,11 +33,14 @@
</v-layout>
</v-flex>
</v-layout>
<div v-if="$apollo.loading">
Still loading
</div>
<v-card v-if="events.length > 0">
<v-layout row wrap>
<v-flex md4 v-for="event in events" :key="event.uuid">
<v-card :to="{ name: 'Event', params:{ uuid: event.uuid } }">
<v-card-media v-if="!event.image"
<v-img v-if="!event.image"
class="white--text"
height="200px"
src="https://picsum.photos/g/400/200/"
@ -49,26 +52,26 @@
</v-flex>
</v-layout>
</v-container>
</v-card-media>
</v-img>
<v-card-title primary-title>
<div>
<span class="grey--text">{{ event.begins_on | formatDay }}</span><br>
<router-link :to="{name: 'Account', params: { name: event.organizer.username } }">
<router-link :to="{name: 'Account', params: { name: event.organizerActor.preferredUsername } }">
<v-avatar size="25px">
<img class="img-circle elevation-7 mb-1"
:src="event.organizer.avatar"
:src="event.organizerActor.avatarUrl"
>
</v-avatar>
</router-link>
<span v-if="event.organizer">Organisé par {{ event.organizer.display_name ? event.organizer.display_name : event.organizer.username }}</span>
<span v-if="event.organizerActor">Organisé par {{ event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername }}</span>
</div>
</v-card-title>
</v-card>
</v-flex>
</v-layout>
</v-card>
<v-alert v-else :value="true" type="info">
No events found nearby {{ ipLocation() }}
<v-alert v-else :value="true" type="error">
No events found
</v-alert>
</v-flex>
</v-layout>
@ -76,83 +79,75 @@
</template>
<script>
import ngeohash from 'ngeohash';
import eventFetch from "../api/eventFetch";
import {AUTH_USER_ACTOR, AUTH_USER_ID} from '@/constants';
import { FETCH_EVENTS } from '@/graphql/event';
export default {
name: 'Home',
data() {
return {
gradient: 'to top right, rgba(63,81,181, .7), rgba(25,32,72, .7)',
user: null,
searchTerm: null,
location_field: {
loading: false,
search: null,
},
locations: [],
events: [],
city: {name: null},
country: {name: null},
locations: [],
city: { name: null },
country: { name: null },
actor: JSON.parse(localStorage.getItem(AUTH_USER_ACTOR)),
user: localStorage.getItem(AUTH_USER_ID),
};
},
created() {
this.fetchData();
apollo: {
events: {
query: FETCH_EVENTS,
},
},
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.name === null ? this.actor.preferredUsername : this.actor.name;
},
},
methods: {
fetchLocations() {
eventFetch('/locations', this.$store)
.then((response) => (response.json()))
.then(response => (response.json()))
.then((response) => {
this.locations = response;
});
},
fetchData() {
eventFetch('/events', this.$store)
.then(response => response.json())
.then((response) => {
this.loading = false;
this.events = response.data;
this.city = response.city;
this.country = response.country;
});
},
geoLocalize() {
const router = this.$router;
if (sessionStorage.getItem('City')) {
router.push({name: 'EventList', params: {location: localStorage.getItem('City')}})
router.push({ name: 'EventList', params: { location: localStorage.getItem('City') } });
} else {
navigator.geolocation.getCurrentPosition((pos) => {
const crd = pos.coords;
const geohash = ngeohash.encode(crd.latitude, crd.longitude, 11);
sessionStorage.setItem('City', geohash);
router.push({name: 'EventList', params: {location: geohash}});
}, (err) => console.warn(`ERROR(${err.code}): ${err.message}`), {
router.push({ name: 'EventList', params: { location: geohash } });
}, err => console.warn(`ERROR(${err.code}): ${err.message}`), {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0
maximumAge: 0,
});
}
},
getAddressData: function (addressData) {
getAddressData(addressData) {
const geohash = ngeohash.encode(addressData.latitude, addressData.longitude, 11);
sessionStorage.setItem('City', geohash);
this.$router.push({name: 'EventList', params: {location: geohash}});
this.$router.push({ name: 'EventList', params: { location: geohash } });
},
viewEvent(event) {
this.$router.push({ name: 'Event', params: { uuid: event.uuid } })
this.$router.push({ name: 'Event', params: { uuid: event.uuid } });
},
ipLocation() {
return this.city.name ? this.city.name : this.country.name;
}
},
},
};
</script>

View File

@ -26,7 +26,7 @@
<script>
export default {
export default {
data() {
return {
description: 'Paris, France',
@ -47,6 +47,6 @@
this.$emit('input', place.formatted_address);
},
},
};
};
</script>

View File

@ -12,33 +12,36 @@
</router-link>
</v-toolbar-title>
<v-autocomplete
:loading="searchElement.loading"
:loading="$apollo.loading"
flat
solo-inverted
prepend-icon="search"
label="Search"
:label="$gettext('Search')"
required
item-text="displayedText"
item-text="label"
class="hidden-sm-and-down"
:items="searchElement.items"
:search-input.sync="search"
v-model="searchSelect"
:items="items"
:search-input.sync="searchText"
v-model="model"
return-object
>
<template slot="item" slot-scope="data">
<template v-if="typeof data.item !== 'object'">
<v-list-tile-content v-text="data.item"></v-list-tile-content>
</template>
<template v-else>
<!-- <div>{{ data }}</div> -->
<v-list-tile v-if="data.item.__typename === 'Event'">
<v-list-tile-avatar>
<img :src="data.item.avatar" v-if="data.item.avatar">
<v-icon v-else>event</v-icon>
<v-icon>event</v-icon>
</v-list-tile-avatar>
<v-list-tile-content v-text="data.item.label"></v-list-tile-content>
</v-list-tile>
<v-list-tile v-else-if="data.item.__typename === 'Actor'">
<v-list-tile-avatar>
<img :src="data.item.avatarUrl" v-if="data.item.avatarUrl">
<v-icon v-else>account_circle</v-icon>
</v-list-tile-avatar>
<v-list-tile-content>
<v-list-tile-title v-html="username_with_domain(data.item)"></v-list-tile-title>
<v-list-tile-sub-title v-html="data.item.type"></v-list-tile-sub-title>
</v-list-tile-content>
</template>
</v-list-tile>
</template>
</v-autocomplete>
<v-spacer></v-spacer>
@ -47,7 +50,7 @@
:close-on-content-click="false"
:nudge-width="200"
v-model="notificationMenu"
v-if="getUser()"
v-if="user"
>
<v-btn icon slot="activator">
<v-badge left color="red">
@ -70,19 +73,20 @@
</v-list>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn flat @click="notificationMenu = false">Close</v-btn>
<v-btn color="primary" flat @click="notificationMenu = false">Save</v-btn>
<v-btn flat @click="notificationMenu = false"><translate>Close</translate></v-btn>
<v-btn color="primary" flat @click="notificationMenu = false"><translate>Save</translate></v-btn>
</v-card-actions>
</v-card>
</v-menu>
<v-btn v-if="!$store.state.user" :to="{ name: 'Login' }">Se connecter</v-btn>
<v-btn v-if="!user" :to="{ name: 'Login' }"><translate>Login</translate></v-btn>
</v-toolbar>
</template>
<script>
import eventFetch from '@/api/eventFetch';
import {AUTH_USER_ACTOR, AUTH_USER_ID} from '@/constants';
import {SEARCH} from '@/graphql/search';
export default {
export default {
name: 'NavBar',
props: {
toggleDrawer: {
@ -94,87 +98,69 @@
return {
notificationMenu: false,
notifications: [
{header: 'Coucou'},
{title: "T'as une notification", subtitle: 'Et elle est cool'},
{ header: 'Coucou' },
{ title: "T'as une notification", subtitle: 'Et elle est cool' },
],
searchElement: {
loading: false,
items: [],
},
search: null,
model: null,
search: [],
searchText: null,
searchSelect: null,
actor: localStorage.getItem(AUTH_USER_ACTOR),
user: localStorage.getItem(AUTH_USER_ID),
};
},
watch: {
search (val) {
val && this.querySelections(val)
apollo: {
search: {
query: SEARCH,
variables() {
return {
searchText: this.searchText,
};
},
searchSelect(val) {
console.log('searchSelect', val);
if (val.type === 'Event') {
this.$router.push({name: 'Event', params: { uuid: val.uuid }});
} else if (val.type === 'Locality') {
this.$router.push({name: 'EventList', params: {location: val.geohash}});
} else {
this.$router.push({name: 'Account', params: { name : this.username_with_domain(val) }});
}
skip() {
return !this.searchText;
},
},
},
watch: {
model(val) {
switch(val.__typename) {
case 'Event':
this.$router.push({ name: 'Event', params: { uuid: val.uuid } });
break;
case 'Actor':
this.$router.push({ name: 'Account', params: { name: this.username_with_domain(val) } });
break;
}
},
},
computed: {
displayed_name: function() {
console.log('displayed name', this.$store.state.actor);
if (this.$store.state.actor) {
return this.$store.state.actor.display_name === null ? this.$store.state.actor.username : this.$store.state.actor.display_name;
items() {
return this.search.map(searchEntry => {
switch (searchEntry.__typename) {
case 'Actor':
searchEntry.label = searchEntry.preferredUsername;
break;
case 'Event':
searchEntry.label = searchEntry.title;
break;
}
return searchEntry;
});
},
displayed_name() {
console.log('displayed name', this.actor);
if (this.actor) {
return this.actor.display_name === null ? this.actor.username : this.actor.display_name;
}
},
},
methods: {
username_with_domain(actor) {
if (actor.type !== 'Event') {
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`)
}
return actor.title;
return actor.preferredUsername + (actor.domain === undefined ? '' : `@${actor.domain}`);
},
getUser() {
return this.$store.state.user === undefined ? false : this.$store.state.user;
},
querySelections(searchTerm) {
this.searchElement.loading = true;
eventFetch(`/search/${searchTerm}`, this.$store)
.then(response => response.json())
.then((results) => {
console.log('results');
console.log(results);
const accountResults = results.data.actors.map((result) => {
if (result.domain) {
result.displayedText = `${result.username}@${result.domain}`;
} else {
result.displayedText = result.username;
}
return result;
});
const eventsResults = results.data.events.map((result) => {
result.displayedText = result.title;
return result;
});
// const cities = new Set();
// const placeResults = results.places.map((result) => {
// result.displayedText = result.addressLocality;
// return result;
// }).filter((result) => {
// if (cities.has(result.addressLocality)) {
// return false;
// }
// cities.add(result.addressLocality);
// return true;
// });
this.searchElement.items = accountResults.concat(eventsResults);
this.searchElement.loading = false;
});
}
}
}
};
</script>
<style>

3
js/src/constants.js Normal file
View File

@ -0,0 +1,3 @@
export const AUTH_TOKEN = 'auth-token';
export const AUTH_USER_ID = 'auth-user-id';
export const AUTH_USER_ACTOR = 'auth-user-actor';

39
js/src/graphql/actor.js Normal file
View File

@ -0,0 +1,39 @@
import gql from 'graphql-tag';
export const FETCH_ACTOR = gql`
query($name:String!) {
actor(preferredUsername: $name) {
url,
outboxUrl,
inboxUrl,
followingUrl,
followersUrl,
sharedInboxUrl,
name,
domain,
summary,
preferredUsername,
suspended,
avatarUrl,
bannerUrl,
organizedEvents {
uuid,
title,
description,
organizer_actor {
avatarUrl,
preferred_username,
name,
}
},
}
}
`;
export const LOGGED_ACTOR = gql`
query {
loggedActor {
avatarUrl,
preferredUsername,
}
}`;

16
js/src/graphql/auth.js Normal file
View File

@ -0,0 +1,16 @@
import gql from 'graphql-tag';
export const LOGIN = gql`
mutation Login($email: String!, $password: String!) {
login(email: $email, password: $password) {
token,
user {
id,
},
actor {
avatarUrl,
preferredUsername,
}
},
}
`;

View File

@ -0,0 +1,29 @@
import gql from 'graphql-tag';
export const FETCH_CATEGORIES = gql`
query {
categories {
id,
title,
description,
picture {
url,
},
}
}
`;
export const CREATE_CATEGORY = gql`
mutation createCategory($title: String!, $description: String!, $picture: Upload!) {
createCategory(title: $title, description: $description, picture: $picture) {
id,
title,
description,
picture {
url,
url_thumbnail
},
},
},
`;

109
js/src/graphql/event.js Normal file
View File

@ -0,0 +1,109 @@
import gql from 'graphql-tag';
export const FETCH_EVENT = gql`
query($uuid:UUID!) {
event(uuid: $uuid) {
uuid,
url,
local,
title,
description,
begins_on,
ends_on,
state,
status,
public,
thumbnail,
large_image,
publish_at,
# address_type,
online_address,
phone,
organizerActor {
avatarUrl,
preferredUsername,
name,
},
attributedTo {
avatarUrl,
preferredUsername,
name,
},
participants {
actor {
avatarUrl,
preferredUsername,
name,
},
role,
},
category {
title,
},
}
}
`;
export const FETCH_EVENTS = gql`
query {
events {
uuid,
url,
local,
title,
description,
begins_on,
ends_on,
state,
status,
public,
thumbnail,
large_image,
publish_at,
# address_type,
online_address,
phone,
organizerActor {
avatarUrl,
preferredUsername,
name,
},
attributedTo {
avatarUrl,
preferredUsername,
name,
},
category {
title,
},
}
}
`;
export const CREATE_EVENT = gql`
mutation CreateEvent(
$title: String!,
$description: String!,
$organizerActorId: Int!,
$categoryId: Int!,
$beginsOn: DateTime!,
$addressType: AddressType!,
) {
createEvent(title: $title, description: $description, beginsOn: $beginsOn, organizerActorId: $organizerActorId, categoryId: $categoryId, addressType: $addressType) {
uuid
}
}
`;
export const EDIT_EVENT = gql`
mutation EditEvent(
$title: String!,
$description: String!,
$organizerActorId: Int!,
$categoryId: Int!,
) {
EditEvent(title: $title, description: $description, organizerActorId: $organizerActorId, categoryId: $categoryId) {
uuid
}
}
`;

View File

@ -0,0 +1 @@
{"__schema":{"types":[{"possibleTypes":[{"name":"Event"},{"name":"Actor"}],"name":"SearchResult","kind":"UNION"}]}}

17
js/src/graphql/search.js Normal file
View File

@ -0,0 +1,17 @@
import gql from 'graphql-tag';
export const SEARCH = gql`
query SearchEvents($searchText: String!) {
search(search: $searchText) {
...on Event {
title,
uuid,
__typename
},
...on Actor {
preferredUsername,
__typename
}
}
}
`;

10
js/src/graphql/upload.js Normal file
View File

@ -0,0 +1,10 @@
import gql from 'graphql-tag';
export const UPLOAD_PICTURE = gql`
mutation {
uploadPicture(file: "file") {
url,
url_thumbnail
}
}
`;

28
js/src/graphql/user.js Normal file
View File

@ -0,0 +1,28 @@
import gql from 'graphql-tag';
export const CREATE_USER = gql`
mutation CreateUser($email: String!, $username: String!, $password: String!) {
createUser(email: $email, username: $username, password: $password) {
preferredUsername,
user {
email,
confirmationSentAt
}
}
}
`;
export const VALIDATE_USER = gql`
mutation ValidateUser($token: String!) {
validateUser(token: $token) {
token,
user {
id,
},
actor {
avatarUrl,
preferredUsername,
}
}
}
`;

View File

@ -1,15 +0,0 @@
export default {
home: {
welcome: 'Welcome on Mobilizon, {username}',
welcome_off: 'Welcome on Mobilizon',
events: 'Events',
groups: 'Groups',
login: 'Login',
register: 'Register',
},
event: {
list: {
title: "Your event list",
},
},
};

View File

@ -1,20 +0,0 @@
export default {
home: {
welcome: 'Bienvenue sur Mobilizon, {username}!',
welcome_off: 'Bienvenue sur Mobilizon',
events: 'Événements',
groups: 'Groupes',
login: 'Se connecter',
register: "S'inscrire",
},
event: {
list: {
title: "Votre liste d'événements",
},
},
session: {
error: {
bad_login: 'Erreur lors de la connexion : Votre nom d\'utilisateur ou votre mot de passe est incorrect',
},
},
};

View File

@ -1,6 +0,0 @@
import en from './en';
import fr from './fr';
export default {
en, fr,
};

View File

@ -0,0 +1,30 @@
# English translations for mobilizon package.
# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/components/Account/Register.vue:70
msgid "A validation email was sent to %{email}"
msgstr "A validation email was sent to %{email}"
#: src/components/Account/Register.vue:71
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr "Before you can login, you need to click on the link inside it to validate your account"
#: src/components/Home.vue:14
msgid "Register"
msgstr "Register"

View File

@ -0,0 +1,30 @@
# French translations for mobilizon package.
# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/components/Account/Register.vue:70
msgid "A validation email was sent to %{email}"
msgstr ""
#: src/components/Account/Register.vue:71
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr ""
#: src/components/Home.vue:14
msgid "Register"
msgstr "S'inscrire"

View File

@ -0,0 +1,30 @@
# French translations for mobilizon package.
# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-10-24 16:25+0200\n"
"PO-Revision-Date: 2018-10-24 16:25+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/components/Account/Register.vue:70
msgid "A validation email was sent to %{email}"
msgstr ""
#: src/components/Account/Register.vue:71
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr ""
#: src/components/Home.vue:14
msgid "Register"
msgstr "S'inscrire"

View File

@ -0,0 +1 @@
{"en_US":{"A validation email was sent to %{email}":"A validation email was sent to %{email}","Before you can login, you need to click on the link inside it to validate your account":"Before you can login, you need to click on the link inside it to validate your account","Register":"Register"},"fr_FR":{"Register":"S'inscrire"}}

View File

@ -5,60 +5,37 @@ import Vue from 'vue';
import VueMarkdown from 'vue-markdown';
import Vuetify from 'vuetify';
import moment from 'moment';
import VuexI18n from 'vuex-i18n';
import GetTextPlugin from 'vue-gettext';
import 'material-design-icons/iconfont/material-icons.css';
import 'vuetify/dist/vuetify.min.css';
import App from './App.vue';
import router from './router';
import store from './store';
import translations from './i18n';
import auth from './auth';
import App from '@/App.vue';
import router from '@/router';
// import store from './store';
import translations from '@/i18n/translations.json';
import { createProvider } from './vue-apollo';
Vue.config.productionTip = false;
Vue.use(VueMarkdown);
Vue.use(Vuetify);
let language = window.navigator.userLanguage || window.navigator.language;
const language = window.navigator.userLanguage || window.navigator.language;
moment.locale(language);
Vue.filter('formatDate', value => (value ? moment(String(value)).format('LLLL') : null));
Vue.filter('formatDay', value => (value ? moment(String(value)).format('LL') : null));
if (!(language in translations)) {
[language] = language.split('-', 1);
}
Vue.use(VuexI18n.plugin, store);
Object.entries(translations).forEach((key) => {
Vue.i18n.add(key[0], key[1]);
Vue.use(GetTextPlugin, {
translations,
defaultLanguage: 'en_US',
});
Vue.i18n.set(language);
Vue.i18n.fallback('en');
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiredAuth) && !store.state.user) {
next({
name: 'Login',
query: { redirect: to.fullPath },
});
} else {
next();
}
});
auth.getUser(store, () => {}, (error) => {
console.warn(error);
});
console.log('store', store);
Vue.config.language = language.replace('-', '_');
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
template: '<App/>',
apolloProvider: createProvider(),
components: { App },
});

View File

@ -1,44 +0,0 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { LOGIN_USER, LOGOUT_USER, LOAD_USER, CHANGE_ACTOR } from './mutation-types';
const state = {
isLogged: !!localStorage.getItem('token'),
user: false,
actor: false,
defaultActor: localStorage.getItem('defaultActor') || null,
};
/* eslint-disable */
const mutations = {
[LOGIN_USER](state, user) {
state.isLogged = true;
state.user = user;
},
[LOAD_USER](state, user) {
state.user = user;
},
[LOGOUT_USER](state) {
state.isLogged = false;
state.user = null;
},
[CHANGE_ACTOR](state, actor) {
state.actor = actor;
state.defaultActor = actor.username;
}
};
/* eslint-enable */
Vue.use(Vuex);
const store = new Vuex.Store({ state, mutations });
store.subscribe((mutation, localState) => {
if (mutation === CHANGE_ACTOR) {
localStorage.setItem('defaultActor', localState.actor.username);
}
});
export default store;

View File

@ -1,4 +0,0 @@
export const LOGIN_USER = 'LOGIN_USER';
export const LOAD_USER = 'LOAD_USER';
export const LOGOUT_USER = 'LOGOUT_USER';
export const CHANGE_ACTOR = 'CHANGE_ACTOR';

135
js/src/vue-apollo.js Normal file
View File

@ -0,0 +1,135 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { ApolloLink } from 'apollo-link';
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { createLink } from 'apollo-absinthe-upload-link';
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client';
import { AUTH_TOKEN } from './constants';
// Install the vue plugin
Vue.use(VueApollo);
// Http endpoint
const httpEndpoint = process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000/api';
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData: {
__schema: {
types: [
{
kind: 'UNION',
name: 'SearchResult',
possibleTypes: [
{ name: 'Event' },
{ name: 'Actor' },
],
}, // this is an example, put your INTERFACE and UNION kinds here!
],
},
},
});
const cache = new InMemoryCache({ fragmentMatcher });
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem(AUTH_TOKEN);
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : null,
},
});
return forward(operation);
});
const uploadLink = createLink({
uri: httpEndpoint,
});
// const link = ApolloLink.from([
// uploadLink,
// authMiddleware,
// HttpLink,
// ]);
const link = authMiddleware.concat(uploadLink);
// Config
const defaultOptions = {
// You can use `https` for secure connection (recommended in production)
httpEndpoint,
// You can use `wss` for secure connection (recommended in production)
// Use `null` to disable subscriptions
// wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000/graphql',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
cache,
link,
defaultHttpLink: false,
};
// Call this in the Vue app file
export function createProvider(options = {}) {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
});
apolloClient.wsClient = wsClient;
// Create vue apollo provider
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
link,
cache,
connectToDevTools: true,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
errorHandler(error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message);
},
});
return apolloProvider;
}
// Manually call this when user log in
export async function onLogin(apolloClient, token) {
if (typeof localStorage !== 'undefined' && token) {
localStorage.setItem(AUTH_TOKEN, token);
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try {
await apolloClient.resetStore();
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message);
}
}
// Manually call this when user log out
export async function onLogout(apolloClient) {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN);
}
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient);
try {
await apolloClient.resetStore();
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message);
}
}

View File

@ -1,8 +1,8 @@
module.exports = {
env: {
mocha: true
mocha: true,
},
rules: {
'import/no-extraneous-dependencies': 'off'
}
}
'import/no-extraneous-dependencies': 'off',
},
};

View File

@ -14,7 +14,7 @@ defmodule Mix.Tasks.CreateBot do
def run([email, name, summary, type, url]) do
Mix.Task.run("app.start")
with {:ok, %User{} = user} <- Actors.find_by_email(email),
with {:ok, %User{} = user} <- Actors.get_user_by_email(email, true),
actor <- Actors.register_bot_account(%{name: name, summary: summary}),
{:ok, %Bot{} = bot} <-
Actors.create_bot(%{

View File

@ -11,6 +11,14 @@ defmodule Mobilizon.Actors do
alias Mobilizon.Service.ActivityPub
def data() do
Dataloader.Ecto.new(Repo, query: &query/2)
end
def query(queryable, _params) do
queryable
end
@doc """
Returns the list of actors.
@ -42,6 +50,28 @@ defmodule Mobilizon.Actors do
Repo.get!(Actor, id)
end
@doc """
Returns the associated actor for an user, either the default set one or the first found
"""
@spec get_actor_for_user(%Mobilizon.Actors.User{}) :: %Mobilizon.Actors.Actor{}
def get_actor_for_user(%Mobilizon.Actors.User{} = user) do
case user.default_actor_id do
nil -> get_first_actor_for_user(user)
actor_id -> get_actor!(actor_id)
end
end
@doc """
Returns the first actor found for an user
Useful when the user has not defined default actor
Raises `Ecto.NoResultsError` if no Actor is found for this ID
"""
defp get_first_actor_for_user(%Mobilizon.Actors.User{id: id} = _user) do
Repo.one!(from(a in Actor, where: a.user_id == ^id))
end
def get_actor_with_everything!(id) do
actor = Repo.get!(Actor, id)
Repo.preload(actor, :organized_events)
@ -162,10 +192,13 @@ defmodule Mobilizon.Actors do
Repo.all(User)
end
def list_users_with_actors do
users = Repo.all(User)
Repo.preload(users, :actors)
end
@doc """
List users with their associated actors. No reason for that, so removed
"""
# def list_users_with_actors do
# users = Repo.all(User)
# Repo.preload(users, :actors)
# end
defp blank?(""), do: nil
defp blank?(n), do: n
@ -226,6 +259,14 @@ defmodule Mobilizon.Actors do
Repo.preload(user, :actors)
end
@spec get_user_with_actor(integer()) :: %User{}
def get_user_with_actor(id) do
case Repo.get(User, id) do
nil -> {:error, "User with ID #{id} not found"}
user -> {:ok, Repo.preload(user, :actors)}
end
end
def get_actor_by_url(url) do
Repo.get_by(Actor, url: url)
end
@ -297,10 +338,17 @@ defmodule Mobilizon.Actors do
@doc """
Find actors by their name or displayed name
"""
def find_actors_by_username_or_name(username) do
def find_actors_by_username_or_name(username, page \\ 1, limit \\ 10)
def find_actors_by_username_or_name("", page, limit), do: []
def find_actors_by_username_or_name(username, page, limit) do
start = (page - 1) * limit
Repo.all(
from(
a in Actor,
limit: ^limit,
offset: ^start,
where:
ilike(a.preferred_username, ^like_sanitize(username)) or
ilike(a.name, ^like_sanitize(username))
@ -340,19 +388,6 @@ defmodule Mobilizon.Actors do
end
end
@doc """
Get an user by email
"""
def find_by_email(email) do
case Repo.preload(Repo.get_by(User, email: email), :actors) do
nil ->
{:error, nil}
user ->
{:ok, user}
end
end
@doc """
Authenticate user
"""
@ -390,31 +425,37 @@ defmodule Mobilizon.Actors do
nil
end
actor =
with actor_changeset <-
Mobilizon.Actors.Actor.registration_changeset(%Mobilizon.Actors.Actor{}, %{
preferred_username: username,
domain: nil,
keys: pem,
avatar_url: avatar
})
user =
}),
{:ok, %Mobilizon.Actors.Actor{id: id} = actor} <- Mobilizon.Repo.insert(actor_changeset),
user_changeset <-
Mobilizon.Actors.User.registration_changeset(%Mobilizon.Actors.User{}, %{
email: email,
password: password
})
actor_with_user = Ecto.Changeset.put_assoc(actor, :user, user)
try do
Mobilizon.Repo.insert!(actor_with_user)
find_by_email(email)
rescue
e in Ecto.InvalidChangesetError ->
{:error, e.changeset}
password: password,
default_actor_id: id
}),
{:ok, %Mobilizon.Actors.User{} = user} <- Mobilizon.Repo.insert(user_changeset) do
{:ok, Map.put(actor, :user, user)}
else
{:error, %Ecto.Changeset{} = changeset} ->
handle_actor_user_changeset(changeset)
end
end
defp handle_actor_user_changeset(changeset) do
changeset =
Ecto.Changeset.traverse_errors(changeset, fn
{msg, opts} -> msg
msg -> msg
end)
{:error, hd(Map.get(changeset, :email))}
end
def register_bot_account(%{name: name, summary: summary}) do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
@ -466,9 +507,16 @@ defmodule Mobilizon.Actors do
iex> get_user_by_email(user, wrong_email)
{:error, nil}
"""
def get_user_by_email(email) do
case Repo.get_by(User, email: email) do
nil -> {:error, nil}
def get_user_by_email(email, activated \\ nil) do
query =
case activated do
nil -> from(u in User, where: u.email == ^email)
true -> from(u in User, where: u.email == ^email and not is_nil(u.confirmed_at))
false -> from(u in User, where: u.email == ^email and is_nil(u.confirmed_at))
end
case Repo.one(query) do
nil -> {:error, :user_not_found}
user -> {:ok, user}
end
end

View File

@ -3,6 +3,7 @@ defmodule Mobilizon.Actors.Service.Activation do
alias Mobilizon.{Mailer, Repo, Actors.User, Actors}
alias Mobilizon.Email.User, as: UserEmail
alias Mobilizon.Actors.Service.Tools
require Logger
@ -15,7 +16,8 @@ defmodule Mobilizon.Actors.Service.Activation do
"confirmation_sent_at" => nil,
"confirmation_token" => nil
}) do
{:ok, Repo.preload(user, :actors)}
Logger.info("User #{user.email} has been confirmed")
{:ok, user}
else
_err ->
{:error, "Invalid token"}
@ -23,8 +25,12 @@ defmodule Mobilizon.Actors.Service.Activation do
end
def resend_confirmation_email(%User{} = user, locale \\ "en") do
{:ok, user} = Actors.update_user(user, %{"confirmation_sent_at" => DateTime.utc_now()})
with :ok <- Tools.we_can_send_email(user, :confirmation_sent_at),
{:ok, user} <- Actors.update_user(user, %{"confirmation_sent_at" => DateTime.utc_now()}) do
send_confirmation_email(user, locale)
Logger.info("Sent confirmation email again to #{user.email}")
{:ok, user.email}
end
end
def send_confirmation_email(%User{} = user, locale \\ "en") do

View File

@ -5,6 +5,7 @@ defmodule Mobilizon.Actors.Service.ResetPassword do
alias Mobilizon.{Mailer, Repo, Actors.User}
alias Mobilizon.Email.User, as: UserEmail
alias Mobilizon.Actors.Service.Tools
@doc """
Check that the provided token is correct and update provided password
@ -20,7 +21,7 @@ defmodule Mobilizon.Actors.Service.ResetPassword do
"reset_password_token" => nil
})
) do
{:ok, Repo.preload(user, :actors)}
{:ok, user}
else
err ->
{:error, :invalid_token}
@ -32,11 +33,11 @@ defmodule Mobilizon.Actors.Service.ResetPassword do
"""
@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),
with :ok <- Tools.we_can_send_email(user, :reset_password_sent_at),
{:ok, %User{} = user_updated} <-
Repo.update(
User.send_password_reset_changeset(user, %{
"reset_password_token" => random_string(30),
"reset_password_token" => Tools.random_string(30),
"reset_password_sent_at" => DateTime.utc_now()
})
) do
@ -50,28 +51,4 @@ defmodule Mobilizon.Actors.Service.ResetPassword do
{:error, reason} -> {:error, reason}
end
end
@spec random_string(integer) :: String.t()
defp random_string(length) do
length
|> :crypto.strong_rand_bytes()
|> 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

View File

@ -0,0 +1,27 @@
defmodule Mobilizon.Actors.Service.Tools do
alias Mobilizon.Actors.User
@spec we_can_send_email(User.t()) :: boolean
def we_can_send_email(%User{} = user, key \\ :reset_password_sent_at) do
case Map.get(user, key) do
nil ->
:ok
_ ->
case Timex.before?(Timex.shift(Map.get(user, key), hours: 1), DateTime.utc_now()) do
true ->
:ok
false ->
{:error, :email_too_soon}
end
end
end
@spec random_string(integer) :: String.t()
def random_string(length) do
length
|> :crypto.strong_rand_bytes()
|> Base.url_encode64()
end
end

View File

@ -12,6 +12,7 @@ defmodule Mobilizon.Actors.User do
field(:password, :string, virtual: true)
field(:role, :integer, default: 0)
has_many(:actors, Actor)
field(:default_actor_id, :integer)
field(:confirmed_at, :utc_datetime)
field(:confirmation_sent_at, :utc_datetime)
field(:confirmation_token, :string)
@ -27,6 +28,7 @@ defmodule Mobilizon.Actors.User do
|> cast(attrs, [
:email,
:role,
:default_actor_id,
:password_hash,
:confirmed_at,
:confirmation_sent_at,
@ -49,7 +51,8 @@ defmodule Mobilizon.Actors.User do
struct
|> changeset(params)
|> cast(params, ~w(password)a, [])
|> validate_required([:email, :password])
|> validate_required([:email, :password, :default_actor_id])
|> validate_email()
|> validate_length(
:password,
min: 6,
@ -92,6 +95,19 @@ defmodule Mobilizon.Actors.User do
end
end
defp validate_email(changeset) do
case changeset do
%Ecto.Changeset{valid?: true, changes: %{email: email}} ->
case EmailChecker.valid?(email) do
false -> add_error(changeset, :email, "Email doesn't fit required format")
_ -> changeset
end
_ ->
changeset
end
end
defp random_string(length) do
length
|> :crypto.strong_rand_bytes()

View File

@ -16,7 +16,7 @@ defmodule Mobilizon.Email.User do
base_email()
|> to(user.email)
|> subject(
gettext("Peakweaver: Confirmation instructions for %{instance}", instance: instance_url)
gettext("Mobilizon: Confirmation instructions for %{instance}", instance: instance_url)
)
|> put_header("Reply-To", get_config(:reply_to))
|> assign(:token, user.confirmation_token)
@ -32,7 +32,7 @@ defmodule Mobilizon.Email.User do
|> to(user.email)
|> subject(
gettext(
"Peakweaver: Reset your password on %{instance} instructions",
"Mobilizon: Reset your password on %{instance} instructions",
instance: instance_url
)
)

View File

@ -5,10 +5,11 @@ defmodule Mobilizon.Events.Category do
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Events.Category
use Arc.Ecto.Schema
schema "categories" do
field(:description, :string)
field(:picture, :string)
field(:picture, MobilizonWeb.Uploaders.Category.Type)
field(:title, :string, null: false)
timestamps()
@ -17,7 +18,8 @@ defmodule Mobilizon.Events.Category do
@doc false
def changeset(%Category{} = category, attrs) do
category
|> cast(attrs, [:title, :description, :picture])
|> cast(attrs, [:title, :description])
|> cast_attachments(attrs, [:picture])
|> validate_required([:title])
|> unique_constraint(:title)
end

View File

@ -86,7 +86,6 @@ defmodule Mobilizon.Events.Event do
|> validate_required([
:title,
:begins_on,
:ends_on,
:organizer_actor_id,
:category_id,
:url,

View File

@ -6,23 +6,16 @@ defmodule Mobilizon.Events do
import Ecto.Query, warn: false
alias Mobilizon.Repo
alias Mobilizon.Events.Event
alias Mobilizon.Events.Comment
alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Actors.Actor
alias Mobilizon.Addresses.Address
@doc """
Returns the list of events.
def data() do
Dataloader.Ecto.new(Mobilizon.Repo, query: &query/2)
end
## Examples
iex> list_events()
[%Event{}, ...]
"""
def list_events do
events = Repo.all(Event)
Repo.preload(events, [:organizer_actor])
def query(queryable, _params) do
queryable
end
def get_events_for_actor(%Actor{id: actor_id} = _actor, page \\ 1, limit \\ 10) do
@ -179,15 +172,47 @@ defmodule Mobilizon.Events do
])
end
@doc """
Returns the list of events.
## Examples
iex> list_events()
[%Event{}, ...]
"""
def list_events(page \\ 1, limit \\ 10) do
start = (page - 1) * limit
query =
from(e in Event,
limit: ^limit,
offset: ^start,
preload: [:organizer_actor]
)
Repo.all(query)
end
@doc """
Find events by name
"""
def find_events_by_name(name) when name == "", do: []
def find_events_by_name(name, page \\ 1, limit \\ 10)
def find_events_by_name("", page, limit), do: list_events(page, limit)
def find_events_by_name(name) do
def find_events_by_name(name, page, limit) do
name = String.trim(name)
events = Repo.all(from(a in Event, where: ilike(a.title, ^like_sanitize(name))))
Repo.preload(events, [:organizer_actor])
start = (page - 1) * limit
query =
from(e in Event,
limit: ^limit,
offset: ^start,
where: ilike(e.title, ^like_sanitize(name)),
preload: [:organizer_actor]
)
Repo.all(query)
end
@doc """
@ -210,9 +235,16 @@ defmodule Mobilizon.Events do
"""
def create_event(attrs \\ %{}) do
case %Event{} |> Event.changeset(attrs) |> Repo.insert() do
{:ok, %Event{} = event} -> {:ok, Repo.preload(event, [:organizer_actor])}
err -> err
with {:ok, %Event{} = event} <- %Event{} |> Event.changeset(attrs) |> Repo.insert(),
{:ok, %Participant{} = _participant} <-
%Participant{}
|> Participant.changeset(%{
actor_id: attrs.organizer_actor_id,
role: 4,
event_id: event.id
})
|> Repo.insert() do
{:ok, Repo.preload(event, [:organizer_actor])}
end
end
@ -475,6 +507,27 @@ defmodule Mobilizon.Events do
Repo.all(Participant)
end
@doc """
Returns the list of participants for an event.
## Examples
iex> list_participants_for_event(someuuid)
[%Participant{}, ...]
"""
def list_participants_for_event(uuid) do
Repo.all(
from(
p in Participant,
join: e in Event,
on: p.event_id == e.id,
where: e.uuid == ^uuid,
preload: [:actor]
)
)
end
@doc """
Gets a single participant.

View File

@ -8,7 +8,7 @@ defmodule MobilizonWeb.AuthPipeline do
module: MobilizonWeb.Guardian,
error_handler: MobilizonWeb.AuthErrorHandler
plug(Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"})
plug(Guardian.Plug.EnsureAuthenticated)
plug(Guardian.Plug.LoadResource, ensure: true)
plug(Guardian.Plug.VerifyHeader, realm: "Bearer")
plug(Guardian.Plug.LoadResource, allow_blank: true)
plug(MobilizonWeb.Context)
end

View File

@ -0,0 +1,20 @@
defmodule MobilizonWeb.Context do
@behaviour Plug
import Plug.Conn
require Logger
def init(opts) do
opts
end
def call(conn, _) do
case Guardian.Plug.current_resource(conn) do
nil ->
conn
user ->
put_private(conn, :absinthe, %{context: %{current_user: user}})
end
end
end

View File

@ -1,79 +0,0 @@
defmodule MobilizonWeb.ActorController do
@moduledoc """
Controller for Actors
"""
use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, User}
alias Mobilizon.Service.ActivityPub
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
actors = Actors.list_actors()
render(conn, "index.json", actors: actors)
end
def create(conn, %{"actor" => actor_params}) do
with %User{} = user <- Guardian.Plug.current_resource(conn),
actor_params <- Map.put(actor_params, "user_id", user.id),
actor_params <- Map.put(actor_params, "keys", keys_for_account()),
{:ok, %Actor{} = actor} <- Actors.create_actor(actor_params) do
conn
|> put_status(:created)
|> put_resp_header("location", actor_path(conn, :show, actor.preferred_username))
|> render("show_basic.json", actor: actor)
end
end
defp keys_for_account() do
key = :public_key.generate_key({:rsa, 2048, 65_537})
entry = :public_key.pem_entry_encode(:RSAPrivateKey, key)
[entry]
|> :public_key.pem_encode()
|> String.trim_trailing()
end
def show(conn, %{"name" => name}) do
with %Actor{} = actor <- Actors.get_actor_by_name_with_everything(name) do
render(conn, "show.json", actor: actor)
else
nil ->
send_resp(conn, :not_found, "")
end
end
@email_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
def search(conn, %{"name" => name}) do
# find already saved accounts
case Actors.search(name) do
{:ok, actors} ->
render(conn, "index.json", actors: actors)
{:error, err} ->
json(conn, err)
end
end
def update(conn, %{"name" => name, "actor" => actor_params}) do
actor = Actors.get_local_actor_by_name(name)
with {:ok, %Actor{} = actor} <- Actors.update_actor(actor, actor_params) do
render(conn, "show_basic.json", actor: actor)
end
end
# def delete(conn, %{"id" => id_str}) do
# {id, _} = Integer.parse(id_str)
# if Guardian.Plug.current_resource(conn).actor.id == id do
# actor = Actors.get_actor!(id)
# with {:ok, %Actor{}} <- Actors.delete_actor(actor) do
# send_resp(conn, :no_content, "")
# end
# else
# send_resp(conn, 401, "")
# end
# end
end

View File

@ -1,78 +0,0 @@
defmodule MobilizonWeb.AddressController do
@moduledoc """
A controller for addresses
"""
use MobilizonWeb, :controller
alias Mobilizon.Addresses
alias Mobilizon.Addresses.Address
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
addresses = Addresses.list_addresses()
render(conn, "index.json", addresses: addresses)
end
def create(conn, %{"address" => address_params}) do
with {:ok, geom} <- Addresses.process_geom(address_params["geom"]) do
address_params = %{address_params | "geom" => geom}
with {:ok, %Address{} = address} <- Addresses.create_address(address_params) do
conn
|> put_status(:created)
|> put_resp_header("location", address_path(conn, :show, address))
|> render("show.json", address: address)
end
end
end
def process_geom(%{"type" => type, "data" => data}) do
import Logger
Logger.debug("Process geom")
Logger.debug(inspect(data))
Logger.debug(inspect(type))
types = [:point]
unless is_atom(type) do
type = String.to_existing_atom(type)
end
case type do
:point ->
%Geo.Point{coordinates: {data["latitude"], data["longitude"]}, srid: 4326}
nil ->
nil
end
end
def process_geom(nil) do
nil
end
def show(conn, %{"id" => id}) do
address = Addresses.get_address!(id)
render(conn, "show.json", address: address)
end
def update(conn, %{"id" => id, "address" => address_params}) do
with {:ok, geom} <- Addresses.process_geom(address_params["geom"]) do
address = Addresses.get_address!(id)
address_params = %{address_params | "geom" => geom}
with {:ok, %Address{} = address} <- Addresses.update_address(address, address_params) do
render(conn, "show.json", address: address)
end
end
end
def delete(conn, %{"id" => id}) do
address = Addresses.get_address!(id)
with {:ok, %Address{}} <- Addresses.delete_address(address) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,48 +0,0 @@
defmodule MobilizonWeb.BotController do
use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Actors.{Bot, Actor}
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
bots = Actors.list_bots()
render(conn, "index.json", bots: bots)
end
def create(conn, %{"bot" => bot_params}) do
with user <- Guardian.Plug.current_resource(conn),
bot_params <- Map.put(bot_params, "user_id", user.id),
%Actor{} = actor <-
Actors.register_bot_account(%{name: bot_params["name"], summary: bot_params["summary"]}),
bot_params <- Map.put(bot_params, "actor_id", actor.id),
{:ok, %Bot{} = bot} <- Actors.create_bot(bot_params) do
conn
|> put_status(:created)
|> put_resp_header("location", bot_path(conn, :show, bot))
|> render("show.json", bot: bot)
end
end
def show(conn, %{"id" => id}) do
bot = Actors.get_bot!(id)
render(conn, "show.json", bot: bot)
end
def update(conn, %{"id" => id, "bot" => bot_params}) do
bot = Actors.get_bot!(id)
with {:ok, %Bot{} = bot} <- Actors.update_bot(bot, bot_params) do
render(conn, "show.json", bot: bot)
end
end
def delete(conn, %{"id" => id}) do
bot = Actors.get_bot!(id)
with {:ok, %Bot{}} <- Actors.delete_bot(bot) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,46 +0,0 @@
defmodule MobilizonWeb.CategoryController do
@moduledoc """
Controller for Categories
"""
use MobilizonWeb, :controller
alias Mobilizon.Events
alias Mobilizon.Events.Category
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
categories = Events.list_categories()
render(conn, "index.json", categories: categories)
end
def create(conn, %{"category" => category_params}) do
with {:ok, %Category{} = category} <- Events.create_category(category_params) do
conn
|> put_status(:created)
|> put_resp_header("location", category_path(conn, :show, category))
|> render("show.json", category: category)
end
end
def show(conn, %{"id" => id}) do
category = Events.get_category!(id)
render(conn, "show.json", category: category)
end
def update(conn, %{"id" => id, "category" => category_params}) do
category = Events.get_category!(id)
with {:ok, %Category{} = category} <- Events.update_category(category, category_params) do
render(conn, "show.json", category: category)
end
end
def delete(conn, %{"id" => id}) do
category = Events.get_category!(id)
with {:ok, %Category{}} <- Events.delete_category(category) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,43 +0,0 @@
defmodule MobilizonWeb.CommentController do
use MobilizonWeb, :controller
alias Mobilizon.Events
alias Mobilizon.Events.Comment
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
comments = Events.list_comments()
render(conn, "index.json", comments: comments)
end
def create(conn, %{"comment" => comment_params}) do
with {:ok, %Comment{} = comment} <- Events.create_comment(comment_params) do
conn
|> put_status(:created)
|> put_resp_header("location", comment_path(conn, :show, comment))
|> render("show.json", comment: comment)
end
end
def show(conn, %{"uuid" => uuid}) do
comment = Events.get_comment_with_uuid!(uuid)
render(conn, "show.json", comment: comment)
end
def update(conn, %{"uuid" => uuid, "comment" => comment_params}) do
comment = Events.get_comment_with_uuid!(uuid)
with {:ok, %Comment{} = comment} <- Events.update_comment(comment, comment_params) do
render(conn, "show.json", comment: comment)
end
end
def delete(conn, %{"uuid" => uuid}) do
comment = Events.get_comment_with_uuid!(uuid)
with {:ok, %Comment{}} <- Events.delete_comment(comment) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,125 +0,0 @@
defmodule MobilizonWeb.EventController do
@moduledoc """
Controller for Events
"""
use MobilizonWeb, :controller
alias Mobilizon.Events
alias Mobilizon.Events.Event
alias Mobilizon.Export.ICalendar
require Logger
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
ip = "88.161.154.97"
Logger.debug(inspect(Geolix.lookup(ip), pretty: true))
with %{
city: %Geolix.Adapter.MMDB2.Result.City{
city: city,
country: country,
location: %Geolix.Adapter.MMDB2.Record.Location{
latitude: latitude,
longitude: longitude
}
}
} <- Geolix.lookup(ip) do
distance =
case city do
nil -> 500_000
_ -> 50_000
end
events = Events.find_close_events(longitude, latitude, distance)
render(
conn,
"index.json",
events: events,
coord: %{longitude: longitude, latitude: latitude, distance: distance},
city: city,
country: country
)
end
end
def index_all(conn, _params) do
events = Events.list_events()
render(conn, "index_all.json", events: events)
end
def create(conn, %{"event" => event_params}) do
event_params = process_event_address(event_params)
Logger.debug("creating event with")
Logger.debug(inspect(event_params))
with {:ok, %Event{} = event} <- Events.create_event(event_params) do
conn
|> put_status(:created)
|> put_resp_header("location", event_path(conn, :show, event.uuid))
|> render("show_simple.json", event: event)
end
end
defp process_event_address(event) do
cond do
Map.has_key?(event, "address_type") && event["address_type"] !== :physical ->
event
Map.has_key?(event, "physical_address") ->
address = event["physical_address"]
geom = MobilizonWeb.AddressController.process_geom(address["geom"])
address =
case geom do
nil ->
address
_ ->
%{address | "geom" => geom}
end
%{event | "physical_address" => address}
true ->
event
end
end
def search(conn, %{"name" => name}) do
events = Events.find_events_by_name(name)
render(conn, "index.json", events: events)
end
def show(conn, %{"uuid" => uuid}) do
case Events.get_event_full_by_uuid(uuid) do
nil ->
send_resp(conn, 404, "")
event ->
render(conn, "show.json", event: event)
end
end
def export_to_ics(conn, %{"uuid" => uuid}) do
event = uuid |> Events.get_event_full_by_uuid() |> ICalendar.export_event()
send_resp(conn, 200, event)
end
def update(conn, %{"uuid" => uuid, "event" => event_params}) do
event = Events.get_event_full_by_uuid(uuid)
with {:ok, %Event{} = event} <- Events.update_event(event, event_params) do
render(conn, "show_simple.json", event: event)
end
end
def delete(conn, %{"uuid" => uuid}) do
with event <- Events.get_event_by_uuid(uuid),
{:ok, %Event{}} <- Events.delete_event(event) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,52 +0,0 @@
# defmodule MobilizonWeb.EventRequestController do
# @moduledoc """
# Controller for Event requests
# """
# use MobilizonWeb, :controller
#
# alias Mobilizon.Events
# alias Mobilizon.Events.{Event, Request}
#
# action_fallback MobilizonWeb.FallbackController
#
# def index_for_user(conn, _params) do
# actor = Guardian.Plug.current_resource(conn).actor
# requests = Events.list_requests_for_actor(actor)
# render(conn, "index.json", requests: requests)
# end
#
# def create(conn, %{"request" => request_params}) do
# request_params = Map.put(request_params, "actor_id", Guardian.Plug.current_resource(conn).actor.id)
# with {:ok, %Request{} = request} <- Events.create_request(request_params) do
# conn
# |> put_status(:created)
# |> put_resp_header("location", event_request_path(conn, :show, request))
# |> render("show.json", request: request)
# end
# end
#
# def create_for_event(conn, %{"request" => request_params, "id" => event_id}) do
# request_params = Map.put(request_params, "event_id", event_id)
# create(conn, request_params)
# end
#
# def show(conn, %{"id" => id}) do
# request = Events.get_request!(id)
# render(conn, "show.json", request: request)
# end
#
# def update(conn, %{"id" => id, "request" => request_params}) do
# request = Events.get_request!(id)
#
# with {:ok, %Request{} = request} <- Events.update_request(request, request_params) do
# render(conn, "show.json", request: request)
# end
# end
#
# def delete(conn, %{"id" => id}) do
# request = Events.get_request!(id)
# with {:ok, %Request{}} <- Events.delete_request(request) do
# send_resp(conn, :no_content, "")
# end
# end
# end

View File

@ -1,43 +0,0 @@
defmodule MobilizonWeb.FollowerController do
use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Actors.Follower
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
followers = Actors.list_followers()
render(conn, "index.json", followers: followers)
end
def create(conn, %{"follower" => follower_params}) do
with {:ok, %Follower{} = follower} <- Actors.create_follower(follower_params) do
conn
|> put_status(:created)
|> put_resp_header("location", follower_path(conn, :show, follower))
|> render("show.json", follower: follower)
end
end
def show(conn, %{"id" => id}) do
follower = Actors.get_follower!(id)
render(conn, "show.json", follower: follower)
end
def update(conn, %{"id" => id, "follower" => follower_params}) do
follower = Actors.get_follower!(id)
with {:ok, %Follower{} = follower} <- Actors.update_follower(follower, follower_params) do
render(conn, "show.json", follower: follower)
end
end
def delete(conn, %{"id" => id}) do
follower = Actors.get_follower!(id)
with {:ok, %Follower{}} <- Actors.delete_follower(follower) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,53 +0,0 @@
defmodule MobilizonWeb.GroupController do
@moduledoc """
Controller for Groups
"""
use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Actors.{Actor, Member}
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
groups = Actors.list_groups()
render(conn, MobilizonWeb.ActorView, "index.json", actors: groups)
end
def create(conn, %{"group" => group_params}) do
with {:ok, %Actor{} = group} <- Actors.create_group(group_params),
{:ok, %Member{} = member} <-
Actors.create_member(%{
"parent_id" => group.id,
"actor_id" => Actors.get_local_actor_by_name(group_params["actor_admin"]).id,
"role" => 2
}) do
conn
|> put_status(:created)
|> put_resp_header("location", actor_path(conn, :show, group))
|> render(MobilizonWeb.ActorView, "actor_basic.json", actor: group)
end
end
def join(conn, %{"name" => group_name, "actor_name" => actor_name}) do
with %Actor{} = group <- Actors.get_group_by_name(group_name),
%Actor{} = actor <- Actors.get_local_actor_by_name(actor_name),
{:ok, %Member{} = member} <-
Actors.create_member(%{"parent_id" => group.id, "actor_id" => actor.id}) do
conn
|> put_status(:created)
|> render(MobilizonWeb.MemberView, "member.json", member: member)
else
nil ->
conn
|> put_status(:not_found)
|> render(MobilizonWeb.ErrorView, "not_found.json",
details: "group or actor doesn't exist"
)
err ->
require Logger
Logger.debug(inspect(err))
end
end
end

View File

@ -1,6 +0,0 @@
defmodule MobilizonWeb.InboxesController do
use MobilizonWeb, :controller
def create(conn) do
end
end

View File

@ -1,4 +1,4 @@
defmodule MobilizonWeb.NodeinfoController do
defmodule MobilizonWeb.NodeInfoController do
use MobilizonWeb, :controller
alias MobilizonWeb
@ -11,7 +11,7 @@ defmodule MobilizonWeb.NodeinfoController do
links: [
%{
rel: "http://nodeinfo.diaspora.software/ns/schema/2.0",
href: MobilizonWeb.Router.Helpers.nodeinfo_url(MobilizonWeb.Endpoint, :nodeinfo, "2.0")
href: MobilizonWeb.Router.Helpers.node_info_url(MobilizonWeb.Endpoint, :nodeinfo, "2.0")
}
]
}

View File

@ -1,10 +0,0 @@
defmodule MobilizonWeb.OutboxesController do
use MobilizonWeb, :controller
def show(conn) do
actor = Guardian.Plug.current_resource(conn).actor
events = actor.events
render(conn, "index.json", events: events)
end
end

View File

@ -1,18 +0,0 @@
defmodule MobilizonWeb.ParticipantController do
@moduledoc """
Controller for participants to an event
"""
use MobilizonWeb, :controller
alias Mobilizon.Events
def join(conn, %{"uuid" => uuid}) do
with event <- Events.get_event_by_uuid(uuid),
%{actor: actor} <- Guardian.Plug.current_resource(conn) do
participant =
Events.create_participant(%{"event_id" => event.id, "actor_id" => actor.id, "role" => 1})
render(conn, "participant.json", %{participant: participant})
end
end
end

View File

@ -1,23 +0,0 @@
defmodule MobilizonWeb.SearchController do
@moduledoc """
Controller for Search
"""
use MobilizonWeb, :controller
alias Mobilizon.Events
alias Mobilizon.Actors
action_fallback(MobilizonWeb.FallbackController)
def search(conn, %{"name" => name}) do
events = Events.find_events_by_name(name)
# find already saved accounts
case Actors.search(name) do
{:ok, actors} ->
render(conn, "search.json", events: events, actors: actors)
{:error, err} ->
json(conn, err)
end
end
end

View File

@ -1,56 +0,0 @@
defmodule MobilizonWeb.SessionController do
@moduledoc """
Controller for (event) Sessions
"""
use MobilizonWeb, :controller
alias Mobilizon.Events
alias Mobilizon.Events.Session
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
sessions = Events.list_sessions()
render(conn, "index.json", sessions: sessions)
end
def create(conn, %{"session" => session_params}) do
with {:ok, %Session{} = session} <- Events.create_session(session_params) do
conn
|> put_status(:created)
|> put_resp_header("location", session_path(conn, :show, session))
|> render("show.json", session: session)
end
end
def show(conn, %{"id" => id}) do
session = Events.get_session!(id)
render(conn, "show.json", session: session)
end
def show_sessions_for_event(conn, %{"uuid" => event_uuid}) do
sessions = Events.list_sessions_for_event(event_uuid)
render(conn, "index.json", sessions: sessions)
end
def show_sessions_for_track(conn, %{"id" => track}) do
sessions = Events.list_sessions_for_track(track)
render(conn, "index.json", sessions: sessions)
end
def update(conn, %{"id" => id, "session" => session_params}) do
session = Events.get_session!(id)
with {:ok, %Session{} = session} <- Events.update_session(session, session_params) do
render(conn, "show.json", session: session)
end
end
def delete(conn, %{"id" => id}) do
session = Events.get_session!(id)
with {:ok, %Session{}} <- Events.delete_session(session) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,46 +0,0 @@
defmodule MobilizonWeb.TagController do
@moduledoc """
Controller for Tags
"""
use MobilizonWeb, :controller
alias Mobilizon.Events
alias Mobilizon.Events.Tag
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
tags = Events.list_tags()
render(conn, "index.json", tags: tags)
end
def create(conn, %{"tag" => tag_params}) do
with {:ok, %Tag{} = tag} <- Events.create_tag(tag_params) do
conn
|> put_status(:created)
|> put_resp_header("location", tag_path(conn, :show, tag))
|> render("show.json", tag: tag)
end
end
def show(conn, %{"id" => id}) do
tag = Events.get_tag!(id)
render(conn, "show.json", tag: tag)
end
def update(conn, %{"id" => id, "tag" => tag_params}) do
tag = Events.get_tag!(id)
with {:ok, %Tag{} = tag} <- Events.update_tag(tag, tag_params) do
render(conn, "show.json", tag: tag)
end
end
def delete(conn, %{"id" => id}) do
tag = Events.get_tag!(id)
with {:ok, %Tag{}} <- Events.delete_tag(tag) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,46 +0,0 @@
defmodule MobilizonWeb.TrackController do
@moduledoc """
Controller for Tracks
"""
use MobilizonWeb, :controller
alias Mobilizon.Events
alias Mobilizon.Events.Track
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
tracks = Events.list_tracks()
render(conn, "index.json", tracks: tracks)
end
def create(conn, %{"track" => track_params}) do
with {:ok, %Track{} = track} <- Events.create_track(track_params) do
conn
|> put_status(:created)
|> put_resp_header("location", track_path(conn, :show, track))
|> render("show.json", track: track)
end
end
def show(conn, %{"id" => id}) do
track = Events.get_track!(id)
render(conn, "show.json", track: track)
end
def update(conn, %{"id" => id, "track" => track_params}) do
track = Events.get_track!(id)
with {:ok, %Track{} = track} <- Events.update_track(track, track_params) do
render(conn, "show.json", track: track)
end
end
def delete(conn, %{"id" => id}) do
track = Events.get_track!(id)
with {:ok, %Track{}} <- Events.delete_track(track) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,148 +0,0 @@
defmodule MobilizonWeb.UserController do
@moduledoc """
Controller for Users
"""
use MobilizonWeb, :controller
alias Mobilizon.Actors
alias Mobilizon.Actors.User
alias Mobilizon.Repo
alias Mobilizon.Actors.Service.{Activation, ResetPassword}
action_fallback(MobilizonWeb.FallbackController)
def index(conn, _params) do
users = Actors.list_users_with_actors()
render(conn, "index.json", users: users)
end
def register(conn, %{"username" => username, "email" => email, "password" => password}) do
with {:ok, %User{} = user} <-
Actors.register(%{email: email, password: password, username: username}) do
Activation.send_confirmation_email(user, "locale")
conn
|> put_status(:created)
|> 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} = MobilizonWeb.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
@time_before_resend 3600
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, seconds: @time_before_resend),
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. Please wait a while before you can ask for resending 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, nil} ->
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} = MobilizonWeb.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(MobilizonWeb.ChangesetView, "error.json", changeset: changeset)
end
end
def show_current_actor(conn, _params) do
user =
conn
|> Guardian.Plug.current_resource()
|> Repo.preload(:actors)
render(conn, "show_simple.json", user: user)
end
# defp handle_changeset_errors(errors) do
# errors
# |> Enum.map(fn {field, detail} ->
# "#{field} " <> render_detail(detail)
# end)
# |> Enum.join()
# end
# defp render_detail({message, values}) do
# Enum.reduce(values, message, fn {k, v}, acc ->
# String.replace(acc, "%{#{k}}", to_string(v))
# end)
# end
# defp render_detail(message) do
# message
# end
def update(conn, %{"id" => id, "user" => user_params}) do
user = Actors.get_user!(id)
with {:ok, %User{} = user} <- Actors.update_user(user, user_params) do
render(conn, "show.json", user: user)
end
end
def delete(conn, %{"id" => id}) do
user = Actors.get_user!(id)
with {:ok, %User{}} <- Actors.delete_user(user) do
send_resp(conn, :no_content, "")
end
end
end

View File

@ -1,41 +0,0 @@
defmodule MobilizonWeb.UserSessionController do
@moduledoc """
Controller for user sessions
"""
use MobilizonWeb, :controller
alias Mobilizon.Actors.User
alias Mobilizon.Actors
def sign_in(conn, %{"email" => email, "password" => password}) do
with {:ok, %User{} = user} <- Actors.find_by_email(email),
{:ok, %User{} = _user} <- User.is_confirmed(user),
{:ok, token, _claims} <- Actors.authenticate(%{user: user, password: password}) do
# Render the token
render(conn, "token.json", %{token: token, user: user})
else
{:error, :not_found} ->
conn
|> put_status(401)
|> 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
def sign_out(conn, _params) do
conn
|> MobilizonWeb.Guardian.Plug.sign_out()
|> send_resp(204, "")
end
end

View File

@ -18,4 +18,8 @@ defmodule MobilizonWeb.WebFingerController do
_e -> send_resp(conn, 404, "Couldn't find user")
end
end
def webfinger(conn, _) do
send_resp(conn, 400, "No query provided")
end
end

View File

@ -10,6 +10,14 @@ defmodule MobilizonWeb.Endpoint do
#
# You should set gzip to true if you are running phoenix.digest
# when deploying your static files in production.
plug(
Plug.Static,
at: "/uploads",
from: "./uploads",
gzip: false
)
plug(
Plug.Static,
at: "/",

View File

@ -0,0 +1,26 @@
defmodule MobilizonWeb.Resolvers.Actor do
alias Mobilizon.Actors.Actor, as: ActorSchema
alias Mobilizon.Actors.User
alias Mobilizon.Actors
def find_actor(_parent, %{preferred_username: name}, _resolution) do
case Actors.get_actor_by_name_with_everything(name) do
nil ->
{:error, "Actor with name #{name} not found"}
actor ->
{:ok, actor}
end
end
@doc """
Returns the current actor for the currently logged-in user
"""
def get_current_actor(_parent, _args, %{context: %{current_user: user}}) do
{:ok, Actors.get_actor_for_user(user)}
end
def get_current_actor(_parent, _args, _resolution) do
{:error, "You need to be logged-in to view current actor"}
end
end

View File

@ -0,0 +1,37 @@
defmodule MobilizonWeb.Resolvers.Category do
require Logger
def list_categories(_parent, _args, _resolution) do
categories =
Mobilizon.Events.list_categories()
|> Enum.map(fn category ->
urls = MobilizonWeb.Uploaders.Category.urls({category.picture, category})
Map.put(category, :picture, %{url: urls.original, url_thumbnail: urls.thumb})
end)
{:ok, categories}
end
def create_category(_parent, %{title: title, picture: picture, description: description}, %{
context: %{current_user: user}
}) do
with {:ok, category} <-
Mobilizon.Events.create_category(%{
title: title,
description: description,
picture: picture
}),
urls <- MobilizonWeb.Uploaders.Category.urls({category.picture, category}) do
Logger.info("Created category " <> title)
{:ok, Map.put(category, :picture, %{url: urls.original, url_thumbnail: urls.thumb})}
else
{:error, %Ecto.Changeset{errors: errors} = _changeset} ->
# This is pretty ridiculous for changeset to error
errors =
Enum.into(errors, %{})
|> Enum.map(fn {key, {value, _}} -> Atom.to_string(key) <> ": " <> value end)
{:error, errors}
end
end
end

View File

@ -0,0 +1,54 @@
defmodule MobilizonWeb.Resolvers.Event do
def list_events(_parent, _args, _resolution) do
{:ok, Mobilizon.Events.list_events()}
end
def find_event(_parent, %{uuid: uuid}, _resolution) do
case Mobilizon.Events.get_event_full_by_uuid(uuid) do
nil ->
{:error, "Event with UUID #{uuid} not found"}
event ->
{:ok, event}
end
end
def list_participants_for_event(_parent, %{uuid: uuid}, _resolution) do
{:ok, Mobilizon.Events.list_participants_for_event(uuid)}
end
@doc """
Search events by title
"""
def search_events(_parent, %{search: search, page: page, limit: limit}, _resolution) do
{:ok, Mobilizon.Events.find_events_by_name(search, page, limit)}
end
@doc """
Search events and actors by title
"""
def search_events_and_actors(_parent, %{search: search, page: page, limit: limit}, _resolution) do
found =
Mobilizon.Events.find_events_by_name(search, page, limit) ++
Mobilizon.Actors.find_actors_by_username_or_name(search, page, limit)
require Logger
Logger.debug(inspect(found))
{:ok, found}
end
@doc """
List participants for event (through an event request)
"""
def list_participants_for_event(%{uuid: uuid}, _args, _resolution) do
{:ok, Mobilizon.Events.list_participants_for_event(uuid)}
end
def create_event(_parent, args, %{context: %{current_user: user}}) do
Mobilizon.Events.create_event(args)
end
def create_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to create events"}
end
end

View File

@ -0,0 +1,2 @@
defmodule MobilizonWeb.Resolvers.Upload do
end

View File

@ -0,0 +1,118 @@
defmodule MobilizonWeb.Resolvers.User do
alias Mobilizon.Actors.{User, Actor}
alias Mobilizon.Actors
@doc """
Find an user by it's ID
"""
def find_user(_parent, %{id: id}, _resolution) do
Actors.get_user_with_actor(id)
end
@doc """
Return current logged-in user
"""
def get_current_user(_parent, _args, %{context: %{current_user: user}}) do
{:ok, user}
end
def get_current_user(_parent, _args, _resolution) do
{:error, "You need to be logged-in to view current user"}
end
@desc """
Login an user. Returns a token and the user
"""
def login_user(_parent, %{email: email, password: password}, _resolution) do
with {:ok, %User{} = user} <- Actors.get_user_by_email(email, true),
{:ok, token, _} <- Actors.authenticate(%{user: user, password: password}),
%Actor{} = actor <- Actors.get_actor_for_user(user) do
{:ok, %{token: token, user: user, actor: actor}}
else
{:error, :user_not_found} ->
{:error, "User with email not found"}
{:error, :unauthorized} ->
{:error, "Impossible to authenticate"}
end
end
@desc """
Register an user :
- create the user
- create the actor
- set the user's default_actor to the newly created actor
- send a validation email to the user
"""
@spec create_user_actor(any(), map(), any()) :: tuple()
def create_user_actor(_parent, args, _resolution) do
with {:ok, %Actor{user: user} = actor} <- Actors.register(args) do
Mobilizon.Actors.Service.Activation.send_confirmation_email(user)
{:ok, actor}
end
end
@doc """
Validate an user, get it's actor and a token
"""
def validate_user(_parent, %{token: token}, _resolution) do
with {:ok, %User{} = user} <-
Mobilizon.Actors.Service.Activation.check_confirmation_token(token),
%Actor{} = actor <- Actors.get_actor_for_user(user),
{:ok, token, _} <- MobilizonWeb.Guardian.encode_and_sign(user) do
{:ok, %{token: token, user: user, actor: actor}}
end
end
@doc """
Send the confirmation email again.
We only do this to accounts unconfirmed
"""
def resend_confirmation_email(_parent, %{email: email, locale: locale}, _resolution) do
with {:ok, user} <- Actors.get_user_by_email(email, false),
{:ok, email} <- Mobilizon.Actors.Service.Activation.resend_confirmation_email(user, locale) do
{:ok, email}
else
{:error, :user_not_found} ->
{:error, "No user to validate with this email was found"}
{:error, :email_too_soon} ->
{:error, "You requested again a confirmation email too soon"}
end
end
@doc """
Send an email to reset the password from an user
"""
def send_reset_password(_parent, %{email: email, locale: locale}, _resolution) do
with {:ok, user} <- Actors.get_user_by_email(email, false),
{:ok, email} <- Mobilizon.Actors.Service.ResetPassword.send_password_reset_email(user, locale) do
{:ok, email}
else
{:error, :user_not_found} ->
{:error, "No user to validate with this email was found"}
{:error, :email_too_soon} ->
{:error, "You requested again a confirmation email too soon"}
end
end
@doc """
Reset the password from an user
"""
def reset_password(_parent, %{password: password, token: token}, _resolution) do
with {:ok, %User{} = user} <-
Mobilizon.Actors.Service.ResetPassword.check_reset_password_token(password, token),
%Actor{} = actor <- Actors.get_actor_for_user(user),
{:ok, token, _} <- MobilizonWeb.Guardian.encode_and_sign(user) do
{:ok, %{token: token, user: user, actor: actor}}
end
end
@desc "Change an user default actor"
def change_default_actor(_parent, %{preferred_username: username}, %{
context: %{current_user: user}
}) do
with %Actor{id: id} <- Actors.get_local_actor_by_name(username) do
Actors.update_user(user, %{default_actor_id: id})
end
end
end

View File

@ -4,8 +4,9 @@ defmodule MobilizonWeb.Router do
"""
use MobilizonWeb, :router
pipeline :api do
pipeline :graphql do
plug(:accepts, ["json"])
plug(MobilizonWeb.AuthPipeline)
end
pipeline :well_known do
@ -17,11 +18,6 @@ defmodule MobilizonWeb.Router do
plug(MobilizonWeb.HTTPSignaturePlug)
end
pipeline :api_auth do
plug(:accepts, ["json"])
plug(MobilizonWeb.AuthPipeline)
end
pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
@ -34,90 +30,21 @@ defmodule MobilizonWeb.Router do
plug(:accepts, ["html", "application/json"])
end
scope "/api", MobilizonWeb do
pipe_through(:api)
scope "/api" do
pipe_through(:graphql)
scope "/v1" do
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)
get("/groups", GroupController, :index)
get("/events", EventController, :index)
get("/events/all", EventController, :index_all)
get("/events/search/:name", EventController, :search)
get("/events/:uuid/ics", EventController, :export_to_ics)
get("/events/:uuid/tracks", TrackController, :show_tracks_for_event)
get("/events/:uuid/sessions", SessionController, :show_sessions_for_event)
get("/events/:uuid", EventController, :show)
get("/comments/:uuid", CommentController, :show)
get("/bots/:id", BotController, :show)
get("/bots", BotController, :index)
get("/actors", ActorController, :index)
get("/actors/search/:name", ActorController, :search)
get("/actors/:name", ActorController, :show)
resources("/followers", FollowerController, except: [:new, :edit])
resources("/tags", TagController, only: [:index, :show])
resources("/categories", CategoryController, only: [:index, :show])
resources("/sessions", SessionController, only: [:index, :show])
resources("/tracks", TrackController, only: [:index, :show])
resources("/addresses", AddressController, only: [:index, :show])
get("/search/:name", SearchController, :search)
scope "/nodeinfo" do
pipe_through(:nodeinfo)
get("/:version", NodeinfoController, :nodeinfo)
end
end
forward("/", Absinthe.Plug, schema: MobilizonWeb.Schema)
end
# Authentificated API
scope "/api", MobilizonWeb do
pipe_through(:api_auth)
scope "/v1" do
get("/user", UserController, :show_current_actor)
post("/sign-out", UserSessionController, :sign_out)
resources("/users", UserController, except: [:new, :edit, :show])
post("/actors", ActorController, :create)
patch("/actors/:name", ActorController, :update)
post("/events", EventController, :create)
patch("/events/:uuid", EventController, :update)
put("/events/:uuid", EventController, :update)
delete("/events/:uuid", EventController, :delete)
post("/events/:uuid/join", ParticipantController, :join)
post("/comments", CommentController, :create)
patch("/comments/:uuid", CommentController, :update)
put("/comments/:uuid", CommentController, :update)
delete("/comments/:uuid", CommentController, :delete)
resources("/bots", BotController, except: [:new, :edit, :show, :index])
post("/groups", GroupController, :create)
post("/groups/:name/join", GroupController, :join)
resources("/members", MemberController)
resources("/sessions", SessionController, except: [:index, :show])
resources("/tracks", TrackController, except: [:index, :show])
get("/tracks/:id/sessions", SessionController, :show_sessions_for_track)
resources("/categories", CategoryController)
resources("/tags", TagController)
resources("/addresses", AddressController, except: [:index, :show])
end
end
forward("/graphiql", Absinthe.Plug.GraphiQL, schema: MobilizonWeb.Schema)
scope "/.well-known", MobilizonWeb do
pipe_through(:well_known)
get("/host-meta", WebFingerController, :host_meta)
get("/webfinger", WebFingerController, :webfinger)
get("/nodeinfo", NodeinfoController, :schemas)
get("/nodeinfo", NodeInfoController, :schemas)
get("/nodeinfo/:version", NodeInfoController, :nodeinfo)
end
scope "/", MobilizonWeb do
@ -141,6 +68,7 @@ defmodule MobilizonWeb.Router do
scope "/", MobilizonWeb do
pipe_through(:browser)
forward("/uploads", UploadPlug)
get("/*path", PageController, :index)
end
end

318
lib/mobilizon_web/schema.ex Normal file
View File

@ -0,0 +1,318 @@
defmodule MobilizonWeb.Schema do
use Absinthe.Schema
import Absinthe.Resolution.Helpers, only: [dataloader: 1]
alias Mobilizon.{Actors, Events}
alias Mobilizon.Actors.Actor
alias Mobilizon.Events.Event
import_types(MobilizonWeb.Schema.Custom.UUID)
import_types(Absinthe.Type.Custom)
import_types(Absinthe.Plug.Types)
# import_types(MobilizonWeb.Schema.EventTypes)
# import_types(MobilizonWeb.Schema.ActorTypes)
alias MobilizonWeb.Resolvers
@desc "An ActivityPub actor"
object :actor do
field(:url, :string)
field(:outbox_url, :string)
field(:inbox_url, :string)
field(:following_url, :string)
field(:followers_url, :string)
field(:shared_inbox_url, :string)
field(:type, :actor_type)
field(:name, :string)
field(:domain, :string)
field(:summary, :string)
field(:preferred_username, :string)
field(:keys, :string)
field(:manually_approves_followers, :boolean)
field(:suspended, :boolean)
field(:avatar_url, :string)
field(:banner_url, :string)
# field(:followers, list_of(:follower))
field :organized_events, list_of(:event) do
resolve(dataloader(Events))
end
# field(:memberships, list_of(:member))
field(:user, :user)
end
enum :actor_type do
value(:Person)
value(:Application)
value(:Group)
value(:Organization)
value(:Service)
end
@desc "A local user of Mobilizon"
object :user do
field(:id, non_null(:id))
field(:email, non_null(:string))
# , resolve: dataloader(:actors))
field(:actors, non_null(list_of(:actor)))
field(:default_actor_id, non_null(:integer))
field(:confirmed_at, :datetime)
field(:confirmation_sent_at, :datetime)
field(:confirmation_token, :string)
field(:reset_password_sent_at, :datetime)
field(:reset_password_token, :string)
end
@desc "A JWT and the associated user ID"
object :login do
field(:token, non_null(:string))
field(:user, non_null(:user))
field(:actor, non_null(:actor))
end
@desc "An event"
object :event do
field(:uuid, :uuid)
field(:url, :string)
field(:local, :boolean)
field(:title, :string)
field(:description, :string)
field(:begins_on, :datetime)
field(:ends_on, :datetime)
field(:state, :integer)
field(:status, :integer)
field(:public, :boolean)
field(:thumbnail, :string)
field(:large_image, :string)
field(:publish_at, :datetime)
field(:address_type, :address_type)
field(:online_address, :string)
field(:phone, :string)
field :organizer_actor, :actor do
resolve(dataloader(Actors))
end
field(:attributed_to, :actor)
# field(:tags, list_of(:tag))
field(:category, :category)
field(:participants, list_of(:participant),
resolve: &Resolvers.Event.list_participants_for_event/3
)
# field(:tracks, list_of(:track))
# field(:sessions, list_of(:session))
# field(:physical_address, :address)
field(:updated_at, :datetime)
field(:created_at, :datetime)
end
@desc "Represents a participant to an event"
object :participant do
# field(:event, :event, resolve: dataloader(Events))
# , resolve: dataloader(Actors)
field(:actor, :actor)
field(:role, :integer)
end
enum :address_type do
value(:physical)
value(:url)
value(:phone)
value(:other)
end
@desc "A category"
object :category do
field(:id, :id)
field(:description, :string)
field(:picture, :picture)
field(:title, :string)
field(:updated_at, :datetime)
field(:created_at, :datetime)
end
@desc "A picture"
object :picture do
field(:url, :string)
field(:url_thumbnail, :string)
end
@desc "A search result"
union :search_result do
types([:event, :actor])
resolve_type(fn
%Actor{}, _ ->
:actor
%Event{}, _ ->
:event
end)
end
def context(ctx) do
loader =
Dataloader.new()
|> Dataloader.add_source(Actors, Actors.data())
|> Dataloader.add_source(Events, Events.data())
Map.put(ctx, :loader, loader)
end
def plugins do
[Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end
query do
@desc "Get all events"
field :events, list_of(:event) do
resolve(&Resolvers.Event.list_events/3)
end
@desc "Search through events and actors"
field :search, list_of(:search_result) do
arg(:search, non_null(:string))
arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10)
resolve(&Resolvers.Event.search_events_and_actors/3)
end
@desc "Get an event by uuid"
field :event, :event do
arg(:uuid, non_null(:uuid))
resolve(&Resolvers.Event.find_event/3)
end
@desc "Get all participants for an event uuid"
field :participants, list_of(:participant) do
arg(:uuid, non_null(:uuid))
resolve(&Resolvers.Event.list_participants_for_event/3)
end
@desc "Get an user"
field :user, :user do
arg(:id, non_null(:id))
resolve(&Resolvers.User.find_user/3)
end
@desc "Get the current user"
field :logged_user, :user do
resolve(&Resolvers.User.get_current_user/3)
end
@desc "Get the current actor for the logged-in user"
field :logged_actor, :actor do
resolve(&Resolvers.Actor.get_current_actor/3)
end
@desc "Get an actor"
field :actor, :actor do
arg(:preferred_username, non_null(:string))
resolve(&Resolvers.Actor.find_actor/3)
end
@doc """
Get the list of categories
"""
field :categories, list_of(:category) do
resolve(&Resolvers.Category.list_categories/3)
end
end
mutation do
@desc "Create an event"
field :create_event, type: :event do
arg(:title, non_null(:string))
arg(:description, non_null(:string))
arg(:begins_on, non_null(:datetime))
arg(:ends_on, :datetime)
arg(:state, :integer)
arg(:status, :integer)
arg(:public, :boolean)
arg(:thumbnail, :string)
arg(:large_image, :string)
arg(:publish_at, :datetime)
arg(:address_type, non_null(:address_type))
arg(:online_address, :string)
arg(:phone, :string)
arg(:organizer_actor_id, non_null(:integer))
arg(:category_id, non_null(:integer))
resolve(&Resolvers.Event.create_event/3)
end
@doc """
Create a category with a title, description and picture
"""
field :create_category, type: :category do
arg(:title, non_null(:string))
arg(:description, non_null(:string))
arg(:picture, non_null(:upload))
resolve(&Resolvers.Category.create_category/3)
end
@desc "Create an user (returns an actor)"
field :create_user, type: :actor do
arg(:email, non_null(:string))
arg(:password, non_null(:string))
arg(:username, non_null(:string))
resolve(&Resolvers.User.create_user_actor/3)
end
@desc "Validate an user after registration"
field :validate_user, type: :login do
arg(:token, non_null(:string))
resolve(&Resolvers.User.validate_user/3)
end
@desc "Resend registration confirmation token"
field :resend_confirmation_email, type: :string do
arg(:email, non_null(:string))
arg(:locale, :string, default_value: "en")
resolve(&Resolvers.User.resend_confirmation_email/3)
end
@doc """
Send a link through email to reset user password
"""
field :send_reset_password, type: :string do
arg(:email, non_null(:string))
arg(:locale, :string, default_value: "en")
resolve(&Resolvers.User.send_reset_password/3)
end
@doc """
Reset user password
"""
field :reset_password, type: :login do
arg(:token, non_null(:string))
arg(:password, non_null(:string))
arg(:locale, :string, default_value: "en")
resolve(&Resolvers.User.reset_password/3)
end
@desc "Login an user"
field :login, :login do
arg(:email, non_null(:string))
arg(:password, non_null(:string))
resolve(&Resolvers.User.login_user/3)
end
@desc "Change default actor for user"
field :change_default_actor, :user do
arg(:preferred_username, non_null(:string))
resolve(&Resolvers.User.change_default_actor/3)
end
@desc "Upload a picture"
field :upload_picture, :picture do
arg(:file, non_null(:upload))
resolve(&Resolvers.Upload.upload_picture/3)
end
end
end

View File

@ -0,0 +1,36 @@
defmodule MobilizonWeb.Schema.Custom.UUID do
@moduledoc """
The UUID4 scalar type allows UUID compliant strings to be passed in and out.
Requires `{ :ecto, ">= 0.0.0" }` package: https://github.com/elixir-ecto/ecto
"""
use Absinthe.Schema.Notation
alias Ecto.UUID
scalar :uuid, name: "UUID" do
description("""
The `UUID` scalar type represents UUID4 compliant string data, represented as UTF-8
character sequences. The UUID4 type is most often used to represent unique
human-readable ID strings.
""")
serialize(&encode/1)
parse(&decode/1)
end
@spec decode(Absinthe.Blueprint.Input.String.t()) :: {:ok, term()} | :error
@spec decode(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil}
defp decode(%Absinthe.Blueprint.Input.String{value: value}) do
UUID.cast(value)
end
defp decode(%Absinthe.Blueprint.Input.Null{}) do
{:ok, nil}
end
defp decode(_) do
:error
end
defp encode(value), do: value
end

View File

@ -0,0 +1,15 @@
defmodule MobilizonWeb.UploadPlug do
use Plug.Builder
plug(Plug.Static,
at: "/",
from: {:mobilizon, "./uploads"}
)
# only: ~w(images robots.txt)
plug(:not_found)
def not_found(conn, _) do
send_resp(conn, 404, "not found")
end
end

View File

@ -0,0 +1,50 @@
defmodule MobilizonWeb.Uploaders.Avatar do
use Arc.Definition
# Include ecto support (requires package arc_ecto installed):
# use Arc.Ecto.Definition
@versions [:original]
# To add a thumbnail version:
# @versions [:original, :thumb]
# Override the bucket on a per definition basis:
# def bucket do
# :custom_bucket_name
# end
# Whitelist file extensions:
# def validate({file, _}) do
# ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name))
# end
# Define a thumbnail transformation:
# def transform(:thumb, _) do
# {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
# end
# Override the persisted filenames:
# def filename(version, _) do
# version
# end
# Override the storage directory:
# def storage_dir(version, {file, scope}) do
# "uploads/user/avatars/#{scope.id}"
# end
# Provide a default URL if there hasn't been a file uploaded
# def default_url(version, scope) do
# "/images/avatars/default_#{version}.png"
# end
# Specify custom headers for s3 objects
# Available options are [:cache_control, :content_disposition,
# :content_encoding, :content_length, :content_type,
# :expect, :expires, :storage_class, :website_redirect_location]
#
# def s3_object_headers(version, {file, scope}) do
# [content_type: MIME.from_path(file.file_name)]
# end
end

View File

@ -0,0 +1,49 @@
defmodule MobilizonWeb.Uploaders.Category do
use Arc.Definition
use Arc.Ecto.Definition
# To add a thumbnail version:
@versions [:original, :thumb]
@extension_whitelist ~w(.jpg .jpeg .gif .png)
# Override the bucket on a per definition basis:
# def bucket do
# :custom_bucket_name
# end
# Whitelist file extensions:
def validate({file, _}) do
file_extension = file.file_name |> Path.extname() |> String.downcase()
Enum.member?(@extension_whitelist, file_extension)
end
# Define a thumbnail transformation:
def transform(:thumb, _) do
{:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png", :png}
end
# Override the persisted filenames:
def filename(version, {file, %{title: title}}) do
"#{title}_#{version}"
end
# TODO : When we're sure creating a category is secured and made possible only for admins, use category name
# Override the storage directory:
def storage_dir(_, _) do
"uploads/categories/"
end
# Provide a default URL if there hasn't been a file uploaded
# def default_url(version, scope) do
# "/images/avatars/default_#{version}.png"
# end
# Specify custom headers for s3 objects
# Available options are [:cache_control, :content_disposition,
# :content_encoding, :content_length, :content_type,
# :expect, :expires, :storage_class, :website_redirect_location]
#
# def s3_object_headers(version, {file, scope}) do
# [content_type: MIME.from_path(file.file_name)]
# end
end

Some files were not shown because too many files have changed in this diff Show More