Merge remote-tracking branch 'origin/main'

This commit is contained in:
778a69cd 2023-12-14 15:06:06 +01:00
commit 7d7abd0dda
19 changed files with 1220 additions and 843 deletions

View file

@ -105,3 +105,8 @@ config :tz_world,
data_dir: System.get_env("MOBILIZON_TIMEZONES_DIR", "/var/lib/mobilizon/timezones")
config :tzdata, :data_dir, System.get_env("MOBILIZON_TZDATA_DIR", "/var/lib/mobilizon/tzdata")
config :web_push_encryption, :vapid_details,
subject: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT", nil),
public_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY", nil),
private_key: System.get_env("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY", nil)

View file

@ -85,6 +85,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
@spec build_config_cache :: map()
defp build_config_cache do
webpush_public_key =
get_in(Application.get_env(:web_push_encryption, :vapid_details), [:public_key])
%{
name: Config.instance_name(),
registrations_open: Config.instance_registrations_open?(),
@ -170,9 +173,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
enabled: Config.get([:instance, :enable_instance_feeds])
},
web_push: %{
enabled: !is_nil(Application.get_env(:web_push_encryption, :vapid_details)),
enabled: is_binary(webpush_public_key) && String.trim(webpush_public_key) != "",
public_key:
get_in(Application.get_env(:web_push_encryption, :vapid_details), [:public_key])
if(is_binary(webpush_public_key), do: String.trim(webpush_public_key), else: nil)
},
export_formats: Config.instance_export_formats(),
analytics: FrontEndAnalytics.config(),

View file

@ -5,7 +5,6 @@ defmodule Mix.Tasks.Mobilizon.WebPush.Gen.Keypair do
Taken from https://github.com/danhper/elixir-web-push-encryption/blob/8fd0f71f3222b466d389f559be9800c49f9bb641/lib/mix/tasks/web_push_gen_keypair.ex
"""
use Mix.Task
import Mix.Tasks.Mobilizon.Common, only: [mix_shell?: 0]
@shortdoc "Manages Mobilizon users"
@ -13,20 +12,28 @@ defmodule Mix.Tasks.Mobilizon.WebPush.Gen.Keypair do
def run(_) do
{public, private} = :crypto.generate_key(:ecdh, :prime256v1)
IO.puts("# Put the following in your #{file_name()} config file:")
IO.puts("Public and private VAPID keys have been generated.")
IO.puts("")
if is_nil(System.get_env("MOBILIZON_DOCKER")) do
IO.puts("# Put the following in your runtime.exs config file:")
IO.puts("")
IO.puts("config :web_push_encryption, :vapid_details,")
IO.puts(" subject: \"mailto:administrator@example.com\",")
IO.puts(" public_key: \"#{ub64(public)}\",")
IO.puts(" private_key: \"#{ub64(private)}\"")
IO.puts("")
else
IO.puts("# Set the following environment variables in your .env file:")
IO.puts("")
IO.puts("MOBILIZON_WEB_PUSH_ENCRYPTION_SUBJECT=\"mailto:administrator@example.com\"")
IO.puts("MOBILIZON_WEB_PUSH_ENCRYPTION_PUBLIC_KEY=\"#{ub64(public)}\"")
IO.puts("MOBILIZON_WEB_PUSH_ENCRYPTION_PRIVATE_KEY=\"#{ub64(private)}\"")
IO.puts("")
end
end
defp ub64(value) do
Base.url_encode64(value, padding: false)
end
defp file_name do
if mix_shell?(), do: "runtime.exs", else: "config.exs"
end
end

1760
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -70,6 +70,7 @@ import { CONFIG } from "@/graphql/config";
import { IConfig } from "@/types/config.model";
import { useRouter } from "vue-router";
import RouteName from "@/router/name";
import { useLazyCurrentUserIdentities } from "./composition/apollo/actor";
const { result: configResult } = useQuery<{ config: IConfig }>(
CONFIG,
@ -138,11 +139,15 @@ interval.value = window.setInterval(async () => {
}
}, 60000) as unknown as number;
const { load: loadIdentities } = useLazyCurrentUserIdentities();
onBeforeMount(async () => {
console.debug("Before mount App");
if (initializeCurrentUser()) {
try {
await initializeCurrentActor();
const result = await loadIdentities();
if (!result) return;
await initializeCurrentActor(result.loggedUser.actors);
} catch (err) {
if (err instanceof NoIdentitiesException) {
await router.push({
@ -223,7 +228,7 @@ const initializeCurrentUser = () => {
console.debug("Initialized current user", userData);
return true;
}
console.debug("Failed to initialize current user");
console.debug("We don't seem to have a currently logged-in user");
return false;
};

View file

@ -9,7 +9,12 @@ let isRefreshing = false;
let pendingRequests: any[] = [];
const resolvePendingRequests = () => {
pendingRequests.map((callback) => callback());
console.debug("resolving pending requests");
pendingRequests.map((callback) => {
console.debug("calling callback", callback);
return callback();
});
console.debug("emptying pending requests after resolving them all");
pendingRequests = [];
};
@ -21,7 +26,23 @@ const isAuthError = (graphQLError: GraphQLError | undefined) => {
const errorLink = onError(
({ graphQLErrors, networkError, forward, operation }) => {
console.debug("We have an apollo error", [graphQLErrors, networkError]);
if (graphQLErrors) {
graphQLErrors.map(
(graphQLError: GraphQLError & { status_code?: number }) => {
if (graphQLError?.status_code !== 401) {
console.debug(
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
);
}
}
);
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
console.debug(JSON.stringify(networkError));
}
if (
graphQLErrors?.some((graphQLError) => isAuthError(graphQLError)) ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -67,6 +88,9 @@ const errorLink = onError(
})
).filter((value) => Boolean(value));
} else {
console.debug(
"Skipping refreshing as isRefreshing is already to true, adding requests to pending"
);
forwardOperation = fromPromise(
new Promise((resolve) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -78,23 +102,6 @@ const errorLink = onError(
return forwardOperation.flatMap(() => forward(operation));
}
if (graphQLErrors) {
graphQLErrors.map(
(graphQLError: GraphQLError & { status_code?: number }) => {
if (graphQLError?.status_code !== 401) {
console.debug(
`[GraphQL error]: Message: ${graphQLError.message}, Location: ${graphQLError.locations}, Path: ${graphQLError.path}`
);
}
}
);
}
if (networkError) {
console.error(`[Network error]: ${networkError}`);
console.debug(JSON.stringify(networkError));
}
}
);

View file

@ -34,7 +34,7 @@ if (!import.meta.env.VITE_HISTOIRE_ENV) {
const retryLink = new RetryLink();
export const fullLink = authMiddleware
.concat(retryLink)
export const fullLink = retryLink
.concat(errorLink)
.concat(authMiddleware)
.concat(link ?? uploadLink);

View file

@ -26,13 +26,13 @@ body {
@apply opacity-50 cursor-not-allowed;
}
.btn-danger {
@apply bg-mbz-danger hover:bg-mbz-danger/90;
@apply border-2 bg-mbz-danger hover:bg-mbz-danger/90 text-white;
}
.btn-success {
@apply bg-mbz-success;
@apply border-2 bg-mbz-success text-white;
}
.btn-warning {
@apply bg-mbz-warning text-black hover:bg-mbz-warning/90 hover:text-slate-800;
@apply border-2 bg-mbz-warning text-black hover:bg-mbz-warning/90 hover:text-slate-800;
}
.btn-text {
@apply bg-transparent border-transparent text-black dark:text-white font-normal underline hover:bg-zinc-200 hover:text-black;
@ -42,13 +42,13 @@ body {
@apply bg-transparent text-black dark:text-white font-semibold py-2 px-4 border border-mbz-bluegreen dark:border-violet-3;
}
.btn-outlined-success {
@apply border-2 border-mbz-success bg-transparent text-mbz-success hover:bg-mbz-success;
@apply border-2 border-mbz-success bg-transparent text-mbz-success hover:bg-mbz-success hover:text-white;
}
.btn-outlined-warning {
@apply bg-transparent border dark:text-white hover:dark:text-slate-900 hover:bg-mbz-warning border-mbz-warning;
}
.btn-outlined-danger {
@apply border-2 bg-transparent border-mbz-danger text-mbz-danger hover:bg-mbz-danger;
@apply border-2 bg-transparent border-mbz-danger text-mbz-danger hover:bg-mbz-danger hover:text-white;
}
.btn-outlined-text {
@apply bg-transparent hover:text-slate-900;
@ -97,6 +97,15 @@ body {
.input {
@apply appearance-none box-border rounded border w-full py-2 px-3 text-black leading-tight dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50;
}
.input-size-small {
@apply text-sm;
}
.input-size-medium {
@apply text-base;
}
.input-size-large {
@apply text-xl;
}
.input-danger {
@apply border-red-500;
}
@ -205,7 +214,7 @@ body {
/* Select */
.select {
@apply dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50 rounded pl-2 pr-8 border-2 border-transparent h-10 shadow-none border rounded w-full;
@apply dark:bg-zinc-600 dark:placeholder:text-zinc-400 dark:text-zinc-50 pl-2 pr-8 border-2 border-transparent h-10 shadow-none rounded w-full;
}
/* Radio */

View file

@ -36,7 +36,7 @@
keypath="{instanceName} is an instance of {mobilizon_link}, a free software built with the community."
>
<template #instanceName>
<b>{{ config?.name }}</b>
<b>{{ instanceName }}</b>
</template>
<template #mobilizon_link>
<a href="https://joinmobilizon.org">{{ t("Mobilizon") }}</a>
@ -57,7 +57,10 @@
}}
</span>
</p>
<SentryFeedback />
<SentryFeedback
v-if="sentryProvider"
:providerConfig="sentryProvider"
/>
<p class="prose dark:prose-invert" v-if="!sentryEnabled">
{{
@ -84,7 +87,7 @@
<div class="buttons" v-if="!sentryEnabled">
<o-tooltip
:label="tooltipConfig.label"
:type="tooltipConfig.type"
:variant="tooltipConfig.variant"
:active="copied !== false"
always
>
@ -101,12 +104,13 @@
</template>
<script lang="ts" setup>
import { checkProviderConfig } from "@/services/statistics";
import { IAnalyticsConfig } from "@/types/config.model";
import { IAnalyticsConfig, IConfig } from "@/types/config.model";
import { computed, defineAsyncComponent, ref } from "vue";
import { useQueryLoading } from "@vue/apollo-composable";
import { useQuery, useQueryLoading } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n";
import { useHead } from "@unhead/vue";
import { useAnalytics } from "@/composition/apollo/config";
import { INSTANCE_NAME } from "@/graphql/config";
const SentryFeedback = defineAsyncComponent(
() => import("./Feedback/SentryFeedback.vue")
);
@ -126,6 +130,12 @@ useHead({
title: computed(() => t("Error")),
});
const { result: instanceConfig } = useQuery<{ config: Pick<IConfig, "name"> }>(
INSTANCE_NAME
);
const instanceName = computed(() => instanceConfig.value?.config.name);
const copyErrorToClipboard = async (): Promise<void> => {
try {
if (window.isSecureContext && navigator.clipboard) {

View file

@ -100,13 +100,12 @@
<span v-if="event.participantStats.notApproved > 0">
<o-button
variant="text"
@click="
gotToWithCheck(participation, {
tag="router-link"
:to="{
name: RouteName.PARTICIPATIONS,
query: { role: ParticipantRole.NOT_APPROVED },
params: { eventId: event.uuid },
})
"
}"
>
{{
$t(

View file

@ -273,6 +273,28 @@
</div>
</o-dropdown-item>
<o-dropdown-item
aria-role="listitem"
has-link
v-if="
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<router-link
class="flex gap-1"
:to="{
name: RouteName.ANNOUNCEMENTS,
params: { eventId: participation.event?.uuid },
}"
>
<Bullhorn />
{{ t("Announcements") }}
</router-link>
</o-dropdown-item>
<o-dropdown-item aria-role="listitem">
<router-link
class="flex gap-1"
@ -302,7 +324,6 @@ import {
organizerDisplayName,
} from "@/types/event.model";
import { displayNameAndUsername, IPerson } from "@/types/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import RouteName from "@/router/name";
import { changeIdentity } from "@/utils/identity";
import LazyImageWrapper from "@/components/Image/LazyImageWrapper.vue";
@ -318,12 +339,14 @@ import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
import Video from "vue-material-design-icons/Video.vue";
import { useProgrammatic } from "@oruga-ui/oruga-next";
import { computed, inject } from "vue";
import { useQuery } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n";
import { Dialog } from "@/plugins/dialog";
import { Snackbar } from "@/plugins/snackbar";
import { useDeleteEvent } from "@/composition/apollo/event";
import Tag from "@/components/TagElement.vue";
import { escapeHtml } from "@/utils/html";
import Bullhorn from "vue-material-design-icons/Bullhorn.vue";
import { useCurrentActorClient } from "@/composition/apollo/actor";
const props = defineProps<{
participation: IParticipant;
@ -332,8 +355,7 @@ const props = defineProps<{
const emit = defineEmits(["eventDeleted"]);
const { result: currentActorResult } = useQuery(CURRENT_ACTOR_CLIENT);
const currentActor = computed(() => currentActorResult.value?.currentActor);
const { currentActor } = useCurrentActorClient();
const { t } = useI18n({ useScope: "global" });
const dialog = inject<Dialog>("dialog");
@ -365,7 +387,7 @@ const openDeleteEventModal = (
)}
<br><br>
${t('To confirm, type your event title "{eventTitle}"', {
eventTitle: event.title,
eventTitle: escapeHtml(event.title),
})}`,
confirmText: t("Delete {eventTitle}", {
eventTitle: event.title,
@ -374,6 +396,7 @@ const openDeleteEventModal = (
placeholder: event.title,
pattern: escapeRegExp(event.title),
},
hasInput: true,
onConfirm: () => callback(event),
});
};
@ -432,7 +455,7 @@ const gotToWithCheck = async (
route: RouteLocationRaw
): Promise<any> => {
if (
participation.actor.id !== currentActor.value.id &&
participation.actor.id !== currentActor.value?.id &&
participation.event.organizerActor
) {
const organizerActor = participation.event.organizerActor as IPerson;

View file

@ -8,7 +8,7 @@
drag-drop
>
<div
class="w-100 rounded text-center p-4 rounded-xl border-dashed border-2 border-gray-600"
class="w-100 text-center p-4 rounded-xl border-dashed border-2 border-gray-600"
>
<span class="mx-auto flex w-fit">
<Upload />

View file

@ -6,7 +6,7 @@ import {
} from "@/graphql/actor";
import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { useQuery } from "@vue/apollo-composable";
import { useLazyQuery, useQuery } from "@vue/apollo-composable";
import { computed, Ref, unref } from "vue";
import { useCurrentUserClient } from "./user";
@ -22,6 +22,12 @@ export function useCurrentActorClient() {
return { currentActor, error, loading };
}
export function useLazyCurrentUserIdentities() {
return useLazyQuery<{
loggedUser: Pick<ICurrentUser, "actors">;
}>(IDENTITIES);
}
export function useCurrentUserIdentities() {
const { currentUser } = useCurrentUserClient();

View file

@ -453,7 +453,7 @@ export const DELETE_PERSON = gql`
* Prefer CREATE_PERSON when creating another identity
*/
export const REGISTER_PERSON = gql`
mutation (
mutation RegisterPerson(
$preferredUsername: String!
$name: String!
$summary: String!

View file

@ -1,4 +1,4 @@
import { provide, createApp, h, computed, ref } from "vue";
import { provide, createApp, h, ref } from "vue";
import VueScrollTo from "vue-scrollto";
// import VueAnnouncer from "@vue-a11y/announcer";
// import VueSkipTo from "@vue-a11y/skip-to";
@ -59,11 +59,7 @@ apolloClient
instanceName.value = configData.config?.name;
});
const head = createHead({
titleTemplate: computed(() =>
instanceName.value ? `%s | ${instanceName.value}` : "%s"
).value,
});
const head = createHead();
app.use(head);
app.mount("#app");

View file

@ -1,14 +1,8 @@
import { AUTH_USER_ACTOR_ID } from "@/constants";
import { UPDATE_CURRENT_ACTOR_CLIENT, IDENTITIES } from "@/graphql/actor";
import { UPDATE_CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { apolloClient } from "@/vue-apollo";
import {
provideApolloClient,
useLazyQuery,
useMutation,
} from "@vue/apollo-composable";
import { computed } from "vue";
import { provideApolloClient, useMutation } from "@vue/apollo-composable";
export class NoIdentitiesException extends Error {}
@ -38,38 +32,31 @@ export async function changeIdentity(identity: IPerson): Promise<void> {
});
}
const { load: loadIdentities } = provideApolloClient(apolloClient)(() =>
useLazyQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>(IDENTITIES)
);
/**
* We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache
* the current identity used
*/
export async function initializeCurrentActor(): Promise<void> {
export async function initializeCurrentActor(
identities: IPerson[] | undefined
): Promise<void> {
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
console.debug("Initializing current actor", actorId);
try {
const result = await loadIdentities();
if (!result) return;
if (!identities) {
console.debug("Failed to load user's identities", identities);
return;
}
console.debug("got identities", result);
const identities = computed(() => result.loggedUser?.actors);
console.debug(
"initializing current actor based on identities",
identities.value
);
if (identities.value && identities.value.length < 1) {
if (identities && identities.length < 1) {
console.warn("Logged user has no identities!");
throw new NoIdentitiesException();
}
const activeIdentity =
(identities.value || []).find(
(identities || []).find(
(identity: IPerson | undefined) => identity?.id === actorId
) || ((identities.value || [])[0] as IPerson);
) || ((identities || [])[0] as IPerson);
if (activeIdentity) {
await changeIdentity(activeIdentity);

View file

@ -33,6 +33,7 @@
</li>
</breadcrumbs-nav>
<DraggableList
v-if="resource.actor"
:resources="resource.children.elements"
:isRoot="resource.path === '/'"
:group="resource.actor"
@ -98,20 +99,34 @@
v-model:active="createResourceModal"
has-modal-card
:close-button-aria-label="t('Close')"
trap-focus
:autoFocus="false"
>
<section class="w-full md:w-[640px]">
<o-notification variant="danger" v-if="modalError">
{{ modalError }}
</o-notification>
<form @submit.prevent="createResource">
<p v-if="newResource.type !== 'folder'">
<p v-if="newResource.type === 'pad'">
{{
t("The pad will be created on {service}", {
service: newResourceHost,
})
}}
</p>
<p v-else-if="newResource.type === 'calc'">
{{
t("The calc will be created on {service}", {
service: newResourceHost,
})
}}
</p>
<p v-else-if="newResource.type === 'visio'">
{{
t("The videoconference will be created on {service}", {
service: newResourceHost,
})
}}
</p>
<o-field :label="t('Title')" label-for="new-resource-title">
<o-input
ref="modalNewResourceInput"
@ -132,7 +147,7 @@
has-modal-card
aria-modal
:close-button-aria-label="t('Close')"
trap-focus
:autoFocus="false"
:width="640"
>
<div class="w-full md:w-[640px]">
@ -298,8 +313,12 @@ const modalError = ref("");
const modalFieldErrors: Record<string, string> = reactive({});
const resourceRenameInput = ref<any>();
const modalNewResourceInput = ref<HTMLElement>();
const modalNewResourceLinkInput = ref<HTMLElement>();
const modalNewResourceInput = ref<{
$refs: { inputRef: HTMLInputElement };
} | null>();
const modalNewResourceLinkInput = ref<{
$refs: { inputRef: HTMLInputElement };
} | null>();
const actualPath = computed((): string => {
const path = Array.isArray(props.path) ? props.path.join("/") : props.path;
@ -419,14 +438,14 @@ const createSentenceForType = (type: string): string => {
const createLinkModal = async (): Promise<void> => {
createLinkResourceModal.value = true;
await nextTick();
modalNewResourceLinkInput.value?.focus();
modalNewResourceLinkInput.value?.$refs.inputRef?.focus();
};
const createFolderModal = async (): Promise<void> => {
newResource.type = "folder";
createResourceModal.value = true;
await nextTick();
modalNewResourceInput.value?.focus();
modalNewResourceInput.value?.$refs.inputRef?.focus();
};
const createResourceFromProvider = async (
@ -436,7 +455,7 @@ const createResourceFromProvider = async (
newResource.type = provider.software;
createResourceModal.value = true;
await nextTick();
modalNewResourceInput.value?.focus();
modalNewResourceInput.value?.$refs.inputRef?.focus();
};
const generateFullResourceUrl = (provider: IProvider): string => {

View file

@ -143,6 +143,7 @@ import { LoginError, LoginErrorCode } from "@/types/enums";
import { useCurrentUserClient } from "@/composition/apollo/user";
import { useHead } from "@unhead/vue";
import { enumTransformer, useRouteQuery } from "vue-use-route-query";
import { useLazyCurrentUserIdentities } from "@/composition/apollo/actor";
const { t } = useI18n({ useScope: "global" });
const router = useRouter();
@ -235,12 +236,17 @@ const loginAction = (e: Event) => {
});
};
const { load: loadIdentities } = useLazyCurrentUserIdentities();
const { onDone: onCurrentUserMutationDone, mutate: updateCurrentUserMutation } =
useMutation(UPDATE_CURRENT_USER_CLIENT);
onCurrentUserMutationDone(async () => {
console.debug("Current user mutation done, now setuping actors…");
try {
await initializeCurrentActor();
const result = await loadIdentities();
if (!result) return;
await initializeCurrentActor(result.loggedUser.actors);
} catch (err: any) {
if (err instanceof NoIdentitiesException && currentUser.value) {
await router.push({
@ -257,6 +263,7 @@ onCurrentUserMutationDone(async () => {
});
const setupClientUserAndActors = async (login: ILogin): Promise<void> => {
console.debug("Setuping client user and actors");
updateCurrentUserMutation({
id: login.user.id,
email: credentials.email,
@ -298,7 +305,7 @@ onMounted(() => {
if (currentUser.value?.isLoggedIn) {
console.debug(
"Current user is already logged-in, redirecting to Homepage",
currentUser
currentUser.value
);
router.push("/");
}

View file

@ -26,15 +26,19 @@
required
type="email"
v-model="emailValue"
expanded
/>
</o-field>
<p class="control">
<p class="my-4 flex gap-2">
<o-button variant="primary" native-type="submit">
{{ t("Submit") }}
</o-button>
<router-link :to="{ name: RouteName.LOGIN }" class="button is-text">{{
t("Cancel")
}}</router-link>
<o-button
tag="router-link"
:to="{ name: RouteName.LOGIN }"
variant="text"
>{{ t("Cancel") }}</o-button
>
</p>
</form>
<div v-else>