Merge branch 'graphql' into 'master'
Move API to GraphQL See merge request framasoft/mobilizon!19
This commit is contained in:
commit
a082b85ccb
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -23,3 +23,5 @@ priv/data/*
|
|||
!priv/data/.gitkeep
|
||||
.vscode/
|
||||
cover/
|
||||
uploads/*
|
||||
!uploads/.gitkeep
|
|
@ -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
|
||||
|
|
|
@ -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
5
docker/tests/Dockerfile
Normal 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/*
|
|
@ -1,7 +1,7 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
'extends': [
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'@vue/airbnb'
|
||||
]
|
||||
}
|
||||
'@vue/airbnb',
|
||||
],
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
}
|
||||
}
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
|
|
58
js/Makefile
Normal file
58
js/Makefile
Normal 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
38
js/get_union_json.js
Normal 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
2182
js/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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": [
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
export default {
|
||||
login(state, user) {
|
||||
state.user = user.user;
|
||||
},
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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')}`,
|
||||
};
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'RegisterAvatar'
|
||||
}
|
||||
name: 'RegisterAvatar',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
|
|
@ -31,8 +31,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import fetchStory from '@/api/eventFetch';
|
||||
|
||||
export default {
|
||||
name: 'ResendConfirmation',
|
||||
props: {
|
||||
|
|
|
@ -31,8 +31,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import fetchStory from '@/api/eventFetch';
|
||||
|
||||
export default {
|
||||
name: 'SendPasswordReset',
|
||||
props: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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--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--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 -->
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
3
js/src/constants.js
Normal 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
39
js/src/graphql/actor.js
Normal 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
16
js/src/graphql/auth.js
Normal 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,
|
||||
}
|
||||
},
|
||||
}
|
||||
`;
|
29
js/src/graphql/category.js
Normal file
29
js/src/graphql/category.js
Normal 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
109
js/src/graphql/event.js
Normal 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
|
||||
}
|
||||
}
|
||||
`;
|
1
js/src/graphql/fragmentTypes.json
Normal file
1
js/src/graphql/fragmentTypes.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"__schema":{"types":[{"possibleTypes":[{"name":"Event"},{"name":"Actor"}],"name":"SearchResult","kind":"UNION"}]}}
|
17
js/src/graphql/search.js
Normal file
17
js/src/graphql/search.js
Normal 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
10
js/src/graphql/upload.js
Normal 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
28
js/src/graphql/user.js
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
import en from './en';
|
||||
import fr from './fr';
|
||||
|
||||
export default {
|
||||
en, fr,
|
||||
};
|
30
js/src/i18n/locale/en_US/LC_MESSAGES/app.po
Normal file
30
js/src/i18n/locale/en_US/LC_MESSAGES/app.po
Normal 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"
|
30
js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po
Normal file
30
js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po
Normal 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"
|
30
js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po~
Normal file
30
js/src/i18n/locale/fr_FR/LC_MESSAGES/app.po~
Normal 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"
|
1
js/src/i18n/translations.json
Normal file
1
js/src/i18n/translations.json
Normal 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"}}
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
@ -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;
|
|
@ -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
135
js/src/vue-apollo.js
Normal 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);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
mocha: true
|
||||
mocha: true,
|
||||
},
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': 'off'
|
||||
}
|
||||
}
|
||||
'import/no-extraneous-dependencies': 'off',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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(%{
|
||||
|
|
|
@ -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,38 @@ 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 +508,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
27
lib/mobilizon/actors/service/tools.ex
Normal file
27
lib/mobilizon/actors/service/tools.ex
Normal 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
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -86,7 +86,6 @@ defmodule Mobilizon.Events.Event do
|
|||
|> validate_required([
|
||||
:title,
|
||||
:begins_on,
|
||||
:ends_on,
|
||||
:organizer_actor_id,
|
||||
:category_id,
|
||||
:url,
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
20
lib/mobilizon_web/context.ex
Normal file
20
lib/mobilizon_web/context.ex
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
defmodule MobilizonWeb.InboxesController do
|
||||
use MobilizonWeb, :controller
|
||||
|
||||
def create(conn) do
|
||||
end
|
||||
end
|
|
@ -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")
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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: "/",
|
||||
|
|
26
lib/mobilizon_web/resolvers/actor.ex
Normal file
26
lib/mobilizon_web/resolvers/actor.ex
Normal 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
|
37
lib/mobilizon_web/resolvers/category.ex
Normal file
37
lib/mobilizon_web/resolvers/category.ex
Normal 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
|
54
lib/mobilizon_web/resolvers/event.ex
Normal file
54
lib/mobilizon_web/resolvers/event.ex
Normal 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
|
2
lib/mobilizon_web/resolvers/upload.ex
Normal file
2
lib/mobilizon_web/resolvers/upload.ex
Normal file
|
@ -0,0 +1,2 @@
|
|||
defmodule MobilizonWeb.Resolvers.Upload do
|
||||
end
|
122
lib/mobilizon_web/resolvers/user.ex
Normal file
122
lib/mobilizon_web/resolvers/user.ex
Normal file
|
@ -0,0 +1,122 @@
|
|||
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
|
|
@ -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
318
lib/mobilizon_web/schema.ex
Normal 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
|
36
lib/mobilizon_web/schema/custom/UUID4.ex
Normal file
36
lib/mobilizon_web/schema/custom/UUID4.ex
Normal 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
|
15
lib/mobilizon_web/upload_plug.ex
Normal file
15
lib/mobilizon_web/upload_plug.ex
Normal 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
|
50
lib/mobilizon_web/uploaders/avatar.ex
Normal file
50
lib/mobilizon_web/uploaders/avatar.ex
Normal 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
|
49
lib/mobilizon_web/uploaders/category.ex
Normal file
49
lib/mobilizon_web/uploaders/category.ex
Normal 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
Loading…
Reference in a new issue