Add anonymous and remote participations

master
Thomas Citharel 3 years ago
parent 17e0b3968f
commit 2ed9050a90
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
  1. 22
      config/config.exs
  2. 2
      config/dev.exs
  3. 4
      config/test.exs
  4. 1
      js/package.json
  5. 9
      js/src/App.vue
  6. 1
      js/src/assets/undraw_mail_2.svg
  7. 1
      js/src/assets/undraw_profile.svg
  8. 6
      js/src/components/Event/AddressAutoComplete.vue
  9. 171
      js/src/components/Event/EventListViewCard.vue
  10. 40
      js/src/components/Event/ParticipationButton.vue
  11. 3
      js/src/components/Footer.vue
  12. 59
      js/src/components/Participation/ConfirmParticipation.vue
  13. 82
      js/src/components/Participation/ParticipationWithAccount.vue
  14. 109
      js/src/components/Participation/ParticipationWithoutAccount.vue
  15. 104
      js/src/components/Participation/UnloggedParticipation.vue
  16. 22
      js/src/components/Utils/VerticalDivider.vue
  17. 42
      js/src/graphql/admin.ts
  18. 39
      js/src/graphql/config.ts
  19. 31
      js/src/graphql/event.ts
  20. 62
      js/src/i18n/en_US.json
  21. 110
      js/src/i18n/fr_FR.json
  22. 76
      js/src/mixins/event.ts
  23. 9
      js/src/router/admin.ts
  24. 33
      js/src/router/event.ts
  25. 14
      js/src/router/index.ts
  26. 128
      js/src/services/AnonymousParticipationStorage.ts
  27. 15
      js/src/types/admin.model.ts
  28. 34
      js/src/types/config.model.ts
  29. 9
      js/src/types/event.model.ts
  30. 8
      js/src/variables.scss
  31. 2
      js/src/views/Account/MyAccount.vue
  32. 2
      js/src/views/Account/Register.vue
  33. 9
      js/src/views/Admin/Dashboard.vue
  34. 4
      js/src/views/Admin/Follows.vue
  35. 94
      js/src/views/Admin/Settings.vue
  36. 286
      js/src/views/Event/Edit.vue
  37. 162
      js/src/views/Event/Event.vue
  38. 16
      js/src/views/Event/Explore.vue
  39. 16
      js/src/views/Event/MyEvents.vue
  40. 17
      js/src/views/Event/Participants.vue
  41. 16
      js/src/views/Home.vue
  42. 57
      js/src/views/Interact.vue
  43. 4
      js/src/views/Moderation/Logs.vue
  44. 5
      js/src/views/Moderation/Report.vue
  45. 2
      js/src/views/Moderation/ReportList.vue
  46. 2
      js/src/views/PageNotFound.vue
  47. 64
      js/src/views/Terms.vue
  48. 132
      js/src/views/User/Login.vue
  49. 2
      js/src/views/User/PasswordChange.vue
  50. 2
      js/src/views/User/PasswordReset.vue
  51. 2
      js/src/views/User/Register.vue
  52. 2
      js/src/views/User/ResendConfirmation.vue
  53. 2
      js/src/views/User/SendPasswordReset.vue
  54. 2
      js/src/views/User/Validate.vue
  55. 6
      js/src/vue-apollo.ts
  56. 5
      js/yarn.lock
  57. 38
      lib/federation/activity_pub/activity_pub.ex
  58. 4
      lib/federation/activity_pub/relay.ex
  59. 4
      lib/federation/activity_pub/utils.ex
  60. 2
      lib/federation/activity_stream/converter/event.ex
  61. 8
      lib/federation/web_finger/web_finger.ex
  62. 40
      lib/graphql/api/participations.ex
  63. 47
      lib/graphql/resolvers/admin.ex
  64. 97
      lib/graphql/resolvers/config.ex
  65. 124
      lib/graphql/resolvers/event.ex
  66. 262
      lib/graphql/resolvers/participant.ex
  67. 30
      lib/graphql/schema/admin.ex
  68. 56
      lib/graphql/schema/config.ex
  69. 10
      lib/graphql/schema/event.ex
  70. 27
      lib/graphql/schema/events/participant.ex
  71. 48
      lib/mobilizon.ex
  72. 62
      lib/mobilizon/actors/actor.ex
  73. 19
      lib/mobilizon/actors/actors.ex
  74. 47
      lib/mobilizon/admin/admin.ex
  75. 27
      lib/mobilizon/admin/setting.ex
  76. 123
      lib/mobilizon/config.ex
  77. 3
      lib/mobilizon/events/event_options.ex
  78. 4
      lib/mobilizon/events/event_participant_stats.ex
  79. 45
      lib/mobilizon/events/events.ex
  80. 14
      lib/mobilizon/events/participant.ex
  81. 24
      lib/mobilizon/users/user.ex
  82. 2
      lib/web/channels/graphql_socket.ex
  83. 64
      lib/web/controllers/page_controller.ex
  84. 15
      lib/web/email/checker.ex
  85. 60
      lib/web/email/participation.ex
  86. 14
      lib/web/router.ex
  87. 163
      lib/web/templates/api/terms.html.eex
  88. 81
      lib/web/templates/email/anonymous_participation_confirmation.html.eex
  89. 11
      lib/web/templates/email/anonymous_participation_confirmation.text.eex
  90. 2
      lib/web/templates/email/event_participation_approved.html.eex
  91. 2
      lib/web/templates/email/event_participation_rejected.html.eex
  92. 2
      lib/web/templates/email/event_updated.html.eex
  93. 2
      lib/web/templates/email/password_reset.html.eex
  94. 2
      lib/web/templates/email/registration_confirmation.html.eex
  95. 2
      lib/web/templates/email/report.html.eex
  96. 7
      lib/web/views/api_view.ex
  97. 76
      mix.exs
  98. 2
      mix.lock
  99. 428
      priv/gettext/ar/LC_MESSAGES/default.po
  100. 428
      priv/gettext/be/LC_MESSAGES/default.po
  101. Some files were not shown because too many files have changed in this diff Show More

@ -156,6 +156,28 @@ config :mobilizon, :maps,
attribution: System.get_env("MAPS_TILES_ATTRIBUTION")
]
config :mobilizon, :anonymous,
participation: [
allowed: true,
validation: %{
email: [
enabled: true,
confirmation_required: true
],
captcha: [enabled: false]
}
],
event_creation: [
allowed: false,
validation: %{
email: [
enabled: true,
confirmation_required: true
],
captcha: [enabled: false]
}
]
config :mobilizon, Oban,
repo: Mobilizon.Storage.Repo,
prune: {:maxlen, 10_000},

@ -73,3 +73,5 @@ config :mobilizon, Mobilizon.Storage.Repo,
port: System.get_env("MOBILIZON_DATABASE_PORT") || "5432",
pool_size: 10,
show_sensitive_data_on_connection_error: true
config :mobilizon, :activitypub, sign_object_fetches: false

@ -18,7 +18,9 @@ config :mobilizon, Mobilizon.Web.Endpoint,
# Print only warnings and errors during test
config :logger,
backends: [:console],
compile_time_purge_level: :debug,
compile_time_purge_matching: [
[level_lower_than: :debug]
],
level: :info
# Configure your database

@ -28,6 +28,7 @@
"apollo-link-ws": "^1.0.19",
"apollo-utilities": "^1.3.2",
"buefy": "^0.8.2",
"bulma-divider": "^0.2.0",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
"intersection-observer": "^0.7.0",

@ -85,6 +85,7 @@ export default class App extends Vue {
/* Bulma imports */
@import "~bulma/bulma";
@import '~bulma-divider';
/* Buefy imports */
@import "~buefy/src/scss/buefy";
@ -102,12 +103,12 @@ $mdi-font-path: "~@mdi/font/fonts";
body {
// background: #f7f8fa;
background: #ebebeb;
background: $body-background-color;
font-family: BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Fira Sans','Droid Sans','Helvetica Neue',Helvetica,Arial,sans-serif;
main {
margin: 1rem auto 0;
}
/*main {*/
/* margin: 1rem auto 0;*/
/*}*/
}
#mobilizon > .container > .message {

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

@ -1,5 +1,5 @@
<template>
<div>
<div class="address-autocomplete">
<b-field expanded>
<template slot="label">
{{ $t('Find an address') }}
@ -245,6 +245,10 @@ export default class AddressAutoComplete extends Vue {
}
</script>
<style lang="scss">
.address-autocomplete {
margin-bottom: 0.75rem;
}
.autocomplete {
.dropdown-menu {
z-index: 2000;

@ -0,0 +1,171 @@
<docs>
A simple card for an event
```vue
<template>
<div>
<EventListViewCard
:event="event"
/>
</div>
</template>
<script>
export default {
data() {
return {
event: {
title: 'Vue Styleguidist first meetup: learn the basics!',
id: 5,
uuid: 'some uuid',
beginsOn: new Date(),
organizerActor: {
preferredUsername: 'tcit',
name: 'Some Random Dude',
domain: null,
id: 4,
displayName() {
return 'Some random dude'
}
},
options: {
maximumAttendeeCapacity: 4
},
participantStats: {
approved: 1,
notApproved: 2
}
}
}
}
}
}
</script>
```
</docs>
<template>
<article class="box">
<div class="columns">
<div class="content column">
<div class="title-wrapper">
<div class="date-component">
<date-calendar-icon :date="event.beginsOn" />
</div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"><h2 class="title">{{ event.title }}</h2></router-link>
</div>
<div class="participation-actor has-text-grey">
<span v-if="event.physicalAddress && event.physicalAddress.locality">{{ event.physicalAddress.locality }}</span>
<span>
<span>{{ $t('Organized by {name}', { name: event.organizerActor.displayName() } ) }}</span>
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" v-if="event.visibility === EventVisibility.PUBLIC" />
<b-icon icon="lock-open" v-if="event.visibility === EventVisibility.UNLISTED" />
<b-icon icon="lock" v-if="event.visibility === EventVisibility.PRIVATE" />
</span>
<span class="column is-narrow participant-stats">
<span v-if="event.options.maximumAttendeeCapacity !== 0">
{{ $t('{approved} / {total} seats', {approved: event.participantStats.participant, total: event.options.maximumAttendeeCapacity }) }}
</span>
<span v-else>
{{ $tc('{count} participants', event.participantStats.participant, { count: event.participantStats.participant })}}
</span>
</span>
</div>
</div>
</div>
</article>
</template>
<script lang="ts">
import { IParticipant, ParticipantRole, EventVisibility, IEventCardOptions } from '@/types/event.model';
import { Component, Prop } from 'vue-property-decorator';
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
import { IPerson } from '@/types/actor';
import { mixins } from 'vue-class-component';
import ActorMixin from '@/mixins/actor';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import EventMixin from '@/mixins/event';
import { RouteName } from '@/router';
import { changeIdentity } from '@/utils/auth';
import { Route } from 'vue-router';
const defaultOptions: IEventCardOptions = {
hideDate: true,
loggedPerson: false,
hideDetails: false,
organizerActor: null,
};
@Component({
components: {
DateCalendarIcon,
},
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
export default class EventListViewCard extends mixins(ActorMixin, EventMixin) {
/**
* The participation associated
*/
@Prop({ required: true }) event!: IParticipant;
/**
* Options are merged with default options
*/
@Prop({ required: false, default: () => defaultOptions }) options!: IEventCardOptions;
currentActor!: IPerson;
ParticipantRole = ParticipantRole;
EventVisibility = EventVisibility;
RouteName = RouteName;
}
</script>
<style lang="scss" scoped>
@import "../../variables";
article.box {
div.content {
padding: 5px;
.participation-actor span, .participant-stats span {
padding: 0 5px;
button {
height: auto;
padding-top: 0;
}
}
div.title-wrapper {
display: flex;
align-items: center;
div.date-component {
flex: 0;
margin-right: 16px;
}
.title {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
font-weight: 400;
line-height: 1em;
font-size: 1.6em;
padding-bottom: 5px;
margin: auto 0;
}
}
}
}
</style>

@ -25,7 +25,7 @@ A button to set your participation
<template>
<div class="participation-button">
<b-dropdown aria-role="list" position="is-bottom-left" v-if="participation && participation.role === ParticipantRole.PARTICIPANT">
<button class="button is-success" type="button" slot="trigger">
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="check" />
<template>
<span>{{ $t('I participate') }}</span>
@ -33,10 +33,6 @@ A button to set your participation
<b-icon icon="menu-down" />
</button>
<!-- <b-dropdown-item :value="false" aria-role="listitem">-->
<!-- {{ $t('Change my identity…')}}-->
<!-- </b-dropdown-item>-->
<b-dropdown-item :value="false" aria-role="listitem" @click="confirmLeave" class="has-text-danger">
{{ $t('Cancel my participation…')}}
</b-dropdown-item>
@ -44,7 +40,7 @@ A button to set your participation
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<button class="button is-success" type="button" slot="trigger">
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="timer-sand-empty" />
<template>
<span>{{ $t('I participate') }}</span>
@ -68,8 +64,8 @@ A button to set your participation
<span>{{ $t('Unfortunately, your participation request was rejected by the organizers.')}}</span>
</div>
<b-dropdown aria-role="list" position="is-bottom-left" v-if="!participation">
<button class="button is-primary" type="button" slot="trigger">
<b-dropdown aria-role="list" position="is-bottom-left" v-else-if="!participation && currentActor.id">
<button class="button is-primary is-large" type="button" slot="trigger">
<template>
<span>{{ $t('Participate') }}</span>
</template>
@ -93,22 +89,29 @@ A button to set your participation
{{ $t('with another identity…')}}
</b-dropdown-item>
</b-dropdown>
<b-button tag="router-link" :to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }" v-else-if="!participation && hasAnonymousParticipationMethods" type="is-primary" size="is-large" native-type="button">{{ $t('Participate') }}</b-button>
<b-button tag="router-link" :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }" v-else-if="!currentActor.id" type="is-primary" size="is-large" native-type="button">{{ $t('Participate') }}</b-button>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IParticipant, ParticipantRole } from '@/types/event.model';
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { IPerson, Person } from '@/types/actor';
import { IDENTITIES } from '@/graphql/actor';
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from '@/graphql/actor';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { RouteName } from '@/router';
import { FETCH_EVENT } from '@/graphql/event';
@Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT,
},
currentActor: CURRENT_ACTOR_CLIENT,
config: CONFIG,
identities: {
query: IDENTITIES,
update: ({ identities }) => identities ? identities.map(identity => new Person(identity)) : [],
@ -120,11 +123,13 @@ import { ICurrentUser } from '@/types/current-user.model';
})
export default class ParticipationButton extends Vue {
@Prop({ required: true }) participation!: IParticipant;
@Prop({ required: true }) event!: IEvent;
@Prop({ required: true }) currentActor!: IPerson;
ParticipantRole = ParticipantRole;
currentUser!: ICurrentUser;
identities: IPerson[] = [];
config!: IConfig;
RouteName = RouteName;
joinEvent(actor: IPerson) {
this.$emit('joinEvent', actor);
@ -138,10 +143,13 @@ export default class ParticipationButton extends Vue {
this.$emit('confirmLeave');
}
get hasAnonymousParticipationMethods(): boolean {
return this.event.options.anonymousParticipation;
}
}
</script>
<style lang="scss">
<style lang="scss" scoped>
.participation-button {
.dropdown {
display: flex;
@ -151,9 +159,11 @@ export default class ParticipationButton extends Vue {
opacity: 0.5;
}
}
}
button {
font-size: 1.5rem;
.anonymousParticipationModal {
/deep/ .animation-content {
z-index: 1;
}
}
</style>

@ -4,6 +4,7 @@
<img src="../assets/footer.png" :alt="$t('World map')" />
<ul>
<li><a href="https://joinmobilizon.org">{{ $t('About') }}</a></li>
<li><router-link :to="{ name: RouteName.TERMS }">{{ $t('Terms') }}</router-link></li>
<li><a href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">{{ $t('License') }}</a></li>
</ul>
<div class="content has-text-centered">
@ -14,6 +15,7 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import Logo from './Logo.vue';
import { RouteName } from '@/router';
@Component({
components: {
@ -21,6 +23,7 @@ import Logo from './Logo.vue';
},
})
export default class Footer extends Vue {
RouteName = RouteName;
}
</script>
<style lang="scss" scoped>

@ -0,0 +1,59 @@
<template>
<section class="container">
<h1 class="title" v-if="loading">
{{ $t('Your participation is being validated') }}
</h1>
<div v-else>
<div v-if="failed">
<b-message :title="$t('Error while validating participation')" type="is-danger">
{{ $t('Either the participation has already been validated, either the validation token is incorrect.') }}
</b-message>
</div>
<h1 class="title" v-else>
{{ $t('Your participation has been validated') }}
</h1>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import { IParticipant } from '@/types/event.model';
import { CONFIRM_PARTICIPATION } from '@/graphql/event';
import { confirmLocalAnonymousParticipation } from '@/services/AnonymousParticipationStorage';
@Component
export default class ConfirmParticipation extends Vue {
@Prop({ type: String, required: true }) token!: string;
loading = true;
failed = false;
async created() {
await this.validateAction();
}
async validateAction() {
try {
const { data } = await this.$apollo.mutate<{ confirmParticipation: IParticipant }>({
mutation: CONFIRM_PARTICIPATION,
variables: {
token: this.token,
},
});
if (data) {
const { confirmParticipation: participation } = data;
await confirmLocalAnonymousParticipation(participation.event.uuid);
await this.$router.replace({ name: RouteName.EVENT, params: { uuid: data.confirmParticipation.event.uuid } } );
}
} catch (err) {
console.error(err);
this.failed = true;
} finally {
this.loading = false;
}
}
}
</script>

@ -0,0 +1,82 @@
<template>
<section class="section container hero is-fullheight">
<div class="hero-body">
<div class="container">
<div class="columns">
<div class="column">
<b-button type="is-primary" size="is-medium" tag="router-link" :to="{ name: RouteName.LOGIN }">{{ $t('Login on {instance}', { instance: host }) }}</b-button>
</div>
<vertical-divider :content="$t('Or')" />
<div class="column">
<h3 class="subtitle">{{ $t('I have an account on another Mobilizon instance.')}}</h3>
<p>{{ $t('Other software may also support this.') }}</p>
<p>{{ $t('We will redirect you to your instance in order to interact with this event') }}</p>
<form @submit.prevent="redirectToInstance">
<b-field :label="$t('Your federated identity')">
<b-field>
<b-input
expanded
autocapitalize="none" autocorrect="off"
v-model="remoteActorAddress"
:placeholder="$t('profile@instance')">
</b-input>
<p class="control">
<button class="button is-primary" type="submit">{{ $t('Go') }}</button>
</p>
</b-field>
</b-field>
</form>
</div>
</div>
<div class="has-text-centered">
<b-button tag="a" type="is-text" @click="$router.go(-1)">
{{ $t('Back to previous page') }}
</b-button>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import VerticalDivider from '@/components/Utils/VerticalDivider.vue';
@Component({
components: { VerticalDivider },
})
export default class ParticipationWithAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
remoteActorAddress: string = '';
RouteName = RouteName;
get host() {
return window.location.hostname;
}
get uri(): string {
return `${window.location.origin}${this.$router.resolve({ name: RouteName.EVENT, params: { uuid: this.uuid } }).href}`;
}
async redirectToInstance() {
let res;
const [_, host] = res = this.remoteActorAddress.split('@', 2);
const remoteInteractionURI = await this.webFingerFetch(host, this.remoteActorAddress);
window.open(remoteInteractionURI);
}
private async webFingerFetch(hostname: string, identity: string): Promise<string> {
const data = await ((await fetch(`http://${hostname}/.well-known/webfinger?resource=acct:${identity}`)).json());
if (data && Array.isArray(data.links)) {
const link: { template: string } = data.links.find((link: any) => {
return link && typeof link.template === 'string' && link.rel === 'http://ostatus.org/schema/1.0/subscribe';
});
if (link && link.template.includes('{uri}')) {
return link.template.replace('{uri}', encodeURIComponent(this.uri));
}
}
throw new Error('No interaction path found in webfinger data');
}
}
</script>

@ -0,0 +1,109 @@
<template>
<section class="container section hero is-fullheight">
<div class="hero-body">
<div class="container">
<form @submit.prevent="joinEvent">
<p>{{ $t('This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.') }}</p>
<b-message type="is-info">{{ $t("Your email will only be used to confirm that you're a real person and send you eventual updates for this event. It will NOT be transmitted to other instances or to the event organizer.") }}</b-message>
<b-message type="is-danger" v-if="error">{{ error }}</b-message>
<b-field :label="$t('Email')">
<b-field>
<b-input
type="email"
v-model="anonymousParticipation.email"
placeholder="Your email"
required>
</b-input>
<p class="control">
<b-button type="is-primary" native-type="submit">{{ $t('Send email') }}</b-button>
</p>
</b-field>
</b-field>
<div class="has-text-centered">
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">
{{ $t('Back to previous page') }}
</b-button>
</div>
</form>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { EventModel, IEvent, IParticipant, ParticipantRole } from '@/types/event.model';
import { FETCH_EVENT, JOIN_EVENT } from '@/graphql/event';
import { IConfig } from '@/types/config.model';
import { CONFIG } from '@/graphql/config';
import { addLocalUnconfirmedAnonymousParticipation } from '@/services/AnonymousParticipationStorage';
import { RouteName } from '@/router';
@Component({
apollo: {
event: {
query: FETCH_EVENT,
variables() {
return {
uuid: this.uuid,
};
},
skip() { return !this.uuid; },
update: (data) => new EventModel(data.event),
},
config: CONFIG,
},
})
export default class ParticipationWithoutAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
anonymousParticipation: { email: String } = { email: '' };
event!: IEvent;
config!: IConfig;
error: String|boolean = false;
async joinEvent() {
this.error = false;
try {
const { data } = await this.$apollo.mutate<{ joinEvent: IParticipant }>({
mutation: JOIN_EVENT,
variables: {
eventId: this.event.id,
actorId: this.config.anonymous.actorId,
email: this.anonymousParticipation.email,
},
update: (store, { data }) => {
if (data == null) return;
const cachedData = store.readQuery<{ event: IEvent }>({
query: FETCH_EVENT,
variables: { uuid: this.event.uuid },
});
if (cachedData == null) return;
const { event } = cachedData;
if (event === null) {
console.error('Cannot update event participant cache, because of null value.');
return;
}
if (data.joinEvent.role === ParticipantRole.NOT_CONFIRMED) {
event.participantStats.notConfirmed = event.participantStats.notConfirmed + 1;
} else {
event.participantStats.going = event.participantStats.going + 1;
event.participantStats.participant = event.participantStats.participant + 1;
}
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: this.event.uuid }, data: { event } });
},
});
if (data && data.joinEvent.metadata.cancellationToken) {
await addLocalUnconfirmedAnonymousParticipation(this.event, data.joinEvent.metadata.cancellationToken);
return this.$router.push({ name: RouteName.EVENT, params: { uuid: this.event.uuid } });
}
} catch (e) {
console.log(JSON.stringify(e));
if (e.message === 'GraphQL error: You are already a participant of this event') {
this.error = this.$t('This email is already registered as participant for this event') as string;
}
}
}
}
</script>

@ -0,0 +1,104 @@
<template>
<section class="section container hero">
<div class="hero-body" v-if="event">
<div class="container">
<h2 class="subtitle">{{ $t('You wish to participate to the following event')}}</h2>
<EventListViewCard v-if="event" :event="event" />
<div class="columns has-text-centered">
<div class="column">
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
<figure class="image is-128x128">
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" />
</figure>
<b-button type="is-primary">{{ $t('I have a Mobilizon account') }}</b-button>
</router-link>
<p>
<small>{{ $t('Either on the {instance} instance or on another instance.', {instance: host })}}</small>
<b-tooltip type="is-dark" :label="$t('Mobilizon is a federated network. You can interact with this event from a different server.')">
<b-icon size="is-small" icon="help-circle-outline" />
</b-tooltip>
</p>
</div>
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" />
<div class="column" v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod">
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }" v-if="event.local">
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
</router-link>
<a :href="`${event.url}/participate/without-account`" v-else>
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
</a>
<p>
<small>{{ $t('Participate using your email address')}}</small><br />
<small v-if="!event.local">{{ $t('You will be redirected to the original instance')}}</small>
</p>
</div>
</div>
<div class="has-text-centered">
<b-button tag="a" type="is-text" @click="$router.go(-1)">
{{ $t('Back to previous page') }}
</b-button>
</div>
</div>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { RouteName } from '@/router';
import { FETCH_EVENT } from '@/graphql/event';
import EventListCard from '@/components/Event/EventListCard.vue';
import EventListViewCard from '@/components/Event/EventListViewCard.vue';
import { EventModel, IEvent } from '@/types/event.model';
import VerticalDivider from '@/components/Utils/VerticalDivider.vue';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
@Component({
components: { VerticalDivider, EventListViewCard, EventListCard },
apollo: {
event: {
query: FETCH_EVENT,
variables() {
return {
uuid: this.uuid,
};
},
skip() { return !this.uuid; },
update: (data) => new EventModel(data.event),
},
config: CONFIG,
},
})
export default class UnloggedParticipation extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
RouteName = RouteName;
event!: IEvent;
config!: IConfig;
get host() {
return window.location.hostname;
}
get anonymousParticipationAllowed(): boolean {
return this.event.options.anonymousParticipation;
}
get hasAnonymousEmailParticipationMethod(): boolean {
return this.config.anonymous.participation.allowed && this.config.anonymous.participation.validation.email.enabled;
}
}
</script>
<style lang="scss" scoped>
.column > a {
display: flex;
flex-direction: column;
align-items: center;
}
</style>

@ -0,0 +1,22 @@
<template>
<div class="is-divider-vertical" :data-content="dataContent"></div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class VerticalDivider extends Vue {
@Prop({ default: 'Or' }) content;
get dataContent() {
return this.content.toLocaleUpperCase();
}
}
</script>
<style lang="scss" scoped>
@import "@/variables.scss";
.is-divider-vertical[data-content]::after {
background-color: $body-background-color;
}
</style>

@ -103,3 +103,45 @@ export const REJECT_RELAY = gql`
}
${RELAY_FRAGMENT}
`;
export const ADMIN_SETTINGS_FRAGMENT = gql`
fragment adminSettingsFragment on AdminSettings {
instanceName,
instanceDescription,
instanceTerms,
instanceTermsType,
instanceTermsUrl
registrationsOpen
}
`;
export const ADMIN_SETTINGS = gql`
query {
adminSettings {
...adminSettingsFragment
}
}
${ADMIN_SETTINGS_FRAGMENT}
`;
export const SAVE_ADMIN_SETTINGS = gql`
mutation SaveAdminSettings(
$instanceName: String,
$instanceDescription: String,
$instanceTerms: String,
$instanceTermsType: InstanceTermsType,
$instanceTermsUrl: String,
$registrationsOpen: Boolean) {
saveAdminSettings(
instanceName: $instanceName,
instanceDescription: $instanceDescription,
instanceTerms: $instanceTerms,
instanceTermsType: $instanceTermsType,
instanceTermsUrl: $instanceTermsUrl,
registrationsOpen: $registrationsOpen
) {
...adminSettingsFragment
}
}
${ADMIN_SETTINGS_FRAGMENT}
`;

@ -9,6 +9,33 @@ query {
registrationsWhitelist,
demoMode,
countryCode,
anonymous {
participation {
allowed,
validation {
email {
enabled,
confirmationRequired
},
captcha {
enabled
}
}
}
eventCreation {
allowed,
validation {
email {
enabled,
confirmationRequired
},
captcha {
enabled
}
}
}
actorId
},
location {
latitude,
longitude,
@ -27,3 +54,15 @@ query {
}
}
`;
export const TERMS = gql`
query Terms($locale: String) {
config {
terms(locale: $locale) {
type,
url,
bodyHtml
}
}
}
`;

@ -14,7 +14,11 @@ const participantQuery = `
domain
},
event {
id
id,
uuid
},
metadata {
cancellationToken
}
`;
@ -41,6 +45,7 @@ const optionsQuery = `
maximumAttendeeCapacity,
remainingAttendeeCapacity,
showRemainingAttendeeCapacity,
anonymousParticipation,
showStartTime,
showEndTime,
offers {
@ -366,10 +371,11 @@ export const EDIT_EVENT = gql`
`;
export const JOIN_EVENT = gql`
mutation JoinEvent($eventId: ID!, $actorId: ID!) {
mutation JoinEvent($eventId: ID!, $actorId: ID!, $email: String) {
joinEvent(
eventId: $eventId,
actorId: $actorId
actorId: $actorId,
email: $email
) {
${participantQuery}
}
@ -377,10 +383,11 @@ export const JOIN_EVENT = gql`
`;
export const LEAVE_EVENT = gql`
mutation LeaveEvent($eventId: ID!, $actorId: ID!) {
mutation LeaveEvent($eventId: ID!, $actorId: ID!, $token: String) {
leaveEvent(
eventId: $eventId,
actorId: $actorId
actorId: $actorId,
token: $token
) {
actor {
id
@ -389,6 +396,20 @@ export const LEAVE_EVENT = gql`
}
`;
export const CONFIRM_PARTICIPATION = gql`
mutation ConfirmParticipation($token: String!) {
confirmParticipation(confirmationToken: $token) {
actor {
id,
},
event {
uuid
},
role
}
}
`;
export const UPDATE_PARTICIPANT = gql`
mutation AcceptParticipant($id: ID!, $moderatorActorId: ID!, $role: ParticipantRoleEnum!) {
updateParticipation(id: $id, moderatorActorId: $moderatorActorId, role: $role) {

@ -15,10 +15,16 @@
"Add to my calendar": "Add to my calendar",
"Add": "Add",
"Additional comments": "Additional comments",
"Admin settings successfully saved.": "Admin settings successfully saved.",
"Admin settings": "Admin settings",
"Administration": "Administration",
"All the places have already been taken": "All the places have been taken|One place is still available|{places} places are still available",
"Allow all comments": "Allow all comments",
"Allow registrations": "Allow registrations",
"An error has occurred.": "An error has occurred.",
"And no anonymous participations|And one anonymous participation|And {count} anonymous participations": "And no anonymous participations|And one anonymous participation|And {count} anonymous participations",
"Anonymous participants will be asked to confirm their participation through e-mail.": "Anonymous participants will be asked to confirm their participation through e-mail.",
"Anonymous participations": "Anonymous participations",
"Approve": "Approve",
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone.": "Are you sure you want to <b>delete</b> this comment? This action cannot be undone.",
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.": "Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.",
@ -27,8 +33,10 @@
"Are you sure you want to cancel your participation at event \"{title}\"?": "Are you sure you want to cancel your participation at event \"{title}\"?",
"Are you sure you want to delete this event? This action cannot be reverted.": "Are you sure you want to delete this event? This action cannot be reverted.",
"Avatar": "Avatar",
"Back to previous page": "Back to previous page",
"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",
"By {name}": "By {name}",
"Cancel anonymous participation": "Cancel anonymous participation",
"Cancel creation": "Cancel creation",
"Cancel edition": "Cancel edition",
"Cancel my participation request…": "Cancel my participation request…",
@ -67,10 +75,15 @@
"Create": "Create",
"Creator": "Creator",
"Current identity has been changed to {identityName} in order to manage this event.": "Current identity has been changed to {identityName} in order to manage this event.",
"Custom URL": "Custom URL",
"Custom text": "Custom text",
"Custom": "Custom",
"Dashboard": "Dashboard",
"Date and time settings": "Date and time settings",
"Date parameters": "Date parameters",
"Date": "Date",
"Default Mobilizon.org terms": "Default Mobilizon.org terms",
"Default": "Default",
"Delete Comment": "Delete Comment",
"Delete Event": "Delete Event",
"Delete event": "Delete event",
@ -90,14 +103,18 @@
"Drafts": "Drafts",
"Edit": "Edit",
"Eg: Stockholm, Dance, Chess…": "Eg: Stockholm, Dance, Chess…",
"Either on the {instance} instance or on another instance.": "Either on the {instance} instance or on another instance.",
"Either the account is already validated, either the validation token is incorrect.": "Either the account is already validated, either the validation token is incorrect.",
"Either the participation has already been validated, either the validation token is incorrect.": "Either the participation has already been validated, either the validation token is incorrect.",
"Email": "Email",
"Ends on…": "Ends on…",
"Enjoy discovering Mobilizon!": "Enjoy discovering Mobilizon!",
"Enter the link URL": "Enter the link URL",
"Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.": "Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.",
"Error while communicating with the server.": "Error while communicating with the server.",
"Error while saving report.": "Error while saving report.",
"Error while validating account": "Error while validating account",
"Error while validating participation": "Error while validating participation",
"Event already passed": "Event already passed",
"Event cancelled": "Event cancelled",
"Event creation": "Event creation",
@ -113,6 +130,7 @@
"Ex: test.mobilizon.org": "Ex: test.mobilizon.org",
"Exclude": "Exclude",
"Explore": "Explore",
"Failed to save admin settings": "Failed to save admin settings",
"Featured events": "Featured events",
"Features": "Features",
"Find an address": "Find an address",
@ -128,6 +146,7 @@
"Gather ⋅ Organize ⋅ Mobilize": "Gather ⋅ Organize ⋅ Mobilize",
"General information": "General information",
"Getting location": "Getting location",
"Go": "Go",
"Going as {name}": "Going as {name}",
"Group List": "Group List",
"Group full name": "Group full name",
@ -137,7 +156,11 @@
"Headline picture": "Headline picture",
"Hide replies": "Hide replies",
"I create an identity": "I create an identity",
"I don't have a Mobilizon account": "I don't have a Mobilizon account",
"I have a Mobilizon account": "I have a Mobilizon account",
"I have an account on another Mobilizon instance.": "I have an account on another Mobilizon instance.",
"I participate": "I participate",
"I want to allow people to participate without an account.": "I want to allow people to participate without an account.",
"I want to approve every participation request": "I want to approve every participation request",
"Identity {displayName} created": "Identity {displayName} created",
"Identity {displayName} deleted": "Identity {displayName} deleted",
@ -147,6 +170,11 @@
"Impossible to login, your email or password seems incorrect.": "Impossible to login, your email or password seems incorrect.",
"In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.": "In the meantime, please consider that the software is not (yet) finished. More information {onBlog}.",
"Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating <b>their own event platform</b>.": "Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating <b>their own event platform</b>.",
"Instance Description": "Instance Description",
"Instance Name": "Instance Name",
"Instance Terms Source": "Instance Terms Source",
"Instance Terms URL": "Instance Terms URL",
"Instance Terms": "Instance Terms",
"Instances": "Instances",
"Join {instance}, a Mobilizon instance": "Join {instance}, a Mobilizon instance",
"Last published event": "Last published event",
@ -163,10 +191,12 @@
"Log in": "Log in",
"Log out": "Log out",
"Login on Mobilizon!": "Login on Mobilizon!",
"Login on {instance}": "Login on {instance}",
"Login": "Login",
"Manage participations": "Manage participations",
"Mark as resolved": "Mark as resolved",
"Members": "Members",
"Mobilizon is a federated network. You can interact with this event from a different server.": "Mobilizon is a federated network. You can interact with this event from a different server.",
"Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.": "Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.",
"Mobilizon is under development, we will add new features to this site during regular updates, until the release of <b>version 1 of the software in the first half of 2020</b>.": "Mobilizon is under development, we will add new features to this site during regular updates, until the release of <b>version 1 of the software in the first half of 2020</b>.",
"Mobilizon’s licence": "Mobilizon’s licence",
@ -207,15 +237,18 @@
"Only alphanumeric characters and underscores are supported.": "Only alphanumeric characters and underscores are supported.",
"Open": "Open",
"Opened reports": "Opened reports",
"Or": "Or",
"Organized by {name}": "Organized by {name}",
"Organized": "Organized",
"Organizer": "Organizer",
"Other software may also support this.": "Other software may also support this.",
"Otherwise this identity will just be removed from the group administrators.": "Otherwise this identity will just be removed from the group administrators.",
"Page limited to my group (asks for auth)": "Page limited to my group (asks for auth)",
"Page not found": "Page not found",
"Participant already was rejected.": "Participant already was rejected.",
"Participant has already been approved as participant.": "Participant has already been approved as participant.",
"Participants": "Participants",
"Participate using your email address": "Participate using your email address",
"Participate": "Participate",
"Participation approval": "Participation approval",
"Participation requested!": "Participation requested!",
@ -234,6 +267,7 @@
"Post a comment": "Post a comment",
"Post a reply": "Post a reply",
"Postal Code": "Postal Code",
"Privacy Policy": "Privacy Policy",
"Private event": "Private event",
"Private feeds": "Private feeds",
"Public RSS/Atom Feed": "Public RSS/Atom Feed",
@ -245,10 +279,13 @@
"Published events": "Published events",
"RSS/Atom Feed": "RSS/Atom Feed",
"Read Framasoft’s statement of intent on the Framablog": "Read Framasoft’s statement of intent on the Framablog",
"Redirecting to event…": "Redirecting to event…",
"Region": "Region",
"Register an account on Mobilizon!": "Register an account on Mobilizon!",
"Register for an event by choosing one of your identities": "Register for an event by choosing one of your identities",
"Register": "Register",
"Registration is allowed, anyone can register.": "Registration is allowed, anyone can register.",
"Registration is closed.": "Registration is closed.",
"Registration is currently closed.": "Registration is currently closed.",
"Registrations are restricted by whitelisting.": "Registrations are restricted by whitelisting.",
"Reject": "Reject",
@ -269,15 +306,19 @@
"Resend confirmation email": "Resend confirmation email",
"Reset my password": "Reset my password",
"Resolved": "Resolved",
"Resource provided is not an URL": "Resource provided is not an URL",
"Save draft": "Save draft",
"Save": "Save",
"Search events, groups, etc.": "Search events, groups, etc.",
"Search results: \"{search}\"": "Search results: \"{search}\"",
"Search": "Search",
"Searching…": "Searching…",
"Send email": "Send email",
"Send me an email to reset my password": "Send me an email to reset my password",
"Send me the confirmation email once again": "Send me the confirmation email once again",
"Send the report": "Send the report",
"Set an URL to a page with your own terms.": "Set an URL to a page with your own terms.",
"Settings": "Settings",
"Share this event": "Share this event",
"Show map": "Show map",
"Show remaining number of places": "Show remaining number of places",
@ -289,6 +330,8 @@
"Status": "Status",
"Street": "Street",
"Tentative: Will be confirmed later": "Tentative: Will be confirmed later",
"Terms": "Terms",
"The actual number of participants may differ, as this event is hosted on another instance.": "The actual number of participants may differ, as this event is hosted on another instance.",
"The content came from another server. Transfer an anonymous copy of the report?": "The content came from another server. Transfer an anonymous copy of the report ?",
"The current identity doesn't have any permission on this event. You should probably change it.": "The current identity doesn't have any permission on this event. You should probably change it.",
"The draft event has been updated": "The draft event has been updated",
@ -302,8 +345,12 @@
"The password was successfully changed": "The password was successfully changed",
"The report will be sent to the moderators of your instance. You can explain why you report this content below.": "The report will be sent to the moderators of your instance. You can explain why you report this content below.",
"The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.": "The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.",
"The {default_terms} will be used. They will be translated in the user's language.": "The {default_terms} will be used. They will be translated in the user's language.",
"There are {participants} participants.": "There are {participants} participants.",
"These events may interest you": "These events may interest you",
"This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.": "This Mobilizon instance and this event organizer allows anonymous participations, but requires validation through email confirmation.",
"This email is already registered as participant for this event": "This email is already registered as participant for this event",
"This information is saved only on your computer. Click for details": "This information is saved only on your computer. Click for details",
"This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.": "This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.",
"This instance isn't opened to registrations, but you can register on other instances.": "This instance isn't opened to registrations, but you can register on other instances.",
"This is a demonstration site to test the beta version of Mobilizon.": "This is a demonstration site to test the beta version of Mobilizon.",
@ -315,6 +362,7 @@
"To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"",
"Transfer to {outsideDomain}": "Transfer to {outsideDomain}",
"Type": "Type",
"URL": "URL",
"Unfortunately, this instance isn't opened to registrations": "Unfortunately, this instance isn't opened to registrations",
"Unfortunately, your participation request was rejected by the organizers.": "Unfortunately, your participation request was rejected by the organizers.",
"Unknown actor": "Unknown actor",
@ -337,6 +385,7 @@
"Warning": "Warning",
"We just sent an email to {email}": "We just sent an email to {email}",