Merge branch 'analytics' into 'main'
Provide analytics on Front-end Closes #690 See merge request framasoft/mobilizon!1200
This commit is contained in:
commit
ca6db74a73
@ -110,7 +110,7 @@ deps:
|
||||
exunit:
|
||||
stage: test
|
||||
services:
|
||||
- name: postgis/postgis:13-3.1
|
||||
- name: postgis/postgis:14-3.2
|
||||
alias: postgres
|
||||
variables:
|
||||
MIX_ENV: test
|
||||
|
@ -345,6 +345,8 @@ config :mobilizon, :exports,
|
||||
Mobilizon.Service.Export.Participants.CSV
|
||||
]
|
||||
|
||||
config :mobilizon, :analytics, providers: []
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{config_env()}.exs"
|
||||
|
@ -1,15 +1,11 @@
|
||||
FROM elixir:latest
|
||||
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
||||
|
||||
ENV REFRESHED_AT=2021-12-15
|
||||
ENV REFRESHED_AT=2022-04-06
|
||||
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool python3-pip python3-setuptools
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash && apt-get install nodejs -yq
|
||||
RUN npm install -g yarn wait-on
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
RUN mix local.hex --force && mix local.rebar --force
|
||||
# Weasyprint 53 requires pango >= 1.44.0, which is not available in Stretch.
|
||||
# TODO: Remove the version requirement when elixir:latest is based on Bullseye
|
||||
# https://github.com/erlang/docker-erlang-otp/issues/362
|
||||
# https://github.com/Kozea/WeasyPrint/issues/1384
|
||||
RUN pip3 install -Iv weasyprint==52 pyexcel_ods3
|
||||
RUN pip3 install -Iv weasyprint pyexcel_ods3
|
||||
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/
|
||||
|
@ -8,7 +8,7 @@
|
||||
"test:unit": "LANG=en_US.UTF-8 LANGUAGE=en_US:en LC_ALL=en_US.UTF-8 TZ=UTC vue-cli-service test:unit",
|
||||
"test:e2e": "vue-cli-service test:e2e",
|
||||
"lint": "vue-cli-service lint",
|
||||
"build:assets": "vue-cli-service build",
|
||||
"build:assets": "vue-cli-service build --report",
|
||||
"build:pictures": "bash ./scripts/build/pictures.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -16,6 +16,8 @@
|
||||
"@absinthe/socket-apollo-link": "^0.2.1",
|
||||
"@apollo/client": "^3.3.16",
|
||||
"@mdi/font": "^6.1.95",
|
||||
"@sentry/tracing": "^6.16.1",
|
||||
"@sentry/vue": "^6.16.1",
|
||||
"@tailwindcss/line-clamp": "^0.3.0",
|
||||
"@tiptap/core": "^2.0.0-beta.41",
|
||||
"@tiptap/extension-blockquote": "^2.0.0-beta.25",
|
||||
@ -69,7 +71,9 @@
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-i18n": "^8.14.0",
|
||||
"vue-matomo": "^4.1.0",
|
||||
"vue-meta": "^2.3.1",
|
||||
"vue-plausible": "^1.3.1",
|
||||
"vue-property-decorator": "^9.0.0",
|
||||
"vue-router": "^3.1.6",
|
||||
"vue-scrollto": "^2.17.1",
|
||||
|
@ -203,6 +203,16 @@ export default class App extends Vue {
|
||||
this.interval = undefined;
|
||||
}
|
||||
|
||||
@Watch("config")
|
||||
async initializeStatistics(config: IConfig) {
|
||||
if (config) {
|
||||
const { statistics } = (await import("./services/statistics")) as {
|
||||
statistics: (config: IConfig, environment: Record<string, any>) => void;
|
||||
};
|
||||
statistics(config, { router: this.$router, version: config.version });
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("$route", { immediate: true })
|
||||
updateAnnouncement(route: Route): void {
|
||||
const pageTitle = this.extractPageTitleFromRoute(route);
|
||||
|
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-80 bg-white rounded-lg shadow-md flex space-x-4 items-center"
|
||||
:class="{ 'flex-col p-4 sm:p-8 pb-10': !inline }"
|
||||
class="bg-white rounded-lg flex space-x-4 items-center"
|
||||
:class="{ 'flex-col p-4 shadow-md sm:p-8 pb-10 w-80': !inline }"
|
||||
>
|
||||
<div>
|
||||
<figure class="w-12 h-12" v-if="actor.avatar">
|
||||
@ -20,8 +20,10 @@
|
||||
class="ltr:-mr-0.5 rtl:-ml-0.5"
|
||||
/>
|
||||
</div>
|
||||
<div :class="{ 'text-center': !inline }">
|
||||
<h5 class="text-xl font-medium violet-title tracking-tight text-gray-900">
|
||||
<div :class="{ 'text-center': !inline }" class="overflow-hidden w-full">
|
||||
<h5
|
||||
class="text-xl font-medium violet-title tracking-tight text-gray-900 whitespace-pre-line line-clamp-2"
|
||||
>
|
||||
{{ displayName(actor) }}
|
||||
</h5>
|
||||
<p class="text-gray-500 truncate" v-if="actor.name">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="ellipsis"
|
||||
class="truncate"
|
||||
:title="
|
||||
isDescriptionDifferentFromLocality
|
||||
? `${physicalAddress.description}, ${physicalAddress.locality}`
|
||||
@ -8,8 +8,7 @@
|
||||
"
|
||||
>
|
||||
<b-icon icon="map-marker" />
|
||||
<span v-if="isDescriptionDifferentFromLocality">
|
||||
{{ physicalAddress.description }},
|
||||
<span v-if="physicalAddress.locality">
|
||||
{{ physicalAddress.locality }}
|
||||
</span>
|
||||
<span v-else>
|
||||
@ -35,11 +34,3 @@ export default class InlineAddress extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.ellipsis {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
@ -48,13 +48,75 @@
|
||||
$t("Mobilizon")
|
||||
}}</a>
|
||||
</i18n>
|
||||
{{
|
||||
$t(
|
||||
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
|
||||
)
|
||||
}}
|
||||
<span v-if="sentryEnabled && sentryReady">
|
||||
{{
|
||||
$t(
|
||||
"We collect your feedback and the error information in order to improve this service."
|
||||
)
|
||||
}}</span
|
||||
>
|
||||
<span v-else>
|
||||
{{
|
||||
$t(
|
||||
"We improve this software thanks to your feedback. To let us know about this issue, two possibilities (both unfortunately require user account creation):"
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</p>
|
||||
<div class="content">
|
||||
<form
|
||||
v-if="sentryEnabled && sentryReady && !submittedFeedback"
|
||||
@submit.prevent="sendErrorToSentry"
|
||||
>
|
||||
<b-field :label="$t('What happened?')" label-for="what-happened">
|
||||
<b-input
|
||||
v-model="feedback"
|
||||
type="textarea"
|
||||
id="what-happened"
|
||||
:placeholder="$t(`I've clicked on X, then on Y`)"
|
||||
/>
|
||||
</b-field>
|
||||
<b-button icon-left="send" native-type="submit" type="is-primary">{{
|
||||
$t("Send feedback")
|
||||
}}</b-button>
|
||||
<p class="content">
|
||||
{{
|
||||
$t(
|
||||
"Please add as many details as possible to help identify the problem."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</form>
|
||||
<b-message type="is-danger" v-else-if="feedbackError">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<i18n path="You may now close this page or {return_to_the_homepage}.">
|
||||
<template #return_to_the_homepage>
|
||||
<router-link :to="{ name: RouteName.HOME }">{{
|
||||
$t("return to the homepage")
|
||||
}}</router-link>
|
||||
</template>
|
||||
</i18n>
|
||||
</b-message>
|
||||
<b-message type="is-success" v-else-if="submittedFeedback">
|
||||
<p>{{ $t("Thanks a lot, your feedback was submitted!") }}</p>
|
||||
<i18n path="You may now close this page or {return_to_the_homepage}.">
|
||||
<template #return_to_the_homepage>
|
||||
<router-link :to="{ name: RouteName.HOME }">{{
|
||||
$t("return to the homepage")
|
||||
}}</router-link>
|
||||
</template>
|
||||
</i18n>
|
||||
</b-message>
|
||||
<div
|
||||
class="content"
|
||||
v-if="!(sentryEnabled && sentryReady) || submittedFeedback"
|
||||
>
|
||||
<p v-if="submittedFeedback">{{ $t("You may also:") }}</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a
|
||||
@ -65,7 +127,7 @@
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href="https://framagit.org/framasoft/mobilizon/-/issues/new?issuable_template=Bug"
|
||||
href="https://framagit.org/framasoft/mobilizon/-/issues/"
|
||||
target="_blank"
|
||||
>{{
|
||||
$t("Open an issue on our bug tracker (advanced users)")
|
||||
@ -74,7 +136,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="content">
|
||||
<p class="content" v-if="!sentryEnabled">
|
||||
{{
|
||||
$t(
|
||||
"Please add as many details as possible to help identify the problem."
|
||||
@ -89,14 +151,14 @@
|
||||
<p>{{ $t("Error stacktrace") }}</p>
|
||||
<pre>{{ error.stack }}</pre>
|
||||
</details>
|
||||
<p>
|
||||
<p v-if="!sentryEnabled">
|
||||
{{
|
||||
$t(
|
||||
"The technical details of the error can help developers solve the problem more easily. Please add them to your feedback."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<div class="buttons">
|
||||
<div class="buttons" v-if="!sentryEnabled">
|
||||
<b-tooltip
|
||||
:label="tooltipConfig.label"
|
||||
:type="tooltipConfig.type"
|
||||
@ -115,14 +177,20 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { CONTACT } from "@/graphql/config";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { checkProviderConfig, convertConfig } from "@/services/statistics";
|
||||
import { IAnalyticsConfig, IConfig } from "@/types/config.model";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { LOGGED_USER } from "@/graphql/user";
|
||||
import { IUser } from "@/types/current-user.model";
|
||||
import { ISentryConfiguration } from "@/types/analytics/sentry.model";
|
||||
import { submitFeedback } from "@/services/statistics/sentry";
|
||||
import RouteName from "@/router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
config: {
|
||||
query: CONTACT,
|
||||
},
|
||||
config: CONFIG,
|
||||
loggedUser: LOGGED_USER,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
@ -138,7 +206,17 @@ export default class ErrorComponent extends Vue {
|
||||
|
||||
copied: "success" | "error" | false = false;
|
||||
|
||||
config!: { contact: string | null; name: string };
|
||||
config!: IConfig;
|
||||
|
||||
feedback = "";
|
||||
|
||||
submittedFeedback = false;
|
||||
|
||||
feedbackError = false;
|
||||
|
||||
loggedUser!: IUser;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async copyErrorToClipboard(): Promise<void> {
|
||||
try {
|
||||
@ -193,6 +271,56 @@ export default class ErrorComponent extends Vue {
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
|
||||
get sentryEnabled(): boolean {
|
||||
return this.sentryProvider?.enabled === true;
|
||||
}
|
||||
|
||||
get sentryProvider(): IAnalyticsConfig | undefined {
|
||||
return this.config && checkProviderConfig(this.config, "sentry");
|
||||
}
|
||||
|
||||
get sentryConfig(): ISentryConfiguration | undefined {
|
||||
if (this.sentryProvider?.configuration) {
|
||||
return convertConfig(
|
||||
this.sentryProvider?.configuration
|
||||
) as ISentryConfiguration;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
get sentryReady() {
|
||||
const eventId = window.sessionStorage.getItem("lastEventId");
|
||||
const dsn = this.sentryConfig?.dsn;
|
||||
const organization = this.sentryConfig?.organization;
|
||||
const project = this.sentryConfig?.project;
|
||||
const host = this.sentryConfig?.host;
|
||||
return eventId && dsn && organization && project && host;
|
||||
}
|
||||
|
||||
async sendErrorToSentry() {
|
||||
try {
|
||||
const eventId = window.sessionStorage.getItem("lastEventId");
|
||||
const dsn = this.sentryConfig?.dsn;
|
||||
const organization = this.sentryConfig?.organization;
|
||||
const project = this.sentryConfig?.project;
|
||||
const host = this.sentryConfig?.host;
|
||||
const endpoint = `https://${host}/api/0/projects/${organization}/${project}/user-feedback/`;
|
||||
if (eventId && dsn && this.sentryReady) {
|
||||
await submitFeedback(endpoint, dsn, {
|
||||
event_id: eventId,
|
||||
name:
|
||||
this.loggedUser?.defaultActor?.preferredUsername || "Unknown user",
|
||||
email: this.loggedUser?.email || "unknown@email.org",
|
||||
comments: this.feedback,
|
||||
});
|
||||
this.submittedFeedback = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.feedbackError = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
@ -67,7 +67,6 @@
|
||||
<inline-address
|
||||
dir="auto"
|
||||
v-if="event.physicalAddress"
|
||||
class="event-subtitle"
|
||||
:physical-address="event.physicalAddress"
|
||||
/>
|
||||
<div
|
||||
|
@ -36,6 +36,7 @@
|
||||
>
|
||||
<router-link
|
||||
v-if="event.attributedTo"
|
||||
class="hover:underline"
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: {
|
||||
@ -53,6 +54,7 @@
|
||||
</router-link>
|
||||
<actor-card v-else :actor="event.organizerActor" :inline="true" />
|
||||
<actor-card
|
||||
:inline="true"
|
||||
:actor="contact"
|
||||
v-for="contact in event.contacts"
|
||||
:key="contact.id"
|
||||
@ -65,6 +67,7 @@
|
||||
>
|
||||
<a
|
||||
target="_blank"
|
||||
class="hover:underline"
|
||||
rel="noopener noreferrer ugc"
|
||||
:href="event.onlineAddress"
|
||||
:title="
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<router-link
|
||||
class="event-minimalist-card-wrapper"
|
||||
class="event-minimalist-card-wrapper bg-white rounded-lg shadow-md"
|
||||
dir="auto"
|
||||
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
|
||||
>
|
||||
|
@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div class="empty-content" :class="{ inline }" role="note">
|
||||
<div
|
||||
class="empty-content"
|
||||
:class="{ inline, 'text-center': center }"
|
||||
role="note"
|
||||
>
|
||||
<b-icon :icon="icon" size="is-large" />
|
||||
<h2 class="empty-content__title">
|
||||
<!-- @slot Mandatory title -->
|
||||
@ -18,6 +22,7 @@ import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
export default class EmptyContent extends Vue {
|
||||
@Prop({ type: String, required: true }) icon!: string;
|
||||
@Prop({ type: Boolean, required: false, default: false }) inline!: boolean;
|
||||
@Prop({ type: Boolean, required: false, default: false }) center!: boolean;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -96,6 +96,15 @@ export const CONFIG = gql`
|
||||
enabled
|
||||
publicKey
|
||||
}
|
||||
analytics {
|
||||
id
|
||||
enabled
|
||||
configuration {
|
||||
key
|
||||
value
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -1309,5 +1309,17 @@
|
||||
"Reset filters": "Reset filters",
|
||||
"Category": "Category",
|
||||
"Select a category": "Select a category",
|
||||
"Any category": "Any category"
|
||||
"Any category": "Any category",
|
||||
"We collect your feedback and the error information in order to improve this service.": "We collect your feedback and the error information in order to improve this service.",
|
||||
"What happened?": "What happened?",
|
||||
"I've clicked on X, then on Y": "I've clicked on X, then on Y",
|
||||
"Send feedback": "Send feedback",
|
||||
"Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.": "Sorry, we wen't able to save your feedback. Don't worry, we'll try to fix this issue anyway.",
|
||||
"return to the homepage": "return to the homepage",
|
||||
"Thanks a lot, your feedback was submitted!": "Thanks a lot, your feedback was submitted!",
|
||||
"You may also:": "You may also:",
|
||||
"You may now close this page or {return_to_the_homepage}.": "You may now close this page or {return_to_the_homepage}.",
|
||||
"This group is a remote group, it's possible the original instance has more informations.": "This group is a remote group, it's possible the original instance has more informations.",
|
||||
"View the group profile on the original instance": "View the group profile on the original instance",
|
||||
"View past events": "View past events"
|
||||
}
|
@ -1309,5 +1309,8 @@
|
||||
"Category": "Catégorie",
|
||||
"Select a category": "Choisissez une categorie",
|
||||
"Any category": "N'importe quelle catégorie",
|
||||
"No instance found.": "Aucune instance trouvée."
|
||||
"No instance found.": "Aucune instance trouvée.",
|
||||
"This group is a remote group, it's possible the original instance has more informations.": "Ce groupe est un groupe distant, il est possible que l'instance d'origine ait plus d'informations.",
|
||||
"View the group profile on the original instance": "Afficher le profil du groupe sur l'instance d'origine",
|
||||
"View past events": "Voir les événements passés"
|
||||
}
|
||||
|
50
js/src/services/statistics/index.ts
Normal file
50
js/src/services/statistics/index.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import {
|
||||
IAnalyticsConfig,
|
||||
IConfig,
|
||||
IKeyValueConfig,
|
||||
} from "@/types/config.model";
|
||||
|
||||
export const statistics = async (config: IConfig, environement: any) => {
|
||||
console.debug("Loading statistics", config.analytics);
|
||||
const matomoConfig = checkProviderConfig(config, "matomo");
|
||||
if (matomoConfig?.enabled === true) {
|
||||
const { matomo } = (await import("./matomo")) as any;
|
||||
matomo(environement, convertConfig(matomoConfig.configuration));
|
||||
}
|
||||
|
||||
const sentryConfig = checkProviderConfig(config, "sentry");
|
||||
if (sentryConfig?.enabled === true) {
|
||||
const { sentry } = (await import("./sentry")) as any;
|
||||
sentry(environement, convertConfig(sentryConfig.configuration));
|
||||
}
|
||||
};
|
||||
|
||||
export const checkProviderConfig = (
|
||||
config: IConfig,
|
||||
providerName: string
|
||||
): IAnalyticsConfig | undefined => {
|
||||
return config?.analytics?.find((provider) => provider.id === providerName);
|
||||
};
|
||||
|
||||
export const convertConfig = (
|
||||
configs: IKeyValueConfig[]
|
||||
): Record<string, any> => {
|
||||
return configs.reduce((acc, config) => {
|
||||
acc[config.key] = toType(config.value, config.type);
|
||||
return acc;
|
||||
}, {} as Record<string, any>);
|
||||
};
|
||||
|
||||
const toType = (value: string, type: string): string | number | boolean => {
|
||||
switch (type) {
|
||||
case "boolean":
|
||||
return value === "true";
|
||||
case "integer":
|
||||
return parseInt(value, 10);
|
||||
case "float":
|
||||
return parseFloat(value);
|
||||
case "string":
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
};
|
14
js/src/services/statistics/matomo.ts
Normal file
14
js/src/services/statistics/matomo.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import Vue from "vue";
|
||||
import VueMatomo from "vue-matomo";
|
||||
|
||||
export const matomo = (environment: any, matomoConfiguration: any) => {
|
||||
console.debug("Loading Matomo statistics");
|
||||
console.debug(
|
||||
"Calling VueMatomo with the following configuration",
|
||||
matomoConfiguration
|
||||
);
|
||||
Vue.use(VueMatomo, {
|
||||
...matomoConfiguration,
|
||||
router: environment.router,
|
||||
});
|
||||
};
|
11
js/src/services/statistics/plausible.ts
Normal file
11
js/src/services/statistics/plausible.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import VueRouter from "vue-router";
|
||||
import Vue from "vue";
|
||||
import { VuePlausible } from "vue-plausible";
|
||||
export default (router: VueRouter, plausibleConfiguration: any) => {
|
||||
console.debug("Loading Plausible statistics");
|
||||
|
||||
Vue.use(VuePlausible, {
|
||||
// see configuration section
|
||||
...plausibleConfiguration,
|
||||
});
|
||||
};
|
54
js/src/services/statistics/sentry.ts
Normal file
54
js/src/services/statistics/sentry.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import Vue from "vue";
|
||||
|
||||
import * as Sentry from "@sentry/vue";
|
||||
import { Integrations } from "@sentry/tracing";
|
||||
|
||||
export const sentry = (environment: any, sentryConfiguration: any) => {
|
||||
console.debug("Loading Sentry statistics");
|
||||
console.debug(
|
||||
"Calling Sentry with the following configuration",
|
||||
sentryConfiguration
|
||||
);
|
||||
// Don't attach errors to previous events
|
||||
window.sessionStorage.removeItem("lastEventId");
|
||||
Sentry.init({
|
||||
Vue,
|
||||
dsn: sentryConfiguration.dsn,
|
||||
integrations: [
|
||||
new Integrations.BrowserTracing({
|
||||
routingInstrumentation: Sentry.vueRouterInstrumentation(
|
||||
environment.router
|
||||
),
|
||||
tracingOrigins: ["localhost", "mobilizon1.com", /^\//],
|
||||
}),
|
||||
],
|
||||
beforeSend(event) {
|
||||
// Check if it is an exception, and if so, save it in session storage
|
||||
// so that it can be retreived from the error component
|
||||
if (event.exception && event.event_id) {
|
||||
window.sessionStorage.setItem("lastEventId", event.event_id);
|
||||
}
|
||||
return event;
|
||||
},
|
||||
// Set tracesSampleRate to 1.0 to capture 100%
|
||||
// of transactions for performance monitoring.
|
||||
// We recommend adjusting this value in production
|
||||
tracesSampleRate: sentryConfiguration.tracesSampleRate,
|
||||
release: environment.version,
|
||||
});
|
||||
};
|
||||
|
||||
export const submitFeedback = async (
|
||||
endpoint: string,
|
||||
dsn: string,
|
||||
params: Record<string, string>
|
||||
): Promise<void> => {
|
||||
await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
Authorization: `DSN ${dsn}`,
|
||||
},
|
||||
body: JSON.stringify(params),
|
||||
});
|
||||
};
|
7
js/src/types/analytics/sentry.model.ts
Normal file
7
js/src/types/analytics/sentry.model.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface ISentryConfiguration {
|
||||
dsn: string;
|
||||
organization?: string;
|
||||
project?: string;
|
||||
host?: string;
|
||||
tracesSampleRate: number;
|
||||
}
|
@ -6,6 +6,18 @@ export interface IOAuthProvider {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface IKeyValueConfig {
|
||||
key: string;
|
||||
value: string;
|
||||
type: "boolean" | "integer" | "string";
|
||||
}
|
||||
|
||||
export interface IAnalyticsConfig {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
configuration: IKeyValueConfig[];
|
||||
}
|
||||
|
||||
export interface IConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
@ -110,4 +122,5 @@ export interface IConfig {
|
||||
exportFormats: {
|
||||
eventParticipants: string[];
|
||||
};
|
||||
analytics: IAnalyticsConfig[];
|
||||
}
|
||||
|
1
js/src/typings/matomo.d.ts
vendored
Normal file
1
js/src/typings/matomo.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare module "vue-matomo";
|
@ -43,21 +43,40 @@
|
||||
<subtitle>
|
||||
{{ showPassedEvents ? $t("Past events") : $t("Upcoming events") }}
|
||||
</subtitle>
|
||||
<b-switch v-model="showPassedEvents">{{ $t("Past events") }}</b-switch>
|
||||
<b-switch class="mb-4" v-model="showPassedEvents">{{
|
||||
$t("Past events")
|
||||
}}</b-switch>
|
||||
<grouped-multi-event-minimalist-card
|
||||
:events="group.organizedEvents.elements"
|
||||
:isCurrentActorMember="isCurrentActorMember"
|
||||
/>
|
||||
<b-message
|
||||
<empty-content
|
||||
v-if="
|
||||
group.organizedEvents.elements.length === 0 &&
|
||||
$apollo.loading === false
|
||||
"
|
||||
type="is-danger"
|
||||
icon="calendar"
|
||||
:inline="true"
|
||||
:center="true"
|
||||
>
|
||||
{{ $t("No events found") }}
|
||||
</b-message>
|
||||
<template v-if="group.domain !== null">
|
||||
<div class="mt-4">
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"This group is a remote group, it's possible the original instance has more informations."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<b-button type="is-text" tag="a" :href="group.url">
|
||||
{{ $t("View the group profile on the original instance") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
</empty-content>
|
||||
<b-pagination
|
||||
class="mt-4"
|
||||
:total="group.organizedEvents.total"
|
||||
v-model="eventsPage"
|
||||
:per-page="EVENTS_PAGE_LIMIT"
|
||||
@ -81,6 +100,7 @@ import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import GroupMixin from "@/mixins/group";
|
||||
import { IMember } from "@/types/actor/member.model";
|
||||
import { FETCH_GROUP_EVENTS } from "@/graphql/event";
|
||||
import EmptyContent from "../../components/Utils/EmptyContent.vue";
|
||||
import { displayName, usernameWithDomain } from "../../types/actor";
|
||||
|
||||
const EVENTS_PAGE_LIMIT = 10;
|
||||
@ -114,6 +134,7 @@ const EVENTS_PAGE_LIMIT = 10;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
EmptyContent,
|
||||
Subtitle,
|
||||
GroupedMultiEventMinimalistCard,
|
||||
},
|
||||
|
@ -579,29 +579,47 @@
|
||||
</div>
|
||||
<empty-content v-else-if="group" icon="calendar" :inline="true">
|
||||
{{ $t("No public upcoming events") }}
|
||||
<template #desc v-if="isCurrentActorFollowing">
|
||||
<i18n
|
||||
class="has-text-grey-dark"
|
||||
path="You will receive notifications about this group's public activity depending on %{notification_settings}."
|
||||
>
|
||||
<router-link
|
||||
:to="{ name: RouteName.NOTIFICATIONS }"
|
||||
slot="notification_settings"
|
||||
>{{ $t("your notification settings") }}</router-link
|
||||
<template #desc>
|
||||
<template v-if="isCurrentActorFollowing">
|
||||
<i18n
|
||||
class="has-text-grey-dark"
|
||||
path="You will receive notifications about this group's public activity depending on %{notification_settings}."
|
||||
>
|
||||
</i18n>
|
||||
<router-link
|
||||
:to="{ name: RouteName.NOTIFICATIONS }"
|
||||
slot="notification_settings"
|
||||
>{{ $t("your notification settings") }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
</template>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
class="my-2"
|
||||
type="is-text"
|
||||
:to="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
query: { future: false },
|
||||
}"
|
||||
>{{ $t("View past events") }}</b-button
|
||||
>
|
||||
</template>
|
||||
</empty-content>
|
||||
<b-skeleton animated v-else-if="$apollo.loading"></b-skeleton>
|
||||
<router-link
|
||||
v-if="organizedEvents.total > 0"
|
||||
:to="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
query: { future: organizedEvents.elements.length > 0 },
|
||||
}"
|
||||
>{{ $t("View all events") }}</router-link
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<b-button
|
||||
tag="router-link"
|
||||
class="my-4"
|
||||
type="is-text"
|
||||
v-if="organizedEvents.total > 0"
|
||||
:to="{
|
||||
name: RouteName.GROUP_EVENTS,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
query: { future: organizedEvents.elements.length > 0 },
|
||||
}"
|
||||
>{{ $t("View all events") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Latest posts") }}</subtitle>
|
||||
|
@ -123,7 +123,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import { ICurrentUser } from "@/types/current-user.model";
|
||||
import { LoginError, LoginErrorCode } from "@/types/enums";
|
||||
@ -207,6 +207,11 @@ export default class Login extends Vue {
|
||||
const { query } = this.$route;
|
||||
this.errorCode = query.code as LoginErrorCode;
|
||||
this.redirect = query.redirect as string | undefined;
|
||||
|
||||
// Already-logged-in and accessing /login
|
||||
if (this.currentUser.isLoggedIn) {
|
||||
this.$router.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
async loginAction(e: Event): Promise<Route | void> {
|
||||
@ -240,7 +245,7 @@ export default class Login extends Vue {
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem("welcome-back", "yes");
|
||||
}
|
||||
this.$router.push({ name: RouteName.HOME });
|
||||
this.$router.replace({ name: RouteName.HOME });
|
||||
return;
|
||||
} catch (err: any) {
|
||||
this.submitted = false;
|
||||
@ -279,13 +284,6 @@ export default class Login extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch("currentUser")
|
||||
redirectToHomepageIfAlreadyLoggedIn(): Promise<Route> | void {
|
||||
if (this.currentUser.isLoggedIn) {
|
||||
return this.$router.push("/");
|
||||
}
|
||||
}
|
||||
|
||||
get hasCaseWarning(): boolean {
|
||||
return this.credentials.email !== this.credentials.email.toLowerCase();
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ export const defaultResolvers = {
|
||||
id: "67",
|
||||
preferredUsername: "someone",
|
||||
name: "Personne",
|
||||
avatar: null,
|
||||
__typename: "CurrentActor",
|
||||
}),
|
||||
},
|
||||
|
@ -22,7 +22,7 @@ import { InMemoryCache } from "@apollo/client/cache";
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Buefy);
|
||||
config.mocks.$t = (key: string): string => key;
|
||||
const $router = { push: jest.fn() };
|
||||
const $router = { push: jest.fn(), replace: jest.fn() };
|
||||
|
||||
describe("Render login form", () => {
|
||||
let wrapper: Wrapper<Vue>;
|
||||
@ -125,9 +125,9 @@ describe("Render login form", () => {
|
||||
await flushPromises();
|
||||
expect(currentUser?.email).toBe("some@email.tld");
|
||||
expect(currentUser?.id).toBe("1");
|
||||
expect(jest.isMockFunction(wrapper.vm.$router.push)).toBe(true);
|
||||
expect(jest.isMockFunction(wrapper.vm.$router.replace)).toBe(true);
|
||||
await flushPromises();
|
||||
expect($router.push).toHaveBeenCalledWith({ name: RouteName.HOME });
|
||||
expect($router.replace).toHaveBeenCalledWith({ name: RouteName.HOME });
|
||||
});
|
||||
|
||||
it("handles a login error", async () => {
|
||||
|
@ -123,6 +123,8 @@ export const configMock = {
|
||||
enabled: true,
|
||||
publicKey: "",
|
||||
},
|
||||
eventCategories: [],
|
||||
analytics: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
599
js/yarn.lock
599
js/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -8,6 +8,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
alias Mobilizon.Addresses.Address
|
||||
alias Mobilizon.Events.Categories
|
||||
alias Mobilizon.Events.Event, as: EventModel
|
||||
alias Mobilizon.Medias.Media
|
||||
|
||||
@ -73,7 +74,7 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
medias: medias,
|
||||
begins_on: object["startTime"],
|
||||
ends_on: object["endTime"],
|
||||
category: object["category"],
|
||||
category: get_category(object["category"]),
|
||||
visibility: visibility,
|
||||
join_options: Map.get(object, "joinMode", "free"),
|
||||
local: is_local?(object["id"]),
|
||||
@ -330,4 +331,15 @@ defmodule Mobilizon.Federation.ActivityStream.Converter.Event do
|
||||
_participant_count
|
||||
),
|
||||
do: nil
|
||||
|
||||
@spec get_category(String.t() | nil) :: String.t()
|
||||
defp get_category(nil), do: "MEETING"
|
||||
|
||||
defp get_category(category) when is_binary(category) do
|
||||
if category in Enum.map(Categories.list(), &String.upcase(to_string(&1.id))) do
|
||||
category
|
||||
else
|
||||
get_category(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -5,6 +5,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
||||
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Events.Categories
|
||||
alias Mobilizon.Service.FrontEndAnalytics
|
||||
|
||||
@doc """
|
||||
Gets config.
|
||||
@ -170,7 +171,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
|
||||
public_key:
|
||||
get_in(Application.get_env(:web_push_encryption, :vapid_details), [:public_key])
|
||||
},
|
||||
export_formats: Config.instance_export_formats()
|
||||
export_formats: Config.instance_export_formats(),
|
||||
analytics: FrontEndAnalytics.config()
|
||||
}
|
||||
end
|
||||
end
|
||||
|
@ -75,6 +75,10 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
|
||||
field(:web_push, :web_push, description: "Web Push settings for the instance")
|
||||
|
||||
field(:export_formats, :export_formats, description: "The instance list of export formats")
|
||||
|
||||
field(:analytics, list_of(:analytics),
|
||||
description: "Configuration for diverse analytics services"
|
||||
)
|
||||
end
|
||||
|
||||
@desc """
|
||||
@ -330,6 +334,28 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
|
||||
field(:public_key, :string, description: "The server's public WebPush VAPID key")
|
||||
end
|
||||
|
||||
object :analytics do
|
||||
field(:id, :string, description: "ID of the analytics service")
|
||||
field(:enabled, :boolean, description: "Whether the service is activated or not")
|
||||
|
||||
field(:configuration, list_of(:analytics_configuration),
|
||||
description: "A list of key-values configuration"
|
||||
)
|
||||
end
|
||||
|
||||
enum :analytics_configuration_type do
|
||||
value(:string, description: "A string")
|
||||
value(:integer, description: "An integer")
|
||||
value(:boolean, description: "A boolean")
|
||||
value(:float, description: "A float")
|
||||
end
|
||||
|
||||
object :analytics_configuration do
|
||||
field(:key, :string, description: "The key for the analytics configuration element")
|
||||
field(:value, :string, description: "The value for the analytics configuration element")
|
||||
field(:type, :analytics_configuration_type, description: "The analytics configuration type")
|
||||
end
|
||||
|
||||
@desc """
|
||||
Export formats configuration
|
||||
"""
|
||||
|
71
lib/service/front_end_analytics/analytics.ex
Normal file
71
lib/service/front_end_analytics/analytics.ex
Normal file
@ -0,0 +1,71 @@
|
||||
defmodule Mobilizon.Service.FrontEndAnalytics do
|
||||
@moduledoc """
|
||||
Behaviour for any analytics service
|
||||
"""
|
||||
|
||||
@callback id() :: String.t()
|
||||
|
||||
@doc """
|
||||
Whether the service is enabled
|
||||
"""
|
||||
@callback enabled?() :: boolean()
|
||||
|
||||
@doc """
|
||||
The configuration for the service
|
||||
"""
|
||||
@callback configuration() :: keyword()
|
||||
|
||||
@doc """
|
||||
The CSP configuration to add for the service to work
|
||||
"""
|
||||
@callback csp() :: keyword()
|
||||
|
||||
@spec providers :: list(module())
|
||||
def providers do
|
||||
:mobilizon
|
||||
|> Application.get_env(:analytics, [])
|
||||
|> Keyword.get(:providers, [])
|
||||
end
|
||||
|
||||
@spec config :: map()
|
||||
def config do
|
||||
Enum.reduce(providers(), [], &load_config/2)
|
||||
end
|
||||
|
||||
@spec csp :: keyword()
|
||||
def csp do
|
||||
providers()
|
||||
|> Enum.map(& &1.csp())
|
||||
|> Enum.reduce([], &merge_csp_config/2)
|
||||
end
|
||||
|
||||
@spec load_config(module(), list(map())) :: list(map())
|
||||
defp load_config(provider, acc) do
|
||||
acc ++
|
||||
[
|
||||
%{
|
||||
id: provider.id(),
|
||||
enabled: provider.enabled?(),
|
||||
configuration: convert_config(provider.configuration())
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
@spec convert_config(Keyword.t()) :: list(map())
|
||||
defp convert_config(config) do
|
||||
Enum.reduce(config, [], fn {key, val}, acc ->
|
||||
acc ++ [%{key: key, value: val, type: type(val)}]
|
||||
end)
|
||||
end
|
||||
|
||||
defp type(val) when is_integer(val), do: :integer
|
||||
defp type(val) when is_float(val), do: :float
|
||||
defp type(val) when is_boolean(val), do: :boolean
|
||||
defp type(val) when is_binary(val), do: :string
|
||||
|
||||
defp merge_csp_config(config, global_config) do
|
||||
Keyword.merge(global_config, config, fn _key, global, config ->
|
||||
"#{global} #{config}"
|
||||
end)
|
||||
end
|
||||
end
|
40
lib/service/front_end_analytics/matomo.ex
Normal file
40
lib/service/front_end_analytics/matomo.ex
Normal file
@ -0,0 +1,40 @@
|
||||
defmodule Mobilizon.Service.FrontEndAnalytics.Matomo do
|
||||
@moduledoc """
|
||||
Matomo analytics provider
|
||||
"""
|
||||
alias Mobilizon.Service.FrontEndAnalytics
|
||||
@behaviour FrontEndAnalytics
|
||||
|
||||
@impl FrontEndAnalytics
|
||||
def id, do: "matomo"
|
||||
|
||||
@doc """
|
||||
Whether the service is enabled
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def enabled? do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.get(:enabled, false)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The configuration for the service
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def configuration do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.drop([:enabled, :csp])
|
||||
end
|
||||
|
||||
@doc """
|
||||
The CSP configuration to add for the service to work
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def csp do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.get(:csp, [])
|
||||
end
|
||||
end
|
41
lib/service/front_end_analytics/plausible.ex
Normal file
41
lib/service/front_end_analytics/plausible.ex
Normal file
@ -0,0 +1,41 @@
|
||||
defmodule Mobilizon.Service.FrontEndAnalytics.Plausible do
|
||||
@moduledoc """
|
||||
Plausible analytics provider
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.FrontEndAnalytics
|
||||
@behaviour FrontEndAnalytics
|
||||
|
||||
@impl FrontEndAnalytics
|
||||
def id, do: "plausible"
|
||||
|
||||
@doc """
|
||||
Whether the service is enabled
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def enabled? do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.get(:enabled, false)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The configuration for the service
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def configuration do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.drop([:enabled, :csp])
|
||||
end
|
||||
|
||||
@doc """
|
||||
The CSP configuration to add for the service to work
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def csp do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.get(:csp, [])
|
||||
end
|
||||
end
|
41
lib/service/front_end_analytics/sentry.ex
Normal file
41
lib/service/front_end_analytics/sentry.ex
Normal file
@ -0,0 +1,41 @@
|
||||
defmodule Mobilizon.Service.FrontEndAnalytics.Sentry do
|
||||
@moduledoc """
|
||||
Sentry analytics provider
|
||||
"""
|
||||
|
||||
alias Mobilizon.Service.FrontEndAnalytics
|
||||
@behaviour FrontEndAnalytics
|
||||
|
||||
@impl FrontEndAnalytics
|
||||
def id, do: "sentry"
|
||||
|
||||
@doc """
|
||||
Whether the service is enabled
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def enabled? do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.get(:enabled, false)
|
||||
end
|
||||
|
||||
@doc """
|
||||
The configuration for the service
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def configuration do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.drop([:enabled, :csp])
|
||||
end
|
||||
|
||||
@doc """
|
||||
The CSP configuration to add for the service to work
|
||||
"""
|
||||
@impl FrontEndAnalytics
|
||||
def csp do
|
||||
:mobilizon
|
||||
|> Application.get_env(__MODULE__, [])
|
||||
|> Keyword.get(:csp, [])
|
||||
end
|
||||
end
|
10
lib/web/cache/activity_pub.ex
vendored
10
lib/web/cache/activity_pub.ex
vendored
@ -29,9 +29,12 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
|
||||
@spec do_get_actor(String.t()) :: {:commit, Actor.t()} | {:ignore, nil}
|
||||
defp do_get_actor("actor_" <> name) do
|
||||
case Actor.find_or_make_actor_from_nickname(name) do
|
||||
{:ok, %ActorModel{} = actor} ->
|
||||
{:ok, %ActorModel{suspended: false} = actor} ->
|
||||
{:commit, actor}
|
||||
|
||||
{:ok, %ActorModel{}} ->
|
||||
{:ignore, nil}
|
||||
|
||||
{:error, _err} ->
|
||||
{:ignore, nil}
|
||||
end
|
||||
@ -45,9 +48,12 @@ defmodule Mobilizon.Web.Cache.ActivityPub do
|
||||
def get_local_actor_by_name(name) do
|
||||
Cachex.fetch(@cache, "local_actor_" <> name, fn "local_actor_" <> name ->
|
||||
case Actors.get_local_actor_by_name(name) do
|
||||
%ActorModel{} = actor ->
|
||||
%ActorModel{suspended: false} = actor ->
|
||||
{:commit, actor}
|
||||
|
||||
{:ok, %ActorModel{}} ->
|
||||
{:ignore, nil}
|
||||
|
||||
nil ->
|
||||
{:ignore, nil}
|
||||
end
|
||||
|
@ -9,6 +9,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
|
||||
"""
|
||||
|
||||
alias Mobilizon.Config
|
||||
alias Mobilizon.Service.FrontEndAnalytics
|
||||
import Plug.Conn
|
||||
|
||||
require Logger
|
||||
@ -136,8 +137,9 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlug do
|
||||
|
||||
@spec get_csp_config(atom(), Keyword.t()) :: iodata()
|
||||
defp get_csp_config(type, options) do
|
||||
options
|
||||
|> Keyword.get(type, Config.get([:http_security, :csp_policy, type]))
|
||||
|> Enum.join(" ")
|
||||
config_policy = Keyword.get(options, type, Config.get([:http_security, :csp_policy, type]))
|
||||
front_end_analytics_policy = [Keyword.get(FrontEndAnalytics.csp(), type, [])]
|
||||
|
||||
Enum.join(config_policy ++ front_end_analytics_policy, " ")
|
||||
end
|
||||
end
|
||||
|
2
mix.exs
2
mix.exs
@ -168,7 +168,7 @@ defmodule Mobilizon.Mixfile do
|
||||
{:mogrify, "~> 0.9"},
|
||||
{:linkify, "~> 0.3"},
|
||||
{:http_signatures, "~> 0.1.0"},
|
||||
{:ex_cldr, "~> 2.0"},
|
||||
{:ex_cldr, "2.27.1"},
|
||||
{:ex_cldr_dates_times, "~> 2.2"},
|
||||
{:ex_optimizer, "~> 0.1"},
|
||||
{:progress_bar, "~> 2.0"},
|
||||
|
4
mix.lock
4
mix.lock
@ -103,7 +103,7 @@
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.7", "05a42377075868a678d446361effba80cefef19ab98941c01a7a4c7560b29121", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "25eaf41028eb351b90d4f69671874643a09944098fefd0d01d442f40a6091b6f"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.0", "5ea4036a3c8f372e6fbf928c822b16028bcaaf2b26ea83d5775670498af7bd92", [:mix], [], "hexpm", "fe61113eff12693a758080ac595dc86bfe3744d4734520a96f6c1a0d7f13c126"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_swoosh": {:hex, :phoenix_swoosh, "1.0.1", "0db6eb6405a6b06cae4fdf4144659b3f4fee4553e2856fe8a53ba12e9fb21a74", [:mix], [{:finch, "~> 0.8", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:swoosh, "~> 1.5", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm", "e34890004baec08f0fa12bd8c77bf64bfb4156b84a07fb79da9322fa94bc3781"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
|
||||
"plug": {:hex, :plug, "1.13.4", "addb6e125347226e3b11489e23d22a60f7ab74786befb86c14f94fb5f23ca9a4", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "06114c1f2a334212fe3ae567dbb3b1d29fd492c1a09783d52f3d489c1a6f4cf2"},
|
||||
@ -122,7 +122,7 @@
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"struct_access": {:hex, :struct_access, "1.1.2", "a42e6ceedd9b9ea090ee94a6da089d56e16f374dbbc010c3eebdf8be17df286f", [:mix], [], "hexpm", "e4c411dcc0226081b95709909551fc92b8feb1a3476108348ea7e3f6c12e586a"},
|
||||
"sweet_xml": {:hex, :sweet_xml, "0.7.2", "4729f997286811fabdd8288f8474e0840a76573051062f066c4b597e76f14f9f", [:mix], [], "hexpm", "6894e68a120f454534d99045ea3325f7740ea71260bc315f82e29731d570a6e8"},
|
||||
"swoosh": {:hex, :swoosh, "1.6.3", "598d3f07641004bedb3eede40057760ae18be1073cff72f079ca1e1fc9cd97b9", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "81ff9d7c7c4005a57465a7eb712edd71db51829aef94c8a34c30c5b9e9964adf"},
|
||||
"swoosh": {:hex, :swoosh, "1.6.4", "ce3a4bf3e5276fd114178ebc5ed072ee0c177a7b3a09e5992aa005778ac143c2", [:mix], [{:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad4c8b534812433730b6241a1d9df38b1da75fdfa340f51887a31d7e9343fffe"},
|
||||
"telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
|
||||
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
|
||||
"timex": {:hex, :timex, "3.7.7", "3ed093cae596a410759104d878ad7b38e78b7c2151c6190340835515d4a46b8a", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "0ec4b09f25fe311321f9fc04144a7e3affe48eb29481d7a5583849b6c4dfa0a7"},
|
||||
|
@ -14,6 +14,7 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
alias Mobilizon.Federation.ActivityPub
|
||||
alias Mobilizon.Service.ActorSuspension
|
||||
alias Mobilizon.Service.HTTP.ActivityPub.Mock
|
||||
alias Mobilizon.Web.ActivityPub.ActorView
|
||||
alias Mobilizon.Web.{Endpoint, PageView}
|
||||
@ -44,6 +45,24 @@ defmodule Mobilizon.Web.ActivityPubControllerTest do
|
||||
|> Jason.encode!()
|
||||
|> Jason.decode!()
|
||||
end
|
||||
|
||||
test "it returns nothing if the actor is suspended", %{conn: conn} do
|
||||
suspended = insert(:actor)
|
||||
|
||||
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
|
||||
assert json_response(conn, 200)
|
||||
|
||||
assert {:ok, true} ==
|
||||
Cachex.exists?(:activity_pub, "actor_" <> suspended.preferred_username)
|
||||
|
||||
ActorSuspension.suspend_actor(suspended)
|
||||
|
||||
assert {:ok, false} ==
|
||||
Cachex.exists?(:activity_pub, "actor_" <> suspended.preferred_username)
|
||||
|
||||
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
|
||||
assert json_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
describe "/events/:uuid" do
|
||||
|
@ -4,7 +4,7 @@ defmodule Mobilizon.Web.PageControllerTest do
|
||||
import Mobilizon.Factory
|
||||
|
||||
alias Mobilizon.Actors.Actor
|
||||
|
||||
alias Mobilizon.Service.ActorSuspension
|
||||
alias Mobilizon.Web.Endpoint
|
||||
alias Mobilizon.Web.Router.Helpers, as: Routes
|
||||
|
||||
@ -37,6 +37,18 @@ defmodule Mobilizon.Web.PageControllerTest do
|
||||
conn = get(conn, Actor.build_url("not_existing", :page))
|
||||
assert html_response(conn, 404)
|
||||
end
|
||||
|
||||
test "GET /@actor when suspended", %{conn: conn} do
|
||||
suspended = insert(:actor)
|
||||
|
||||
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
|
||||
assert html_response(conn, 200)
|
||||
|
||||
ActorSuspension.suspend_actor(suspended)
|
||||
|
||||
conn = get(conn, Actor.build_url(suspended.preferred_username, :page))
|
||||
assert html_response(conn, 404)
|
||||
end
|
||||
end
|
||||
|
||||
test "GET /events/:uuid", %{conn: conn} do
|
||||
|
@ -73,7 +73,7 @@ defmodule Mobilizon.Web.Plugs.HTTPSecurityPlugTest do
|
||||
[csp] = Conn.get_resp_header(conn, "content-security-policy")
|
||||
|
||||
assert csp =~
|
||||
~r/script-src 'self' 'unsafe-eval' 'sha256-[\w+\/=]*' example.com matomo.example.com;/
|
||||
~r/script-src 'self' 'unsafe-eval' 'sha256-[\w+\/=]*' example.com matomo.example.com ;/
|
||||
end
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user