Merge branch 'app-tokens' into 'main'

Application tokens

Closes #67

See merge request framasoft/mobilizon!1361
This commit is contained in:
Thomas Citharel 2023-03-24 15:02:33 +00:00
commit 1a82cb95bc
158 changed files with 13482 additions and 2040 deletions

View file

@ -39,6 +39,9 @@
# to `false` below: # to `false` below:
# #
color: true, color: true,
plugins: [
{CredoCodeClimate, []}
],
# #
# You can customize the parameters of any check by adding a second element # You can customize the parameters of any check by adding a second element
# to the tuple. # to the tuple.

View file

@ -1,4 +1,5 @@
[ [
import_deps: [:ecto, :ecto_sql, :phoenix],
plugins: [Phoenix.LiveView.HTMLFormatter], plugins: [Phoenix.LiveView.HTMLFormatter],
inputs: ["{mix,.formatter}.exs", "{config,lib,test,priv}/**/*.{ex,exs,heex}"] inputs: ["{mix,.formatter}.exs", "{config,lib,test,priv}/**/*.{ex,exs,heex}"]
] ]

1
.gitignore vendored
View file

@ -49,3 +49,4 @@ test-junit-report.xml
js/junit.xml js/junit.xml
.env .env
demo/ demo/
codeclimate.json

View file

@ -67,6 +67,10 @@ lint-elixir:
- mix credo diff --from-git-merge-base $TARGET_SHA1 --strict -a || export EXITVALUE=1 - mix credo diff --from-git-merge-base $TARGET_SHA1 --strict -a || export EXITVALUE=1
- mix sobelow --config || export EXITVALUE=1 - mix sobelow --config || export EXITVALUE=1
- exit $EXITVALUE - exit $EXITVALUE
artifacts:
reports:
codequality: codeclimate.json
lint-front: lint-front:
image: node:16 image: node:16

View file

@ -14,3 +14,30 @@ C042E87389F7BDCFF4E076E95731AE69
C42BFAEF7100F57BED75998B217C857A C42BFAEF7100F57BED75998B217C857A
D11958E86F1B6D37EF656B63405CA8A4 D11958E86F1B6D37EF656B63405CA8A4
F16F054F2628609A726B9FF2F089D484 F16F054F2628609A726B9FF2F089D484
26E816A7B054CB0347A2C6451F03B92B
2B76BDDB2BB4D36D69FAE793EBD63894
301A837DE24C6AEE1DA812DF9E5486C1
395A2740CB468F93F6EBE6E90EE08291
4013C9866943B9381D9F9F97027F88A9
4C796DD588A4B1C98E86BBCD0349949A
51289D8D7BDB59CB6473E0DED0591ED7
5A70DC86895DB3610C605EA9F31ED300
705C17F9C852F546D886B20DB2C4D0D1
75D2074B6F771BA8C032008EC18CABDF
7B1C6E35A374C38FF5F07DBF23B3EAE2
955ACF52ADD8FCAA450FB8138CB1FD1A
A092A563729E1F2C1C8D5D809A31F754
BFA12FDEDEAD7DEAB6D44DF6FDFBD5E1
D9A08930F140F9BA494BB90B3F812C87
FE1EEB91EA633570F703B251AE2D4D4E
02B15A0FE85181E2470E4E1E6740DFF6
128653EA565172F81FD177D1D6491CF3
2EB031217231C480C89EA0C1576EF3CA
39CFFBCF3FD4F6DB0E4DE4A9A78D3961
40C6EAD7C05ABB6A85BB904589DEF72F
49DE9560D506F9E7EF3AFD8DA6E5564B
759F752FA0768CCC7871895DC2A5CD51
7EEC79571F3F7CEEB04A8B86D908382A
E7967805C1EA5301F2722C7BDB2F25F3
BDFB0FB1AAF69C18212CBCFD42F8B717
40220A533CCACB3A1CE9DBF1A8A430A1

View file

@ -312,11 +312,18 @@ config :mobilizon, Oban,
{"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background}, {"@hourly", Mobilizon.Service.Workers.CleanUnconfirmedUsersWorker, queue: :background},
{"@hourly", Mobilizon.Service.Workers.ExportCleanerWorker, queue: :background}, {"@hourly", Mobilizon.Service.Workers.ExportCleanerWorker, queue: :background},
{"@hourly", Mobilizon.Service.Workers.SendActivityRecapWorker, queue: :notifications}, {"@hourly", Mobilizon.Service.Workers.SendActivityRecapWorker, queue: :notifications},
{"@daily", Mobilizon.Service.Workers.CleanOldActivityWorker, queue: :background} {"@daily", Mobilizon.Service.Workers.CleanOldActivityWorker, queue: :background},
{"@hourly", Mobilizon.Service.Workers.CleanApplicationData,
queue: :background, args: %{type: :application_token}},
{"@hourly", Mobilizon.Service.Workers.CleanApplicationData,
queue: :background, args: %{type: :application_device_activation}}
]}, ]},
{Oban.Plugins.Pruner, max_age: 300} {Oban.Plugins.Pruner, max_age: 300}
] ]
config :hammer,
backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}
config :mobilizon, :rich_media, config :mobilizon, :rich_media,
parsers: [ parsers: [
Mobilizon.Service.RichMedia.Parsers.OEmbed, Mobilizon.Service.RichMedia.Parsers.OEmbed,

View file

@ -78,7 +78,7 @@ config :tesla, Mobilizon.Service.HTTP.HostMetaClient,
config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock config :mobilizon, Mobilizon.Service.Geospatial, service: Mobilizon.Service.Geospatial.Mock
config :mobilizon, Oban, queues: false, plugins: false config :mobilizon, Oban, testing: :manual
config :mobilizon, Mobilizon.Web.Auth.Guardian, secret_key: "some secret" config :mobilizon, Mobilizon.Web.Auth.Guardian, secret_key: "some secret"

View file

@ -45,11 +45,12 @@
"@tiptap/extension-strike": "^2.0.0-beta.26", "@tiptap/extension-strike": "^2.0.0-beta.26",
"@tiptap/extension-text": "^2.0.0-beta.15", "@tiptap/extension-text": "^2.0.0-beta.15",
"@tiptap/extension-underline": "^2.0.0-beta.7", "@tiptap/extension-underline": "^2.0.0-beta.7",
"@tiptap/pm": "^2.0.0-beta.220",
"@tiptap/suggestion": "^2.0.0-beta.195", "@tiptap/suggestion": "^2.0.0-beta.195",
"@tiptap/vue-3": "^2.0.0-beta.96", "@tiptap/vue-3": "^2.0.0-beta.96",
"@vue-a11y/announcer": "^2.1.0", "@vue-a11y/announcer": "^2.1.0",
"@vue-a11y/skip-to": "^2.1.2", "@vue-a11y/skip-to": "^2.1.2",
"@vue-leaflet/vue-leaflet": "^0.8.0", "@vue-leaflet/vue-leaflet": "^0.9.0",
"@vue/apollo-composable": "^4.0.0-beta.1", "@vue/apollo-composable": "^4.0.0-beta.1",
"@vue/compiler-sfc": "^3.2.37", "@vue/compiler-sfc": "^3.2.37",
"@vueuse/core": "^9.1.0", "@vueuse/core": "^9.1.0",
@ -59,7 +60,7 @@
"autoprefixer": "^10", "autoprefixer": "^10",
"blurhash": "^2.0.0", "blurhash": "^2.0.0",
"date-fns": "^2.16.0", "date-fns": "^2.16.0",
"date-fns-tz": "^1.1.6", "date-fns-tz": "^2.0.0",
"floating-vue": "^2.0.0-beta.17", "floating-vue": "^2.0.0-beta.17",
"graphql": "^15.8.0", "graphql": "^15.8.0",
"graphql-tag": "^2.10.3", "graphql-tag": "^2.10.3",
@ -74,16 +75,6 @@
"p-debounce": "^4.0.0", "p-debounce": "^4.0.0",
"phoenix": "^1.6", "phoenix": "^1.6",
"postcss": "^8", "postcss": "^8",
"prosemirror-commands": "^1.5.0",
"prosemirror-dropcursor": "^1.6.1",
"prosemirror-gapcursor": "^1.3.1",
"prosemirror-history": "^1.3.0",
"prosemirror-keymap": "^1.2.0",
"prosemirror-model": "^1.19.0",
"prosemirror-schema-list": "^1.2.2",
"prosemirror-state": "^1.4.2",
"prosemirror-transform": "^1.7.1",
"prosemirror-view": "^1.30.0",
"register-service-worker": "^1.7.2", "register-service-worker": "^1.7.2",
"sanitize-html": "^2.5.3", "sanitize-html": "^2.5.3",
"tailwindcss": "^3", "tailwindcss": "^3",
@ -100,7 +91,7 @@
"zhyswan-vuedraggable": "^4.1.3" "zhyswan-vuedraggable": "^4.1.3"
}, },
"devDependencies": { "devDependencies": {
"@histoire/plugin-vue": "^0.12.4", "@histoire/plugin-vue": "^0.15.8",
"@playwright/test": "^1.25.1", "@playwright/test": "^1.25.1",
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.1.4",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
@ -114,8 +105,8 @@
"@types/phoenix": "^1.5.2", "@types/phoenix": "^1.5.2",
"@types/sanitize-html": "^2.5.0", "@types/sanitize-html": "^2.5.0",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^4.0.0",
"@vitest/coverage-c8": "^0.28.2", "@vitest/coverage-c8": "^0.29.2",
"@vitest/ui": "^0.28.2", "@vitest/ui": "^0.29.2",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^11.0.0",
"@vue/test-utils": "^2.0.2", "@vue/test-utils": "^2.0.2",
@ -125,17 +116,17 @@
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.3.0",
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"histoire": "^0.12.4", "histoire": "^0.15.8",
"jsdom": "^21.1.0", "jsdom": "^21.1.0",
"mock-apollo-client": "^1.1.0", "mock-apollo-client": "^1.1.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"prettier-eslint": "^15.0.1", "prettier-eslint": "^15.0.1",
"rollup-plugin-visualizer": "^5.7.1", "rollup-plugin-visualizer": "^5.7.1",
"sass": "^1.34.1", "sass": "^1.34.1",
"typescript": "~4.9.4", "typescript": "~5.0.0",
"vite": "^4.0.4", "vite": "^4.0.4",
"vite-plugin-pwa": "^0.14.1", "vite-plugin-pwa": "^0.14.1",
"vitest": "^0.28.2", "vitest": "^0.29.2",
"vue-i18n-extract": "^2.0.4" "vue-i18n-extract": "^2.0.4"
} }
} }

View file

@ -65,18 +65,21 @@
</o-field> </o-field>
<o-modal <o-modal
has-modal-card has-modal-card
v-model="showNewElementModal" v-model:active="showNewElementModal"
:close-button-aria-label="$t('Close')" :close-button-aria-label="$t('Close')"
> >
<div class="modal-card"> <div class="">
<header class="modal-card-head"> <header class="">
<button <h2>{{ t("Create a new metadata element") }}</h2>
type="button" <p>
class="delete" {{
@click="showNewElementModal = false" t(
/> "You can put any arbitrary content in this element. URLs will be clickable."
)
}}
</p>
</header> </header>
<div class="modal-card-body"> <div class="">
<form @submit="addNewElement"> <form @submit="addNewElement">
<o-field :label="$t('Element title')"> <o-field :label="$t('Element title')">
<o-input v-model="newElement.title" /> <o-input v-model="newElement.title" />
@ -84,7 +87,7 @@
<o-field :label="$t('Element value')"> <o-field :label="$t('Element value')">
<o-input v-model="newElement.value" /> <o-input v-model="newElement.value" />
</o-field> </o-field>
<o-button variant="primary" native-type="submit">{{ <o-button class="mt-2" variant="primary" native-type="submit">{{
$t("Add") $t("Add")
}}</o-button> }}</o-button>
</form> </form>

View file

@ -70,7 +70,9 @@ function setupApp({ app }) {
new Promise((resolve) => new Promise((resolve) =>
resolve({ resolve({
data: { data: {
identities: [{ id: "9", preferredUsername: "sam", name: "Samuel" }], loggedUser: {
actors: [{ id: "9", preferredUsername: "sam", name: "Samuel" }],
},
}, },
}) })
) )

View file

@ -44,5 +44,7 @@ import GroupSection from "@/components/Group/GroupSection.vue";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
defineProps<{ group: IGroup }>(); defineProps<{
group: Pick<IGroup, "preferredUsername" | "domain" | "discussions">;
}>();
</script> </script>

View file

@ -56,5 +56,7 @@ import GroupSection from "@/components/Group/GroupSection.vue";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
defineProps<{ group: IGroup }>(); defineProps<{
group: Pick<IGroup, "preferredUsername" | "domain" | "resources">;
}>();
</script> </script>

View file

@ -0,0 +1,164 @@
<template>
<div>
<h1 class="text-3xl">
{{ t("Autorize this application to access your account?") }}
</h1>
<div class="rounded-lg bg-white dark:bg-zinc-900 shadow-xl my-6">
<div class="p-4 pb-0">
<p class="text-3xl font-bold">{{ authApplication.name }}</p>
<p>{{ authApplication.website }}</p>
</div>
<p class="p-4">
{{
t(
"You'll be able to revoke access for this application in your account settings."
)
}}
</p>
<div class="">
<div
v-if="collapses.length === 0"
class="rounded-lg bg-mbz-danger shadow-xl my-6 p-4 flex items-center gap-2"
>
<AlertCircle :size="42" />
<p>
{{
t(
"This application didn't ask for known permissions. It's likely the request is incorrect."
)
}}
</p>
</div>
<p v-else class="px-4 font-bold">
{{ t("This application asks for the following permissions:") }}
</p>
<o-collapse
class="mt-3 border-b pb-2 border-zinc-700 text-black dark:text-white"
:class="{
'bg-mbz-warning dark:!text-black': collapse?.type === 'warning',
}"
animation="slide"
v-for="(collapse, index) of collapses"
:key="index"
:open="isOpen === index"
@open="isOpen = index"
>
<template #trigger="props">
<div class="flex py-1" role="button">
<o-icon :icon="collapse.icon" class="px-2" />
<p class="font-bold text-lg p-2 flex-1">
{{ collapse.title }}
</p>
<a
class="flex items-center cursor-pointer p-3 justify-center self-end"
>
<o-icon :icon="props.open ? 'chevron-up' : 'chevron-down'">
</o-icon>
</a>
</div>
</template>
<div class="p-2">
<div class="content">
{{ collapse.text }}
</div>
</div>
</o-collapse>
</div>
<div class="flex gap-3 p-4">
<o-button
:disabled="collapses.length === 0"
@click="() => authorize()"
>{{ t("Authorize") }}</o-button
>
<o-button outlined tag="router-link" :to="{ name: RouteName.HOME }">{{
t("Decline")
}}</o-button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useHead } from "@vueuse/head";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useMutation } from "@vue/apollo-composable";
import { AUTORIZE_APPLICATION } from "@/graphql/application";
import RouteName from "@/router/name";
import { IApplication } from "@/types/application.model";
import { scope } from "./scopes";
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
const { t } = useI18n({ useScope: "global" });
const props = defineProps<{
authApplication: IApplication;
redirectURI?: string | null;
state?: string | null;
scope?: string | null;
}>();
const isOpen = ref<number>(-1);
const collapses = computed(() =>
(props.scope ?? "")
.split(" ")
.map((localScope) => scope[localScope])
.filter((localScope) => localScope)
);
const { mutate: authorizeMutation, onDone: onAuthorizeMutationDone } =
useMutation<
{
authorizeApplication: {
code: string;
state: string;
clientId: string;
scope: string;
};
},
{
applicationClientId: string;
redirectURI: string;
state?: string | null;
scope?: string | null;
}
>(AUTORIZE_APPLICATION);
const authorize = () => {
authorizeMutation({
applicationClientId: props.authApplication.clientId,
redirectURI: props.redirectURI as string,
state: props.state,
scope: props.scope,
});
};
onAuthorizeMutationDone(({ data }) => {
const code = data?.authorizeApplication?.code;
const localClientId = data?.authorizeApplication?.clientId;
const localScope = data?.authorizeApplication?.scope;
const returnedState = data?.authorizeApplication?.state ?? "";
if (!code || !localClientId || !localScope) return;
if (props.redirectURI) {
const params = new URLSearchParams(
Object.entries({
code,
state: returnedState,
client_id: localClientId,
scope: localScope,
})
);
window.location.assign(
new URL(`${props.redirectURI}?${params.toString()}`)
);
}
});
useHead({
title: computed(() => t("Authorize application")),
});
</script>

View file

@ -0,0 +1,283 @@
import { i18n } from "@/utils/i18n";
const t = i18n.global.t;
export const scope: Record<
string,
{ title: string; type?: "warning"; text: string; icon?: string }
> = {
read: {
title: t("Read all of your account's data"),
type: "warning",
text: t(
"This application will be allowed to see all of your events organized, the events you participate to, as well as every data from your groups."
),
icon: "eye-outline",
},
write: {
title: t("Modify all of your account's data"),
text: t(
"This application will be allowed to publish and manage events on your behalf, post and manage comments, participate to events, manage all of your groups, including group events, resources, posts and discussions. It will also be allowed to manage your account and profile settings."
),
type: "warning",
icon: "pencil-outline",
},
"write:event:create": {
title: t("Publish events"),
text: t(
"This application will be allowed to publish events on your behalf"
),
icon: "calendar",
},
"write:event:update": {
title: t("Update events"),
text: t("This application will be allowed to update events on your behalf"),
icon: "calendar",
},
"write:event:delete": {
title: t("Delete events"),
text: t("This application will be allowed to delete events on your behalf"),
icon: "calendar",
},
"write:media:upload": {
title: t("Upload media"),
text: t("This application will be allowed to upload media on your behalf"),
icon: "image",
},
"write:media:remove": {
title: t("Remove uploaded media"),
text: t(
"This application will be allowed to remove uploaded media on your behalf"
),
icon: "image",
},
"write:group:post:create": {
title: t("Publish group posts"),
text: t(
"This application will be allowed to publish group posts on your behalf"
),
icon: "bullhorn",
},
"write:group:post:update": {
title: t("Update group posts"),
text: t(
"This application will be allowed to update group posts on your behalf"
),
icon: "bullhorn",
},
"write:group:post:delete": {
title: t("Delete group posts"),
text: t(
"This application will be allowed to delete group posts on your behalf"
),
icon: "bullhorn",
},
"read:group:resources": {
title: t("Access your group's resources"),
text: t(
"This application will be allowed to access all of the groups you're a member of on your behalf"
),
icon: "link",
},
"write:group:resources:create": {
title: t("Create group resources"),
text: t(
"This application will be allowed to create resources in all of the groups you're a member of on your behalf"
),
icon: "link",
},
"write:group:resources:update": {
title: t("Update group resources"),
text: t(
"This application will be allowed to update resources in all of the groups you're a member of on your behalf"
),
icon: "link",
},
"write:group:resources:delete": {
title: t("Delete group resources"),
text: t(
"This application will be allowed to delete resources in all of the groups you're a member of on your behalf"
),
icon: "link",
},
"read:group:events": {
title: t("Access group events"),
text: t(
"This application will be allowed to list and access group events in all of the groups you're a member of on your behalf"
),
icon: "calendar",
},
"read:group:discussions": {
title: t("Access group discussions"),
text: t(
"This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf"
),
icon: "chat",
},
"read:group:members": {
title: t("Access group members"),
text: t(
"This application will be allowed to list group members in all of the groups you're a member of on your behalf"
),
icon: "account-circle",
},
"read:group:followers": {
title: t("Access group followers"),
text: t(
"This application will be allowed to list group followers in all of the groups you're a member of on your behalf"
),
icon: "account-circle",
},
"read:group:activities": {
title: t("Access group activities"),
text: t(
"This application will be allowed to access group activities in all of the groups you're a member of on your behalf"
),
icon: "timeline-text",
},
"read:group:todo_lists": {
title: t("Access group todo-lists"),
text: t(
"This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf"
),
icon: "checkbox-marked",
},
"write:group:group_membership": {
title: t("Manage group memberships"),
text: t(
"This application will be allowed to join and leave groups on your behalf"
),
icon: "account-circle",
},
"write:group:members": {
title: t("Manage group members"),
text: t(
"This application will be allowed to manage group members in all of the groups you're a member of on your behalf"
),
icon: "account-circle",
},
"read:profile:organized_events": {
title: t("Access organized events"),
text: t(
"This application will be allowed to list and view your organized events"
),
icon: "calendar",
},
"read:profile:participations": {
title: t("Access participations"),
text: t(
"This application will be allowed to list and view the events you're participating to"
),
icon: "account-circle",
},
"read:profile:memberships": {
title: t("Access memberships"),
text: t(
"This application will be allowed to list and view the groups you're a member of"
),
icon: "account-circle",
},
"read:profile:follows": {
title: t("Access followed groups"),
text: t(
"This application will be allowed to list and view the groups you're following"
),
icon: "account-circle",
},
"write:profile:create": {
title: t("Create new profiles"),
text: t(
"This application will be allowed to create new profiles for your account on your behalf"
),
icon: "account-circle",
},
"write:profile:update": {
title: t("Update profiles"),
text: t(
"This application will be allowed to update your profiles on your behalf"
),
icon: "account-circle",
},
"write:profile:delete": {
title: t("Delete profiles"),
text: t(
"This application will be allowed to delete your profiles on your behalf"
),
icon: "account-circle",
},
"write:comment:create": {
title: t("Post comments"),
text: t("This application will be allowed to post comments on your behalf"),
icon: "comment",
},
"write:comment:update": {
title: t("Update comments"),
text: t(
"This application will be allowed to update comments on your behalf"
),
icon: "comment",
},
"write:comment:delete": {
title: t("Delete comments"),
text: t(
"This application will be allowed to delete comments on your behalf"
),
icon: "comment",
},
"write:group:discussion:create": {
title: t("Create group discussions"),
text: t(
"This application will be allowed to create group discussions on your behalf"
),
icon: "comment",
},
"write:group:discussion:update": {
title: t("Update group discussions"),
text: t(
"This application will be allowed to update group discussions on your behalf"
),
icon: "comment",
},
"write:group:discussion:delete": {
title: t("Delete group discussions"),
text: t(
"This application will be allowed to delete group discussions on your behalf"
),
icon: "comment",
},
"write:profile:feed_token:create": {
title: t("Create feed tokens"),
text: t(
"This application will be allowed to create feed tokens on your behalf"
),
icon: "rss",
},
"write:feed_token:delete": {
title: t("Delete feed tokens"),
text: t(
"This application will be allowed to delete feed tokens on your behalf"
),
icon: "rss",
},
"write:participation": {
title: t("Manage event participations"),
text: t(
"This application will be allowed to manage events participations on your behalf"
),
icon: "rss",
},
"write:user:setting:activity": {
title: t("Manage activity settings"),
text: t(
"This application will be allowed to manage your account activity settings"
),
icon: "cog",
},
"write:user:setting:push": {
title: t("Manage push notification settings"),
text: t(
"This application will be allowed to manage your account push notification settings"
),
icon: "cog",
},
};

View file

@ -46,7 +46,7 @@ import { useRouter } from "vue-router";
import Draggable from "zhyswan-vuedraggable"; import Draggable from "zhyswan-vuedraggable";
import { IResource } from "@/types/resource"; import { IResource } from "@/types/resource";
import RouteName from "@/router/name"; import RouteName from "@/router/name";
import { IGroup, usernameWithDomain } from "@/types/actor"; import { IMinimalActor, usernameWithDomain } from "@/types/actor";
import ResourceDropdown from "./ResourceDropdown.vue"; import ResourceDropdown from "./ResourceDropdown.vue";
import { UPDATE_RESOURCE } from "@/graphql/resources"; import { UPDATE_RESOURCE } from "@/graphql/resources";
import { inject, ref } from "vue"; import { inject, ref } from "vue";
@ -59,7 +59,7 @@ import { Snackbar } from "@/plugins/snackbar";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
resource: IResource; resource: IResource;
group: IGroup; group: IMinimalActor;
inline?: boolean; inline?: boolean;
}>(), }>(),
{ inline: false } { inline: false }

View file

@ -17,6 +17,10 @@
:title="t('Notifications')" :title="t('Notifications')"
:to="{ name: RouteName.NOTIFICATIONS }" :to="{ name: RouteName.NOTIFICATIONS }"
/> />
<SettingMenuItem
:title="t('Apps')"
:to="{ name: RouteName.AUTHORIZED_APPS }"
/>
</SettingMenuSection> </SettingMenuSection>
<SettingMenuSection <SettingMenuSection
:title="t('Profiles')" :title="t('Profiles')"

View file

@ -250,6 +250,14 @@ const icons: Record<string, () => Promise<any>> = {
), ),
ExitToApp: () => ExitToApp: () =>
import(`../../../node_modules/vue-material-design-icons/ExitToApp.vue`), import(`../../../node_modules/vue-material-design-icons/ExitToApp.vue`),
CheckboxMarked: () =>
import(
`../../../node_modules/vue-material-design-icons/CheckboxMarked.vue`
),
EyeOutline: () =>
import(`../../../node_modules/vue-material-design-icons/EyeOutline.vue`),
PencilOutline: () =>
import(`../../../node_modules/vue-material-design-icons/PencilOutline.vue`),
}; };
const props = withDefaults( const props = withDefaults(

View file

@ -5,6 +5,7 @@ import {
PERSON_STATUS_GROUP, PERSON_STATUS_GROUP,
} from "@/graphql/actor"; } from "@/graphql/actor";
import { IPerson } from "@/types/actor"; import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { useQuery } from "@vue/apollo-composable"; import { useQuery } from "@vue/apollo-composable";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { useCurrentUserClient } from "./user"; import { useCurrentUserClient } from "./user";
@ -24,18 +25,16 @@ export function useCurrentActorClient() {
export function useCurrentUserIdentities() { export function useCurrentUserIdentities() {
const { currentUser } = useCurrentUserClient(); const { currentUser } = useCurrentUserClient();
const { result, error, loading } = useQuery<{ identities: IPerson[] }>( const { result, error, loading } = useQuery<{
IDENTITIES, loggedUser: Pick<ICurrentUser, "actors">;
{}, }>(IDENTITIES, {}, () => ({
() => ({ enabled:
enabled: currentUser.value?.id !== undefined &&
currentUser.value?.id !== undefined && currentUser.value?.id !== null &&
currentUser.value?.id !== null && currentUser.value?.isLoggedIn === true,
currentUser.value?.isLoggedIn === true, }));
})
);
const identities = computed(() => result.value?.identities); const identities = computed(() => result.value?.loggedUser?.actors);
return { identities, error, loading }; return { identities, error, loading };
} }

View file

@ -215,12 +215,14 @@ export function useRegistrationConfig() {
}>(REGISTRATIONS, undefined, { fetchPolicy: "cache-only" }); }>(REGISTRATIONS, undefined, { fetchPolicy: "cache-only" });
const registrationsOpen = computed( const registrationsOpen = computed(
() => result.value?.config.registrationsOpen () => result.value?.config?.registrationsOpen
); );
const registrationsAllowlist = computed( const registrationsAllowlist = computed(
() => result.value?.config.registrationsAllowlist () => result.value?.config?.registrationsAllowlist
);
const databaseLogin = computed(
() => result.value?.config?.auth?.databaseLogin
); );
const databaseLogin = computed(() => result.value?.config.auth.databaseLogin);
return { return {
registrationsOpen, registrationsOpen,
registrationsAllowlist, registrationsAllowlist,

View file

@ -0,0 +1,44 @@
import { useQuery } from "@vue/apollo-composable";
import { computed, unref } from "vue";
import { useCurrentUserClient } from "./user";
import type { Ref } from "vue";
import { IGroup } from "@/types/actor";
import { GROUP_DISCUSSIONS_LIST } from "@/graphql/discussion";
export function useGroupDiscussionsList(
name: string | undefined | Ref<string | undefined>,
options?: {
discussionsPage?: number;
discussionsLimit?: number;
}
) {
const { currentUser } = useCurrentUserClient();
const { result, error, loading, onResult, onError, refetch } = useQuery<
{
group: Pick<
IGroup,
"id" | "preferredUsername" | "name" | "domain" | "discussions"
>;
},
{
name: string;
discussionsPage?: number;
discussionsLimit?: number;
}
>(
GROUP_DISCUSSIONS_LIST,
() => ({
name: unref(name),
...options,
}),
() => ({
enabled:
unref(name) !== undefined &&
unref(name) !== "" &&
currentUser.value?.isLoggedIn,
fetchPolicy: "cache-and-network",
})
);
const group = computed(() => result.value?.group);
return { group, error, loading, onResult, onError, refetch };
}

View file

@ -2,7 +2,7 @@ import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { import {
CREATE_GROUP, CREATE_GROUP,
DELETE_GROUP, DELETE_GROUP,
FETCH_GROUP, FETCH_GROUP_PUBLIC,
LEAVE_GROUP, LEAVE_GROUP,
UPDATE_GROUP, UPDATE_GROUP,
} from "@/graphql/group"; } from "@/graphql/group";
@ -50,7 +50,7 @@ export function useGroup(
discussionsLimit?: number; discussionsLimit?: number;
} }
>( >(
FETCH_GROUP, FETCH_GROUP_PUBLIC,
() => ({ () => ({
name: unref(name), name: unref(name),
...options, ...options,

View file

@ -0,0 +1,44 @@
import { useQuery } from "@vue/apollo-composable";
import { computed, unref } from "vue";
import { useCurrentUserClient } from "./user";
import type { Ref } from "vue";
import { IGroup } from "@/types/actor";
import { GROUP_RESOURCES_LIST } from "@/graphql/resources";
export function useGroupResourcesList(
name: string | undefined | Ref<string | undefined>,
options?: {
resourcesPage?: number;
resourcesLimit?: number;
}
) {
const { currentUser } = useCurrentUserClient();
const { result, error, loading, onResult, onError, refetch } = useQuery<
{
group: Pick<
IGroup,
"id" | "preferredUsername" | "name" | "domain" | "resources"
>;
},
{
name: string;
resourcesPage?: number;
resourcesLimit?: number;
}
>(
GROUP_RESOURCES_LIST,
() => ({
name: unref(name),
...options,
}),
() => ({
enabled:
unref(name) !== undefined &&
unref(name) !== "" &&
currentUser.value?.isLoggedIn,
fetchPolicy: "cache-and-network",
})
);
const group = computed(() => result.value?.group);
return { group, error, loading, onResult, onError, refetch };
}

View file

@ -18,12 +18,13 @@ export function useCurrentUserClient() {
result: currentUserResult, result: currentUserResult,
error, error,
loading, loading,
onResult,
} = useQuery<{ } = useQuery<{
currentUser: ICurrentUser; currentUser: ICurrentUser;
}>(CURRENT_USER_CLIENT); }>(CURRENT_USER_CLIENT);
const currentUser = computed(() => currentUserResult.value?.currentUser); const currentUser = computed(() => currentUserResult.value?.currentUser);
return { currentUser, error, loading }; return { currentUser, error, loading, onResult };
} }
export function useLoggedUser() { export function useLoggedUser() {
@ -82,11 +83,13 @@ export function registerAccount() {
{ context } { context }
) => { ) => {
if (context?.userAlreadyActivated) { if (context?.userAlreadyActivated) {
const identitiesData = store.readQuery<{ identities: IPerson[] }>({ const currentUserData = store.readQuery<{
loggedUser: Pick<ICurrentUser, "actors">;
}>({
query: IDENTITIES, query: IDENTITIES,
}); });
if (identitiesData && localData) { if (currentUserData && localData) {
const newPersonData = { const newPersonData = {
...localData.registerPerson, ...localData.registerPerson,
type: ActorType.PERSON, type: ActorType.PERSON,
@ -95,8 +98,8 @@ export function registerAccount() {
store.writeQuery({ store.writeQuery({
query: IDENTITIES, query: IDENTITIES,
data: { data: {
...identitiesData, ...currentUserData.loggedUser,
identities: [...identitiesData.identities, newPersonData], actors: [[...currentUserData.loggedUser.actors, newPersonData]],
}, },
}); });
} }

View file

@ -282,8 +282,11 @@ export const LOGGED_USER_MEMBERSHIPS = gql`
export const IDENTITIES = gql` export const IDENTITIES = gql`
query Identities { query Identities {
identities { loggedUser {
...ActorFragment id
actors {
...ActorFragment
}
} }
} }
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}

View file

@ -0,0 +1,88 @@
import gql from "graphql-tag";
export const AUTH_APPLICATION = gql`
query AuthApplication($clientId: String!) {
authApplication(clientId: $clientId) {
id
clientId
name
website
}
}
`;
export const AUTORIZE_APPLICATION = gql`
mutation AuthorizeApplication(
$applicationClientId: String!
$redirectURI: String!
$state: String
$scope: String!
) {
authorizeApplication(
clientId: $applicationClientId
redirectURI: $redirectURI
state: $state
scope: $scope
) {
code
state
clientId
scope
}
}
`;
export const AUTORIZE_DEVICE_APPLICATION = gql`
mutation AuthorizeDeviceApplication(
$applicationClientId: String!
$userCode: String
) {
authorizeDeviceApplication(
clientId: $applicationClientId
userCode: $userCode
) {
clientId
scope
}
}
`;
export const AUTH_AUTHORIZED_APPLICATIONS = gql`
query AuthAuthorizedApplications {
loggedUser {
id
authAuthorizedApplications {
id
application {
name
website
}
lastUsedAt
insertedAt
}
}
}
`;
export const REVOKED_AUTHORIZED_APPLICATION = gql`
mutation RevokeApplicationToken($appTokenId: String!) {
revokeApplicationToken(appTokenId: $appTokenId) {
id
}
}
`;
export const DEVICE_ACTIVATION = gql`
mutation DeviceActivation($userCode: String!) {
deviceActivation(userCode: $userCode) {
id
application {
id
clientId
name
website
}
scope
}
}
`;

View file

@ -72,6 +72,7 @@ export const CONFIG = gql`
features { features {
groups groups
eventCreation eventCreation
antispam
} }
restrictions { restrictions {
onlyAdminCanCreateGroups onlyAdminCanCreateGroups
@ -112,6 +113,9 @@ export const CONFIG = gql`
isDefault isDefault
} }
} }
exportFormats {
eventParticipants
}
} }
} }
`; `;

View file

@ -148,3 +148,25 @@ export const DISCUSSION_COMMENT_CHANGED = gql`
} }
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
`; `;
export const GROUP_DISCUSSIONS_LIST = gql`
query GroupDiscussionsList(
$name: String!
$discussionsPage: Int
$discussionsLimit: Int
) {
group(preferredUsername: $name) {
id
preferredUsername
name
domain
discussions(page: $discussionsPage, limit: $discussionsLimit) {
total
elements {
...DiscussionBasicFields
}
}
}
}
${DISCUSSION_BASIC_FIELDS_FRAGMENT}
`;

View file

@ -54,8 +54,8 @@ export const LIST_GROUPS = gql`
${ACTOR_FRAGMENT} ${ACTOR_FRAGMENT}
`; `;
export const GROUP_FIELDS_FRAGMENTS = gql` export const GROUP_BASIC_FIELDS_FRAGMENTS = gql`
fragment GroupFullFields on Group { fragment GroupBasicFields on Group {
...ActorFragment ...ActorFragment
suspended suspended
visibility visibility
@ -137,18 +137,23 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
} }
total total
} }
discussions(page: $discussionsPage, limit: $discussionsLimit) {
total
elements {
...DiscussionBasicFields
}
}
posts(page: $postsPage, limit: $postsLimit) { posts(page: $postsPage, limit: $postsLimit) {
total total
elements { elements {
...PostBasicFields ...PostBasicFields
} }
} }
}
${ACTOR_FRAGMENT}
${ADDRESS_FRAGMENT}
${EVENT_OPTIONS_FRAGMENT}
${TAG_FRAGMENT}
${POST_BASIC_FIELDS}
`;
export const GROUP_FIELDS_FRAGMENTS = gql`
fragment GroupFullFields on Group {
...GroupBasicFields
members(page: $membersPage, limit: $membersLimit) { members(page: $membersPage, limit: $membersLimit) {
elements { elements {
id id
@ -196,14 +201,13 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
total total
} }
} }
${ACTOR_FRAGMENT} ${GROUP_BASIC_FIELDS_FRAGMENTS}
${ADDRESS_FRAGMENT} ${DISCUSSION_BASIC_FIELDS_FRAGMENT}
${EVENT_OPTIONS_FRAGMENT} ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
${TAG_FRAGMENT}
`; `;
export const FETCH_GROUP = gql` export const FETCH_GROUP_PUBLIC = gql`
query FetchGroup( query FetchGroupPublic(
$name: String! $name: String!
$afterDateTime: DateTime $afterDateTime: DateTime
$beforeDateTime: DateTime $beforeDateTime: DateTime
@ -211,28 +215,12 @@ export const FETCH_GROUP = gql`
$organisedEventsLimit: Int $organisedEventsLimit: Int
$postsPage: Int $postsPage: Int
$postsLimit: Int $postsLimit: Int
$membersPage: Int
$membersLimit: Int
$discussionsPage: Int
$discussionsLimit: Int
) { ) {
group(preferredUsername: $name) { group(preferredUsername: $name) {
...GroupFullFields ...GroupBasicFields
} }
} }
${GROUP_FIELDS_FRAGMENTS} ${GROUP_BASIC_FIELDS_FRAGMENTS}
${DISCUSSION_BASIC_FIELDS_FRAGMENT}
${POST_BASIC_FIELDS}
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`;
export const FETCH_GROUP_BY_ID = gql`
query FetchGroupById($id: ID!) {
groupById(id: $name) {
...GroupFullFields
}
}
${GROUP_FIELDS_FRAGMENTS}
`; `;
export const GET_GROUP = gql` export const GET_GROUP = gql`
@ -407,6 +395,7 @@ export const GROUP_TIMELINE = gql`
openness openness
physicalAddress { physicalAddress {
id id
originId
} }
banner { banner {
id id

View file

@ -161,3 +161,34 @@ export const PREVIEW_RESOURCE_LINK = gql`
} }
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT} ${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`; `;
export const GROUP_RESOURCES_LIST = gql`
query GroupResourcesList(
$name: String!
$resourcesPage: Int
$resourcesLimit: Int
) {
group(preferredUsername: $name) {
id
preferredUsername
name
domain
resources(page: $resourcesPage, limit: $resourcesLimit) {
elements {
id
title
resourceUrl
summary
updatedAt
type
path
metadata {
...ResourceMetadataBasicFields
}
}
total
}
}
}
${RESOURCE_METADATA_BASIC_FIELDS_FRAGMENT}
`;

View file

@ -1453,5 +1453,105 @@
"Report as ham": "Report as ham", "Report as ham": "Report as ham",
"Report as undetected spam": "Report as undetected spam", "Report as undetected spam": "Report as undetected spam",
"The report contents (eventual comments and event) and the reported profile details will be transmitted to Akismet.": "The report contents (eventual comments and event) and the reported profile details will be transmitted to Akismet.", "The report contents (eventual comments and event) and the reported profile details will be transmitted to Akismet.": "The report contents (eventual comments and event) and the reported profile details will be transmitted to Akismet.",
"Submit to Akismet": "Submit to Akismet" "Submit to Akismet": "Submit to Akismet",
"Autorize this application to access your account?": "Autorize this application to access your account?",
"This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust.": "This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust.",
"Authorize application": "Authorize application",
"Authorize": "Authorize",
"You'll be able to revoke access for this application in your account settings.": "You'll be able to revoke access for this application in your account settings.",
"Read all of your account's data": "Read all of your account's data",
"This application will be allowed to see all of your events organized, the events you participate to, …": "",
"Modify all of your account's data": "Modify all of your account's data",
"This application will be allowed to publish events on your behalf, participate to events": "",
"Publish events": "Publish events",
"This application will be allowed to publish events on your behalf": "This application will be allowed to publish events on your behalf",
"Update events": "Update events",
"This application will be allowed to update events on your behalf": "This application will be allowed to update events on your behalf",
"Delete events": "Delete events",
"This application will be allowed to delete events on your behalf": "This application will be allowed to delete events on your behalf",
"Upload media": "Upload media",
"This application will be allowed to upload media on your behalf": "This application will be allowed to upload media on your behalf",
"Remove uploaded media": "Remove uploaded media",
"This application will be allowed to remove uploaded media on your behalf": "This application will be allowed to remove uploaded media on your behalf",
"Publish group posts": "Publish group posts",
"This application will be allowed to publish group posts on your behalf": "This application will be allowed to publish group posts on your behalf",
"Update group posts": "Update group posts",
"This application will be allowed to update group posts on your behalf": "This application will be allowed to update group posts on your behalf",
"Delete group posts": "Delete group posts",
"This application will be allowed to delete group posts on your behalf": "This application will be allowed to delete group posts on your behalf",
"Access your group's resources": "Access your group's resources",
"This application will be allowed to access all of the groups you're a member of on your behalf": "This application will be allowed to access all of the groups you're a member of on your behalf",
"Create group resources": "Create group resources",
"This application will be allowed to create resources in all of the groups you're a member of on your behalf": "This application will be allowed to create resources in all of the groups you're a member of on your behalf",
"Update group resources": "Update group resources",
"This application will be allowed to update resources in all of the groups you're a member of on your behalf": "This application will be allowed to update resources in all of the groups you're a member of on your behalf",
"Delete group resources": "Delete group resources",
"This application will be allowed to delete resources in all of the groups you're a member of on your behalf": "This application will be allowed to delete resources in all of the groups you're a member of on your behalf",
"Access group events": "Access group events",
"This application will be allowed to list and access group events in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group events in all of the groups you're a member of on your behalf",
"Access group discussions": "Access group discussions",
"This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf",
"Access group members": "Access group members",
"This application will be allowed to list group members in all of the groups you're a member of on your behalf": "This application will be allowed to list group members in all of the groups you're a member of on your behalf",
"Access group followers": "Access group followers",
"This application will be allowed to list group followers in all of the groups you're a member of on your behalf": "This application will be allowed to list group followers in all of the groups you're a member of on your behalf",
"Access group activities": "Access group activities",
"This application will be allowed to access group activities in all of the groups you're a member of on your behalf": "This application will be allowed to access group activities in all of the groups you're a member of on your behalf",
"Access group todo-lists": "Access group todo-lists",
"This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf",
"Manage group memberships": "Manage group memberships",
"This application will be allowed to join and leave groups on your behalf": "This application will be allowed to join and leave groups on your behalf",
"Manage group members": "Manage group members",
"This application will be allowed to manage group members in all of the groups you're a member of on your behalf": "This application will be allowed to manage group members in all of the groups you're a member of on your behalf",
"Access organized events": "Access organized events",
"This application will be allowed to list and view your organized events": "This application will be allowed to list and view your organized events",
"Access participations": "Access participations",
"This application will be allowed to list and view the events you're participating to": "This application will be allowed to list and view the events you're participating to",
"Access memberships": "Access memberships",
"This application will be allowed to list and view the groups you're a member of": "This application will be allowed to list and view the groups you're a member of",
"Access followed groups": "Access followed groups",
"This application will be allowed to list and view the groups you're following": "This application will be allowed to list and view the groups you're following",
"Create new profiles": "Create new profiles",
"This application will be allowed to create new profiles for your account on your behalf": "This application will be allowed to create new profiles for your account on your behalf",
"Update profiles": "Update profiles",
"This application will be allowed to update your profiles on your behalf": "This application will be allowed to update your profiles on your behalf",
"Delete profiles": "Delete profiles",
"This application will be allowed to delete your profiles on your behalf": "This application will be allowed to delete your profiles on your behalf",
"Post comments": "Post comments",
"This application will be allowed to post comments on your behalf": "This application will be allowed to post comments on your behalf",
"Update comments": "Update comments",
"This application will be allowed to update comments on your behalf": "This application will be allowed to update comments on your behalf",
"Delete comments": "Delete comments",
"This application will be allowed to delete comments on your behalf": "This application will be allowed to delete comments on your behalf",
"Create group discussions": "Create group discussions",
"This application will be allowed to create group discussions on your behalf": "This application will be allowed to create group discussions on your behalf",
"Update group discussions": "Update group discussions",
"This application will be allowed to update group discussions on your behalf": "This application will be allowed to update group discussions on your behalf",
"Delete group discussions": "Delete group discussions",
"This application will be allowed to delete group discussions on your behalf": "This application will be allowed to delete group discussions on your behalf",
"Create feed tokens": "Create feed tokens",
"This application will be allowed to create feed tokens on your behalf": "This application will be allowed to create feed tokens on your behalf",
"Delete feed tokens": "Delete feed tokens",
"This application will be allowed to delete feed tokens on your behalf": "This application will be allowed to delete feed tokens on your behalf",
"Manage event participations": "Manage event participations",
"This application will be allowed to manage events participations on your behalf": "This application will be allowed to manage events participations on your behalf",
"Manage activity settings": "Manage activity settings",
"This application will be allowed to manage your account activity settings": "This application will be allowed to manage your account activity settings",
"Manage push notification settings": "Manage push notification settings",
"This application will be allowed to manage your account push notification settings": "This application will be allowed to manage your account push notification settings",
"Apps": "Apps",
"Device activation": "Device activation",
"Application not found": "Application not found",
"The provided application was not found.": "The provided application was not found.",
"Your application code": "Your application code",
"You need to provide the following code to your application": "You need to provide the following code to your application",
"Enter the code displayed on your device": "Enter the code displayed on your device",
"Continue": "Continue",
"The device code is incorrect or no longer valid.": "The device code is incorrect or no longer valid.",
"These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.": "These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.",
"Last used on {last_used_date}": "Last used on {last_used_date}",
"Never used": "Never used",
"Authorized on {authorization_date}": "Authorized on {authorization_date}",
"Revoke": "Revoke",
"Application was revoked": "Application was revoked"
} }

View file

@ -1451,5 +1451,103 @@
"{username} was invited to {group}": "{username} a été invité à {group}", "{username} was invited to {group}": "{username} a été invité à {group}",
"{user}'s follow request was accepted": "La demande de suivi de {user} a été acceptée", "{user}'s follow request was accepted": "La demande de suivi de {user} a été acceptée",
"{user}'s follow request was rejected": "La demande de suivi de {user} a été rejetée", "{user}'s follow request was rejected": "La demande de suivi de {user} a été rejetée",
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap" "© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
"Autorize this application to access your account?": "Autoriser cette application à accéder à votre compte ?",
"This application will be able to access all of your informations and post content on your behalf. Make sure you only approve applications you trust.": "Cette application sera capable d'accéder à toutes vos informations et poster du contenu en votre nom. Assurez-vous d'approuver uniquement des applications en lesquelles vous avez confiance.",
"Authorize application": "Autoriser l'application",
"Authorize": "Autoriser",
"You'll be able to revoke access for this application in your account settings.": "Vous pourrez révoquer l'accès pour cette applications dans les paramètres de votre compte.",
"Read all of your account's data": "Lire toutes les données de votre compte",
"Modify all of your account's data": "Modifier toutes les données de votre compte",
"Publish events": "Publier des événements",
"This application will be allowed to publish events on your behalf": "Cette application pourra publier des événements en votre nom",
"Update events": "Update events",
"This application will be allowed to update events on your behalf": "Cette application pourra mettre à jour des événements en votre nom",
"Delete events": "Delete events",
"This application will be allowed to delete events on your behalf": "Cette application pourra supprimer des événements en votre nom",
"Upload media": "Upload media",
"This application will be allowed to upload media on your behalf": "Cette application pourra téléverser des médias en votre nom",
"Remove uploaded media": "Remove uploaded media",
"This application will be allowed to remove uploaded media on your behalf": "Cette application pourra supprimer des médias téléversés en votre nom",
"Publish group posts": "Publish group posts",
"This application will be allowed to publish group posts on your behalf": "Cette application pourra publier des billets de groupes en votre nom",
"Update group posts": "Update group posts",
"This application will be allowed to update group posts on your behalf": "Cette application pourra mettre à jour des billets de groupes en votre nom",
"Delete group posts": "Delete group posts",
"This application will be allowed to delete group posts on your behalf": "Cette application pourra supprimer des billets de groupes en votre nom",
"Access your group's resources": "Access your group's resources",
"This application will be allowed to access all of the groups you're a member of on your behalf": "Cette application pourra accéder à tous les groupes dont vous êtes membres",
"Create group resources": "Create group resources",
"This application will be allowed to create resources in all of the groups you're a member of on your behalf": "Cette application pourra créer des ressources dans chacun des groupes dont vous êtes membre en votre nom",
"Update group resources": "Update group resources",
"This application will be allowed to update resources in all of the groups you're a member of on your behalf": "Cette application pourra mettre à jour des ressources dans chacun des groupes dont vous êtes membre en votre nom",
"Delete group resources": "Delete group resources",
"This application will be allowed to delete resources in all of the groups you're a member of on your behalf": "Cette application pourra supprimer des ressources dans chacun des groupes dont vous êtes membre en votre nom",
"Access group events": "Access group events",
"This application will be allowed to list and access group events in all of the groups you're a member of on your behalf": "Cette application pourra lister et accéder aux événements des groupes dont vous êtes membre",
"Access group discussions": "Access group discussions",
"This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group discussions in all of the groups you're a member of on your behalf",
"Access group members": "Access group members",
"This application will be allowed to list group members in all of the groups you're a member of on your behalf": "This application will be allowed to list group members in all of the groups you're a member of on your behalf",
"Access group followers": "Access group followers",
"This application will be allowed to list group followers in all of the groups you're a member of on your behalf": "This application will be allowed to list group followers in all of the groups you're a member of on your behalf",
"Access group activities": "Access group activities",
"This application will be allowed to access group activities in all of the groups you're a member of on your behalf": "This application will be allowed to access group activities in all of the groups you're a member of on your behalf",
"Access group todo-lists": "Access group todo-lists",
"This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf": "This application will be allowed to list and access group todo-lists in all of the groups you're a member of on your behalf",
"Manage group memberships": "Manage group memberships",
"This application will be allowed to join and leave groups on your behalf": "This application will be allowed to join and leave groups on your behalf",
"Manage group members": "Manage group members",
"This application will be allowed to manage group members in all of the groups you're a member of on your behalf": "This application will be allowed to manage group members in all of the groups you're a member of on your behalf",
"Access organized events": "Access organized events",
"This application will be allowed to list and view your organized events": "This application will be allowed to list and view your organized events",
"Access participations": "Access participations",
"This application will be allowed to list and view the events you're participating to": "This application will be allowed to list and view the events you're participating to",
"Access memberships": "Access memberships",
"This application will be allowed to list and view the groups you're a member of": "This application will be allowed to list and view the groups you're a member of",
"Access followed groups": "Access followed groups",
"This application will be allowed to list and view the groups you're following": "This application will be allowed to list and view the groups you're following",
"Create new profiles": "Create new profiles",
"This application will be allowed to create new profiles for your account on your behalf": "This application will be allowed to create new profiles for your account on your behalf",
"Update profiles": "Update profiles",
"This application will be allowed to update your profiles on your behalf": "This application will be allowed to update your profiles on your behalf",
"Delete profiles": "Delete profiles",
"This application will be allowed to delete your profiles on your behalf": "This application will be allowed to delete your profiles on your behalf",
"Post comments": "Post comments",
"This application will be allowed to post comments on your behalf": "This application will be allowed to post comments on your behalf",
"Update comments": "Update comments",
"This application will be allowed to update comments on your behalf": "This application will be allowed to update comments on your behalf",
"Delete comments": "Delete comments",
"This application will be allowed to delete comments on your behalf": "This application will be allowed to delete comments on your behalf",
"Create group discussions": "Create group discussions",
"This application will be allowed to create group discussions on your behalf": "This application will be allowed to create group discussions on your behalf",
"Update group discussions": "Update group discussions",
"This application will be allowed to update group discussions on your behalf": "This application will be allowed to update group discussions on your behalf",
"Delete group discussions": "Delete group discussions",
"This application will be allowed to delete group discussions on your behalf": "This application will be allowed to delete group discussions on your behalf",
"Create feed tokens": "Create feed tokens",
"This application will be allowed to create feed tokens on your behalf": "This application will be allowed to create feed tokens on your behalf",
"Delete feed tokens": "Delete feed tokens",
"This application will be allowed to delete feed tokens on your behalf": "This application will be allowed to delete feed tokens on your behalf",
"Manage event participations": "Manage event participations",
"This application will be allowed to manage events participations on your behalf": "This application will be allowed to manage events participations on your behalf",
"Manage activity settings": "Manage activity settings",
"This application will be allowed to manage your account activity settings": "This application will be allowed to manage your account activity settings",
"Manage push notification settings": "Manage push notification settings",
"This application will be allowed to manage your account push notification settings": "This application will be allowed to manage your account push notification settings",
"Apps": "Apps",
"Device activation": "Device activation",
"Application not found": "Application not found",
"The provided application was not found.": "The provided application was not found.",
"Your application code": "Your application code",
"You need to provide the following code to your application": "You need to provide the following code to your application",
"Enter the code displayed on your device": "Enter the code displayed on your device",
"Continue": "Continue",
"The device code is incorrect or no longer valid.": "The device code is incorrect or no longer valid.",
"These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.": "These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access.",
"Last used on {last_used_date}": "Last used on {last_used_date}",
"Never used": "Never used",
"Authorized on {authorization_date}": "Authorized on {authorization_date}",
"Revoke": "Revoke",
"Application was revoked": "Application was revoked"
} }

View file

@ -27,6 +27,7 @@ export enum SettingsRouteName {
CREATE_IDENTITY = "CreateIdentity", CREATE_IDENTITY = "CreateIdentity",
UPDATE_IDENTITY = "UpdateIdentity", UPDATE_IDENTITY = "UpdateIdentity",
IDENTITIES = "IDENTITIES", IDENTITIES = "IDENTITIES",
AUTHORIZED_APPS = "AUTHORIZED_APPS",
} }
export const settingsRoutes: RouteRecordRaw[] = [ export const settingsRoutes: RouteRecordRaw[] = [
@ -84,6 +85,18 @@ export const settingsRoutes: RouteRecordRaw[] = [
}, },
}, },
}, },
{
path: "authorized-apps",
name: SettingsRouteName.AUTHORIZED_APPS,
component: (): Promise<any> => import("@/views/Settings/AppsView.vue"),
props: true,
meta: {
requiredAuth: true,
announcer: {
message: (): string => t("Apps") as string,
},
},
},
{ {
path: "admin", path: "admin",
name: SettingsRouteName.ADMIN, name: SettingsRouteName.ADMIN,

View file

@ -13,6 +13,8 @@ export enum UserRouteName {
EMAIL_VALIDATE = "EMAIL_VALIDATE", EMAIL_VALIDATE = "EMAIL_VALIDATE",
VALIDATE = "Validate", VALIDATE = "Validate",
LOGIN = "Login", LOGIN = "Login",
OAUTH_AUTORIZE = "OAUTH_AUTORIZE",
OAUTH_LOGIN_DEVICE = "OAUTH_LOGIN_DEVICE",
} }
export const userRoutes: RouteRecordRaw[] = [ export const userRoutes: RouteRecordRaw[] = [
@ -42,7 +44,7 @@ export const userRoutes: RouteRecordRaw[] = [
}, },
}, },
{ {
path: "/resend-instructions", path: "/resend-instructions/:email?",
name: UserRouteName.RESEND_CONFIRMATION, name: UserRouteName.RESEND_CONFIRMATION,
component: (): Promise<any> => component: (): Promise<any> =>
import("@/views/User/ResendConfirmation.vue"), import("@/views/User/ResendConfirmation.vue"),
@ -55,7 +57,7 @@ export const userRoutes: RouteRecordRaw[] = [
}, },
}, },
{ {
path: "/password-reset/send", path: "/password-reset/send/:email?",
name: UserRouteName.SEND_PASSWORD_RESET, name: UserRouteName.SEND_PASSWORD_RESET,
component: (): Promise<any> => import("@/views/User/SendPasswordReset.vue"), component: (): Promise<any> => import("@/views/User/SendPasswordReset.vue"),
props: true, props: true,
@ -108,4 +110,27 @@ export const userRoutes: RouteRecordRaw[] = [
announcer: { message: (): string => t("Login") as string }, announcer: { message: (): string => t("Login") as string },
}, },
}, },
{
path: "/oauth/autorize_approve",
name: UserRouteName.OAUTH_AUTORIZE,
component: (): Promise<any> => import("@/views/OAuth/AuthorizeView.vue"),
meta: {
requiredAuth: true,
announcer: {
message: (): string => t("Authorize application") as string,
},
},
},
{
path: "/login/device",
name: UserRouteName.OAUTH_LOGIN_DEVICE,
component: (): Promise<any> =>
import("@/views/OAuth/DeviceActivationView.vue"),
meta: {
requiredAuth: true,
announcer: {
message: (): string => t("Device activation") as string,
},
},
},
]; ];

View file

@ -15,6 +15,8 @@ export interface IActor {
type: ActorType; type: ActorType;
} }
export type IMinimalActor = Pick<IActor, "preferredUsername" | "domain">;
export class Actor implements IActor { export class Actor implements IActor {
id?: string; id?: string;
@ -57,7 +59,7 @@ export class Actor implements IActor {
} }
export function usernameWithDomain( export function usernameWithDomain(
actor: IActor | undefined, actor: IMinimalActor | undefined,
force = false force = false
): string { ): string {
if (!actor) return ""; if (!actor) return "";

View file

@ -0,0 +1,15 @@
export interface IApplication {
name: string;
clientId: string;
clientSecret?: string;
redirectUris?: string;
scope: string | null;
website: string | null;
}
export interface IApplicationToken {
id: string;
application: IApplication;
lastUsedAt: string;
insertedAt: string;
}

View file

@ -7,6 +7,7 @@ import { IFollowedGroupEvent } from "./followedGroupEvent.model";
import { PictureInformation } from "./picture"; import { PictureInformation } from "./picture";
import { IMember } from "./actor/member.model"; import { IMember } from "./actor/member.model";
import { IFeedToken } from "./feedtoken.model"; import { IFeedToken } from "./feedtoken.model";
import { IApplicationToken } from "./application.model";
export interface ICurrentUser { export interface ICurrentUser {
id: string; id: string;
@ -14,6 +15,7 @@ export interface ICurrentUser {
isLoggedIn: boolean; isLoggedIn: boolean;
role: ICurrentUserRole; role: ICurrentUserRole;
defaultActor?: IPerson; defaultActor?: IPerson;
actors: IPerson[];
} }
export interface IUserPreferredLocation { export interface IUserPreferredLocation {
@ -66,4 +68,5 @@ export interface IUser extends ICurrentUser {
currentSignInAt: string; currentSignInAt: string;
memberships: Paginate<IMember>; memberships: Paginate<IMember>;
feedTokens: IFeedToken[]; feedTokens: IFeedToken[];
authAuthorizedApplications: IApplicationToken[];
} }

View file

@ -1,13 +1,14 @@
import { AUTH_USER_ACTOR_ID } from "@/constants"; import { AUTH_USER_ACTOR_ID } from "@/constants";
import { UPDATE_CURRENT_ACTOR_CLIENT, IDENTITIES } from "@/graphql/actor"; import { UPDATE_CURRENT_ACTOR_CLIENT, IDENTITIES } from "@/graphql/actor";
import { IPerson } from "@/types/actor"; import { IPerson } from "@/types/actor";
import { ICurrentUser } from "@/types/current-user.model";
import { apolloClient } from "@/vue-apollo"; import { apolloClient } from "@/vue-apollo";
import { import {
provideApolloClient, provideApolloClient,
useLazyQuery,
useMutation, useMutation,
useQuery,
} from "@vue/apollo-composable"; } from "@vue/apollo-composable";
import { computed, watch } from "vue"; import { computed } from "vue";
export class NoIdentitiesException extends Error {} export class NoIdentitiesException extends Error {}
@ -27,6 +28,10 @@ export async function changeIdentity(identity: IPerson): Promise<void> {
} }
} }
const { onResult: setIdentities, load: loadIdentities } = provideApolloClient(
apolloClient
)(() => useLazyQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>(IDENTITIES));
/** /**
* We fetch from localStorage the latest actor ID used, * We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache * then fetch the current identities to set in cache
@ -35,13 +40,15 @@ export async function changeIdentity(identity: IPerson): Promise<void> {
export async function initializeCurrentActor(): Promise<void> { export async function initializeCurrentActor(): Promise<void> {
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID); const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
const { result: identitiesResult } = provideApolloClient(apolloClient)(() => loadIdentities();
useQuery<{ identities: IPerson[] }>(IDENTITIES)
);
const identities = computed(() => identitiesResult.value?.identities); setIdentities(async ({ data }) => {
const identities = computed(() => data?.loggedUser?.actors);
console.debug(
"initializing current actor based on identities",
identities.value
);
watch(identities, async () => {
if (identities.value && identities.value.length < 1) { if (identities.value && identities.value.length < 1) {
console.warn("Logged user has no identities!"); console.warn("Logged user has no identities!");
throw new NoIdentitiesException(); throw new NoIdentitiesException();

View file

@ -234,6 +234,7 @@ import { convertToUsername } from "@/utils/username";
import { Dialog } from "@/plugins/dialog"; import { Dialog } from "@/plugins/dialog";
import { Notifier } from "@/plugins/notifier"; import { Notifier } from "@/plugins/notifier";
import { AbsintheGraphQLErrors } from "@/types/errors.model"; import { AbsintheGraphQLErrors } from "@/types/errors.model";
import { ICurrentUser } from "@/types/current-user.model";
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const router = useRouter(); const router = useRouter();
@ -348,7 +349,7 @@ const {
onError: deletePersonError, onError: deletePersonError,
} = useMutation(DELETE_PERSON, () => ({ } = useMutation(DELETE_PERSON, () => ({
update: (store: ApolloCache<InMemoryCache>) => { update: (store: ApolloCache<InMemoryCache>) => {
const data = store.readQuery<{ identities: IPerson[] }>({ const data = store.readQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>({
query: IDENTITIES, query: IDENTITIES,
}); });
@ -356,7 +357,12 @@ const {
store.writeQuery({ store.writeQuery({
query: IDENTITIES, query: IDENTITIES,
data: { data: {
identities: data.identities.filter((i) => i.id !== identity.value.id), loggedUser: {
...data.loggedUser,
actors: data.loggedUser.actors.filter(
(i) => i.id !== identity.value.id
),
},
}, },
}); });
} }
@ -379,10 +385,10 @@ deletePersonDone(async () => {
*/ */
const client = resolveClient(); const client = resolveClient();
const data = client.readQuery<{ const data = client.readQuery<{
identities: IPerson[]; loggedUser: Pick<ICurrentUser, "actors">;
}>({ query: IDENTITIES }); }>({ query: IDENTITIES });
if (data) { if (data) {
await maybeUpdateCurrentActorCache(data.identities[0]); await maybeUpdateCurrentActorCache(data.loggedUser.actors[0]);
} }
await redirectIfNoIdentitySelected(); await redirectIfNoIdentitySelected();
@ -408,7 +414,7 @@ const {
store: ApolloCache<InMemoryCache>, store: ApolloCache<InMemoryCache>,
{ data: updateData }: FetchResult { data: updateData }: FetchResult
) => { ) => {
const data = store.readQuery<{ identities: IPerson[] }>({ const data = store.readQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>({
query: IDENTITIES, query: IDENTITIES,
}); });
@ -452,7 +458,7 @@ const {
store: ApolloCache<InMemoryCache>, store: ApolloCache<InMemoryCache>,
{ data: updateData }: FetchResult { data: updateData }: FetchResult
) => { ) => {
const data = store.readQuery<{ identities: IPerson[] }>({ const data = store.readQuery<{ loggedUser: Pick<ICurrentUser, "actors"> }>({
query: IDENTITIES, query: IDENTITIES,
}); });
@ -460,10 +466,13 @@ const {
store.writeQuery({ store.writeQuery({
query: IDENTITIES, query: IDENTITIES,
data: { data: {
identities: [ loggedUser: {
...data.identities, ...data.loggedUser,
{ ...updateData?.createPerson, type: ActorType.PERSON }, actors: [
], ...data.loggedUser.actors,
{ ...updateData?.createPerson, type: ActorType.PERSON },
],
},
}, },
}); });
} }

View file

@ -77,10 +77,9 @@ import { displayName, usernameWithDomain } from "@/types/actor";
import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue"; import DiscussionListItem from "@/components/Discussion/DiscussionListItem.vue";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
import { MemberRole } from "@/types/enums"; import { MemberRole } from "@/types/enums";
import { useGroupDiscussionsList } from "@/composition/apollo/discussions";
import { IMember } from "@/types/actor/member.model"; import { IMember } from "@/types/actor/member.model";
import EmptyContent from "@/components/Utils/EmptyContent.vue"; import EmptyContent from "@/components/Utils/EmptyContent.vue";
import { useGroup } from "@/composition/apollo/group";
import { usePersonStatusGroup } from "@/composition/apollo/actor"; import { usePersonStatusGroup } from "@/composition/apollo/actor";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRouteQuery, integerTransformer } from "vue-use-route-query"; import { useRouteQuery, integerTransformer } from "vue-use-route-query";
@ -92,10 +91,13 @@ const DISCUSSIONS_PER_PAGE = 10;
const props = defineProps<{ preferredUsername: string }>(); const props = defineProps<{ preferredUsername: string }>();
const { group, loading: groupLoading } = useGroup(props.preferredUsername, { const { group, loading: groupLoading } = useGroupDiscussionsList(
discussionsPage: page.value, props.preferredUsername,
discussionsLimit: DISCUSSIONS_PER_PAGE, {
}); discussionsPage: page.value,
discussionsLimit: DISCUSSIONS_PER_PAGE,
}
);
const { person, loading: personLoading } = usePersonStatusGroup( const { person, loading: personLoading } = usePersonStatusGroup(
props.preferredUsername props.preferredUsername
@ -109,7 +111,7 @@ useHead({
const groupMemberships = computed((): (string | undefined)[] => { const groupMemberships = computed((): (string | undefined)[] => {
if (!person.value || !person.value.id) return []; if (!person.value || !person.value.id) return [];
return person.value.memberships.elements return (person.value.memberships?.elements ?? [])
.filter( .filter(
(membership: IMember) => (membership: IMember) =>
![ ![

View file

@ -649,7 +649,7 @@ const FullAddressAutoComplete = defineAsyncComponent(
// }, // },
// }, // },
// group: { // group: {
// query: FETCH_GROUP, // query: FETCH_GROUP_PUBLIC,
// fetchPolicy: "cache-and-network", // fetchPolicy: "cache-and-network",
// variables() { // variables() {
// return { // return {

View file

@ -73,9 +73,9 @@
t( t(
"{count} members", "{count} members",
{ {
count: group.members.total, count: group.members?.total,
}, },
group.members.total group.members?.total
) )
}} }}
<router-link <router-link
@ -388,9 +388,9 @@
<!-- Private things --> <!-- Private things -->
<div class="flex-1 m-0 flex flex-col flex-wrap gap-2"> <div class="flex-1 m-0 flex flex-col flex-wrap gap-2">
<!-- Group discussions --> <!-- Group discussions -->
<Discussions :group="group" class="flex-1" /> <Discussions :group="discussionGroup ?? group" class="flex-1" />
<!-- Resources --> <!-- Resources -->
<Resources :group="group" class="flex-1" /> <Resources :group="resourcesGroup ?? group" class="flex-1" />
</div> </div>
<!-- Public things --> <!-- Public things -->
<div class="flex-1 m-0 flex flex-col flex-wrap gap-2"> <div class="flex-1 m-0 flex flex-col flex-wrap gap-2">
@ -452,9 +452,9 @@
t( t(
"{count} members", "{count} members",
{ {
count: group.members.total, count: group.members?.total,
}, },
group.members.total group.members?.total
) )
}} }}
</event-metadata-block> </event-metadata-block>
@ -671,6 +671,7 @@ import { useAnonymousReportsConfig } from "../../composition/apollo/config";
import { computed, defineAsyncComponent, inject, ref, watch } from "vue"; import { computed, defineAsyncComponent, inject, ref, watch } from "vue";
import { useCurrentActorClient } from "@/composition/apollo/actor"; import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useGroup, useLeaveGroup } from "@/composition/apollo/group"; import { useGroup, useLeaveGroup } from "@/composition/apollo/group";
import { useGroupDiscussionsList } from "@/composition/apollo/discussions";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
import AccountGroup from "vue-material-design-icons/AccountGroup.vue"; import AccountGroup from "vue-material-design-icons/AccountGroup.vue";
@ -691,6 +692,7 @@ import Posts from "@/components/Group/Sections/PostsSection.vue";
import Events from "@/components/Group/Sections/EventsSection.vue"; import Events from "@/components/Group/Sections/EventsSection.vue";
import { Dialog } from "@/plugins/dialog"; import { Dialog } from "@/plugins/dialog";
import { Notifier } from "@/plugins/notifier"; import { Notifier } from "@/plugins/notifier";
import { useGroupResourcesList } from "@/composition/apollo/resources";
const props = defineProps<{ const props = defineProps<{
preferredUsername: string; preferredUsername: string;
@ -705,6 +707,14 @@ const {
} = useGroup(props.preferredUsername, { afterDateTime: new Date() }); } = useGroup(props.preferredUsername, { afterDateTime: new Date() });
const router = useRouter(); const router = useRouter();
const { group: discussionGroup } = useGroupDiscussionsList(
props.preferredUsername
);
const { group: resourcesGroup } = useGroupResourcesList(
props.preferredUsername,
{ resourcesPage: 1, resourcesLimit: 3 }
);
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
// const { person } = usePersonStatusGroup(group); // const { person } = usePersonStatusGroup(group);
@ -1037,7 +1047,7 @@ const isCurrentActorOnADifferentDomainThanGroup = computed((): boolean => {
const members = computed((): IMember[] => { const members = computed((): IMember[] => {
return ( return (
group.value?.members.elements.filter( (group.value?.members?.elements ?? []).filter(
(member: IMember) => (member: IMember) =>
![ ![
MemberRole.INVITED, MemberRole.INVITED,

View file

@ -18,27 +18,47 @@
<section class="timeline"> <section class="timeline">
<o-field> <o-field>
<o-radio v-model="activityType" :native-value="undefined"> <o-radio class="pr-4" v-model="activityType" :native-value="undefined">
<TimelineText /> <TimelineText />
{{ t("All activities") }}</o-radio {{ t("All activities") }}</o-radio
> >
<o-radio v-model="activityType" :native-value="ActivityType.MEMBER"> <o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.MEMBER"
>
<o-icon icon="account-multiple-plus"></o-icon> <o-icon icon="account-multiple-plus"></o-icon>
{{ t("Members") }}</o-radio {{ t("Members") }}</o-radio
> >
<o-radio v-model="activityType" :native-value="ActivityType.GROUP"> <o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.GROUP"
>
<o-icon icon="cog"></o-icon> <o-icon icon="cog"></o-icon>
{{ t("Settings") }}</o-radio {{ t("Settings") }}</o-radio
> >
<o-radio v-model="activityType" :native-value="ActivityType.EVENT"> <o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.EVENT"
>
<o-icon icon="calendar"></o-icon> <o-icon icon="calendar"></o-icon>
{{ t("Events") }}</o-radio {{ t("Events") }}</o-radio
> >
<o-radio v-model="activityType" :native-value="ActivityType.POST"> <o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.POST"
>
<o-icon icon="bullhorn"></o-icon> <o-icon icon="bullhorn"></o-icon>
{{ t("Posts") }}</o-radio {{ t("Posts") }}</o-radio
> >
<o-radio v-model="activityType" :native-value="ActivityType.DISCUSSION"> <o-radio
class="pr-4"
v-model="activityType"
:native-value="ActivityType.DISCUSSION"
>
<o-icon icon="chat"></o-icon> <o-icon icon="chat"></o-icon>
{{ t("Discussions") }}</o-radio {{ t("Discussions") }}</o-radio
> >
@ -48,11 +68,16 @@
> >
</o-field> </o-field>
<o-field> <o-field>
<o-radio v-model="activityAuthor" :native-value="undefined"> <o-radio
class="pr-4"
v-model="activityAuthor"
:native-value="undefined"
>
<TimelineText /> <TimelineText />
{{ t("All activities") }}</o-radio {{ t("All activities") }}</o-radio
> >
<o-radio <o-radio
class="pr-4"
v-model="activityAuthor" v-model="activityAuthor"
:native-value="ActivityAuthorFilter.SELF" :native-value="ActivityAuthorFilter.SELF"
> >
@ -60,6 +85,7 @@
{{ t("From yourself") }}</o-radio {{ t("From yourself") }}</o-radio
> >
<o-radio <o-radio
class="pr-4"
v-model="activityAuthor" v-model="activityAuthor"
:native-value="ActivityAuthorFilter.BY" :native-value="ActivityAuthorFilter.BY"
> >
@ -89,7 +115,7 @@
<h2 v-else> <h2 v-else>
{{ formatDateString(date) }} {{ formatDateString(date) }}
</h2> </h2>
<ul> <ul class="before:opacity-10">
<li v-for="activityItem in activityItems" :key="activityItem.id"> <li v-for="activityItem in activityItems" :key="activityItem.id">
<skeleton-activity-item v-if="activityItem.type === 'skeleton'" /> <skeleton-activity-item v-if="activityItem.type === 'skeleton'" />
<component <component
@ -202,6 +228,7 @@ const page = ref(1);
const { const {
result: groupTimelineResult, result: groupTimelineResult,
fetchMore: fetchMoreActivities, fetchMore: fetchMoreActivities,
onError: onGroupTLError,
loading, loading,
} = useQuery<{ group: IGroup }>(GROUP_TIMELINE, () => ({ } = useQuery<{ group: IGroup }>(GROUP_TIMELINE, () => ({
preferredUsername: props.preferredUsername, preferredUsername: props.preferredUsername,
@ -211,6 +238,8 @@ const {
author: activityAuthor.value, author: activityAuthor.value,
})); }));
onGroupTLError((err) => console.error(err));
const group = computed(() => groupTimelineResult.value?.group); const group = computed(() => groupTimelineResult.value?.group);
useHead({ useHead({

View file

@ -46,6 +46,7 @@
>{{ t("Close") }}</o-button >{{ t("Close") }}</o-button
> >
<o-button <o-button
v-if="antispamEnabled"
outlined outlined
@click="reportToAntispam(true)" @click="reportToAntispam(true)"
variant="text" variant="text"
@ -53,6 +54,7 @@
>{{ t("Report as spam") }}</o-button >{{ t("Report as spam") }}</o-button
> >
<o-button <o-button
v-if="antispamEnabled"
outlined outlined
@click="reportToAntispam(false)" @click="reportToAntispam(false)"
variant="text" variant="text"

View file

@ -0,0 +1,125 @@
<template>
<div class="container mx-auto w-96">
<div v-show="authApplicationLoading && !resultCode">
<o-skeleton active size="large" class="mt-6" />
<o-skeleton active width="80%" />
<div
class="rounded-lg bg-mbz-warning shadow-xl my-6 p-4 flex items-center gap-2"
>
<div>
<o-skeleton circle active width="42px" height="42px" />
</div>
<div class="w-full">
<o-skeleton active />
<o-skeleton active />
<o-skeleton active />
</div>
</div>
<div class="rounded-lg bg-white shadow-xl my-6">
<div class="p-4 pb-0">
<p class="text-3xl"><o-skeleton active size="large" /></p>
<o-skeleton active width="40%" />
</div>
<div class="flex gap-3 p-4">
<o-skeleton active />
<o-skeleton active />
</div>
</div>
</div>
<AuthorizeApplication
v-if="authApplication"
v-show="!authApplicationLoading && !authApplicationError && !resultCode"
:auth-application="authApplication"
:redirectURI="redirectURI"
:state="state"
:scope="scope"
/>
<div v-show="authApplicationError">
<div
class="rounded-lg text-white bg-mbz-danger shadow-xl my-6 p-4 flex items-center gap-2"
v-if="authApplicationGraphError?.message === 'not_found'"
>
<AlertCircle :size="42" />
<div>
<p class="font-bold">
{{ t("Application not found") }}
</p>
<p>{{ t("The provided application was not found.") }}</p>
</div>
</div>
<o-button
variant="text"
tag="router-link"
:to="{ name: RouteName.HOME }"
>{{ t("Back to homepage") }}</o-button
>
</div>
<div
v-if="resultCode"
class="rounded-lg bg-white shadow-xl my-6 p-4 flex items-center gap-2"
>
<div>
<p class="font-bold">
{{ t("Your application code") }}
</p>
<p>
{{
t(
"You need to provide the following code to your application. It will only be valid for a few minutes."
)
}}
</p>
<p class="text-4xl">{{ resultCode }}</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouteQuery } from "vue-use-route-query";
import { useHead } from "@vueuse/head";
import { computed, ref } from "vue";
import { useI18n } from "vue-i18n";
import { useQuery } from "@vue/apollo-composable";
import { AUTH_APPLICATION } from "@/graphql/application";
import { IApplication } from "@/types/application.model";
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
import type { AbsintheGraphQLError } from "@/types/errors.model";
import RouteName from "@/router/name";
import AuthorizeApplication from "@/components/OAuth/AuthorizeApplication.vue";
const { t } = useI18n({ useScope: "global" });
const clientId = useRouteQuery("client_id", null);
const redirectURI = useRouteQuery("redirect_uri", null);
const state = useRouteQuery("state", null);
const scope = useRouteQuery("scope", null);
const resultCode = ref<string | null>(null);
const {
result: authApplicationResult,
loading: authApplicationLoading,
error: authApplicationError,
} = useQuery<{ authApplication: IApplication }, { clientId: string }>(
AUTH_APPLICATION,
() => ({
clientId: clientId.value as string,
}),
() => ({
enabled: clientId.value !== null,
})
);
const authApplication = computed(
() => authApplicationResult.value?.authApplication
);
const authApplicationGraphError = computed(
() => authApplicationError.value?.graphQLErrors[0] as AbsintheGraphQLError
);
useHead({
title: computed(() => t("Authorize application")),
});
</script>

View file

@ -0,0 +1,149 @@
<template>
<div class="container mx-auto w-96">
<form
@submit.prevent="() => validateCode({ userCode: code })"
@paste="pasteCode"
class="rounded-lg bg-white dark:bg-zinc-900 shadow-xl my-6 p-4"
v-if="!application"
>
<h1 class="text-3xl text-center">
{{ t("Device activation") }}
</h1>
<p class="mb-4 text-center">
{{ t("Enter the code displayed on your device") }}
</p>
<div class="flex items-center justify-between mb-4 gap-2">
<div
v-for="i in Array(9).keys()"
:key="i"
:class="i === 4 ? 'w-6' : 'w-8'"
>
<span
:id="`user-code-${i}`"
v-if="i === 4"
class="block text-3xl text-center"
>-</span
>
<o-input
autocapitalize="characters"
@update:modelValue="(val: string) => inputs[i] = val.toUpperCase()"
:useHtml5Validation="true"
:id="`user-code-${i}`"
:ref="(el: Element) => userCodeInputs[i] = el"
:modelValue="inputs[i]"
v-else
size="large"
style="font-size: 22px; padding: 0.5rem 0.15rem 0.5rem 0.25rem"
required
maxlength="1"
pattern="[A-Z]{1}"
:autofocus="i === 0 ? true : undefined"
/>
</div>
</div>
<div
class="rounded-lg bg-mbz-danger shadow-xl my-6 p-4 flex items-center gap-2"
v-if="error"
>
<AlertCircle :size="42" />
<div>
<p>{{ error }}</p>
</div>
</div>
<div class="text-center">
<o-button native-type="submit">{{ t("Continue") }}</o-button>
</div>
</form>
<AuthorizeApplication v-if="application" :auth-application="application" />
</div>
</template>
<script lang="ts" setup>
import { DEVICE_ACTIVATION } from "@/graphql/application";
import { useMutation } from "@vue/apollo-composable";
import { useHead } from "@vueuse/head";
import { computed, reactive, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import AuthorizeApplication from "@/components/OAuth/AuthorizeApplication.vue";
import { IApplication } from "@/types/application.model";
import { AbsintheGraphQLErrors } from "@/types/errors.model";
import AlertCircle from "vue-material-design-icons/AlertCircle.vue";
const { t } = useI18n({ useScope: "global" });
const {
mutate: validateCode,
onDone: onDeviceActivationDone,
onError: onDeviceActivationError,
} = useMutation<{
deviceActivation: { application: IApplication; id: string; scope: string };
}>(DEVICE_ACTIVATION);
const inputs = reactive<string[]>([]);
const application = ref<IApplication | null>(null);
onDeviceActivationDone(({ data }) => {
const foundApplication = data?.deviceActivation?.application;
if (foundApplication) {
application.value = foundApplication;
}
});
const code = computed(() => {
return inputs.join("");
});
const userCodeInputs = reactive<Record<number, Element>>([]);
watch(inputs, (localInputs) => {
localInputs.forEach((input, index) => {
if (input && index < 8) {
if (index === 3) {
index = 4;
}
(userCodeInputs[index + 1] as HTMLInputElement).focus();
}
});
});
const error = ref<string | null>(null);
onDeviceActivationError(
({ graphQLErrors }: { graphQLErrors: AbsintheGraphQLErrors }) => {
if (graphQLErrors[0].status_code === 404) {
error.value = t("The device code is incorrect or no longer valid.");
}
resetInputs();
(userCodeInputs[0] as HTMLInputElement).focus();
setTimeout(() => {
error.value = null;
}, 10000);
}
);
const resetInputs = () => {
inputs.splice(0);
};
const pasteCode = (e: ClipboardEvent) => {
let pastedCode = e.clipboardData?.getData("text").trim();
if (!pastedCode) return;
if (pastedCode.match(/^[A-Z]{4}-[A-Z]{4}$/)) {
pastedCode = pastedCode.slice(0, 4) + pastedCode.slice(5);
}
if (pastedCode.match(/^[A-Z]{8}$/)) {
pastedCode.split("").forEach((val, index) => {
const realIndex = index > 3 ? index + 1 : index;
inputs[realIndex] = val;
});
}
};
useHead({
title: computed(() => t("Device activation")),
});
</script>

View file

@ -290,7 +290,10 @@ const { result: membershipsResult, loading: membershipsLoading } = useQuery<{
}>( }>(
PERSON_MEMBERSHIPS, PERSON_MEMBERSHIPS,
() => ({ id: currentActor.value?.id }), () => ({ id: currentActor.value?.id }),
() => ({ enabled: currentActor.value?.id !== undefined }) () => ({
enabled:
currentActor.value?.id !== undefined && currentActor.value?.id !== null,
})
); );
const memberships = computed(() => membershipsResult.value?.person.memberships); const memberships = computed(() => membershipsResult.value?.person.memberships);

View file

@ -0,0 +1,145 @@
<template>
<div v-if="loggedUser">
<breadcrumbs-nav
:links="[
{
name: RouteName.AUTHORIZED_APPS,
text: t('Apps'),
},
{
name: RouteName.ACCOUNT_SETTINGS_GENERAL,
text: t('General'),
},
]"
/>
<section>
<h2>{{ t("Apps") }}</h2>
<p>
{{
t(
"These apps can access your account through the API. If you see here apps that you don't recognize, that don't work as expected or that you don't use anymore, you can revoke their access."
)
}}
</p>
<div
class="flex justify-between items-center rounded-lg bg-white shadow-xl my-6"
v-for="authAuthorizedApplication in authAuthorizedApplications"
:key="authAuthorizedApplication.id"
>
<div class="p-4">
<p class="text-3xl font-bold">
{{ authAuthorizedApplication.application.name }}
</p>
<a
v-if="authAuthorizedApplication.application.website"
target="_blank"
:href="authAuthorizedApplication.application.website"
>{{
urlToHostname(authAuthorizedApplication.application.website)
}}</a
>
<p>
<span v-if="authAuthorizedApplication.lastUsedAt">{{
t("Last used on {last_used_date}", {
last_used_date: formatDateString(
authAuthorizedApplication.lastUsedAt
),
})
}}</span>
<span v-else>{{ t("Never used") }}</span>
{{
t("Authorized on {authorization_date}", {
authorization_date: formatDateString(
authAuthorizedApplication.insertedAt
),
})
}}
</p>
</div>
<div class="p-4">
<o-button
@click="() => revoke({ appTokenId: authAuthorizedApplication.id })"
variant="danger"
>{{ t("Revoke") }}</o-button
>
</div>
</div>
</section>
</div>
</template>
<script lang="ts" setup>
import { useLoggedUser } from "@/composition/apollo/user";
import {
AUTH_AUTHORIZED_APPLICATIONS,
REVOKED_AUTHORIZED_APPLICATION,
} from "@/graphql/application";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { useHead } from "@vueuse/head";
import { computed, inject } from "vue";
import { useI18n } from "vue-i18n";
import RouteName from "../../router/name";
import { IUser } from "@/types/current-user.model";
import { formatDateString } from "@/filters/datetime";
import { Notifier } from "@/plugins/notifier";
const { t } = useI18n({ useScope: "global" });
const { loggedUser } = useLoggedUser();
const { result: authAuthorizedApplicationsResult } = useQuery<{
loggedUser: Pick<IUser, "authAuthorizedApplications">;
}>(AUTH_AUTHORIZED_APPLICATIONS);
const authAuthorizedApplications = computed(
() =>
authAuthorizedApplicationsResult.value?.loggedUser
?.authAuthorizedApplications
);
const urlToHostname = (url: string | undefined): string | null => {
if (!url) return null;
try {
return new URL(url).hostname;
} catch (e) {
return null;
}
};
const { mutate: revoke, onDone: onRevokedApplication } = useMutation<
{ revokeApplicationToken: { id: string } },
{ appTokenId: string }
>(REVOKED_AUTHORIZED_APPLICATION, {
update: (cache, { data: returnedData }) => {
const data = cache.readQuery<{
loggedUser: Pick<IUser, "authAuthorizedApplications">;
}>({ query: AUTH_AUTHORIZED_APPLICATIONS });
if (!data) return;
if (!returnedData) return;
const authorizedApplications =
data.loggedUser.authAuthorizedApplications.filter(
(app) => app.id !== returnedData.revokeApplicationToken.id
);
cache.writeQuery({
query: AUTH_AUTHORIZED_APPLICATIONS,
data: {
...data,
loggedUser: {
...data.loggedUser,
authAuthorizedApplications: authorizedApplications,
},
},
});
},
});
const notifier = inject<Notifier>("notifier");
onRevokedApplication(() => {
notifier?.success(t("Application was revoked"));
});
useHead({
title: computed(() => t("Apps")),
});
</script>

View file

@ -106,7 +106,7 @@
variant="text" variant="text"
:to="{ :to="{
name: RouteName.REGISTER, name: RouteName.REGISTER,
params: { query: {
default_email: credentials.email, default_email: credentials.email,
default_password: credentials.password, default_password: credentials.password,
}, },
@ -143,14 +143,6 @@ import { LoginError, LoginErrorCode } from "@/types/enums";
import { useCurrentUserClient } from "@/composition/apollo/user"; import { useCurrentUserClient } from "@/composition/apollo/user";
import { useHead } from "@vueuse/head"; import { useHead } from "@vueuse/head";
const props = withDefaults(
defineProps<{
email?: string;
password?: string;
}>(),
{ email: "", password: "" }
);
const { t } = useI18n({ useScope: "global" }); const { t } = useI18n({ useScope: "global" });
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@ -177,8 +169,9 @@ const errors = ref<string[]>([]);
const submitted = ref(false); const submitted = ref(false);
const credentials = reactive({ const credentials = reactive({
email: "", email: typeof route.query.email === "string" ? route.query.email : "",
password: "", password:
typeof route.query.password === "string" ? route.query.password : "",
}); });
const redirect = ref<string | undefined>(""); const redirect = ref<string | undefined>("");
@ -298,9 +291,6 @@ const currentProvider = computed(() => {
}); });
onMounted(() => { onMounted(() => {
credentials.email = props.email;
credentials.password = props.password;
const query = route?.query; const query = route?.query;
errorCode.value = query?.code as LoginErrorCode; errorCode.value = query?.code as LoginErrorCode;
redirect.value = query?.redirect as string | undefined; redirect.value = query?.redirect as string | undefined;

View file

@ -181,7 +181,7 @@
variant="text" variant="text"
:to="{ :to="{
name: RouteName.LOGIN, name: RouteName.LOGIN,
params: { query: {
email: credentials.email, email: credentials.email,
password: credentials.password, password: credentials.password,
}, },
@ -212,7 +212,7 @@ import AuthProviders from "../../components/User/AuthProviders.vue";
import { computed, reactive, ref, watch } from "vue"; import { computed, reactive, ref, watch } from "vue";
import { useMutation, useQuery } from "@vue/apollo-composable"; import { useMutation, useQuery } from "@vue/apollo-composable";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useHead } from "@vueuse/head"; import { useHead } from "@vueuse/head";
import { AbsintheGraphQLErrors } from "@/types/errors.model"; import { AbsintheGraphQLErrors } from "@/types/errors.model";
@ -221,6 +221,7 @@ type errorMessage = { type: errorType; message: string };
type credentialsType = { email: string; password: string; locale: string }; type credentialsType = { email: string; password: string; locale: string };
const { t, locale } = useI18n({ useScope: "global" }); const { t, locale } = useI18n({ useScope: "global" });
const route = useRoute();
const router = useRouter(); const router = useRouter();
const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG); const { result: configResult } = useQuery<{ config: IConfig }>(CONFIG);
@ -229,17 +230,10 @@ const config = computed(() => configResult.value?.config);
const showGravatar = ref(false); const showGravatar = ref(false);
const props = withDefaults(
defineProps<{
email?: string;
password?: string;
}>(),
{ email: "", password: "" }
);
const credentials = reactive<credentialsType>({ const credentials = reactive<credentialsType>({
email: props.email, email: typeof route.query.email === "string" ? route.query.email : "",
password: props.password, password:
typeof route.query.password === "string" ? route.query.password : "",
locale: "en", locale: "en",
}); });

View file

@ -36,7 +36,7 @@ test("Login has everything we need", async ({ page }) => {
await page.goBack(); await page.goBack();
await registerLink.click(); await registerLink.click();
await page.waitForURL("/register/user"); await page.waitForURL("/register/user?default_email=&default_password=");
expect(page.url()).toContain("/register/user"); expect(page.url()).toContain("/register/user");
await page.goBack(); await page.goBack();
}); });

View file

@ -45,6 +45,13 @@ export default defineConfig(({ command }) => {
resolve: { resolve: {
alias: { alias: {
"@": path.resolve(__dirname, "./src"), "@": path.resolve(__dirname, "./src"),
unfetch: path.resolve(
__dirname,
"node_modules",
"unfetch",
"dist",
"unfetch.mjs"
),
}, },
}, },
css: { css: {

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,89 @@
defmodule Mobilizon.GraphQL.Authorization do
@moduledoc """
Check authorizations
"""
use Rajska,
valid_roles: [:user, :moderator, :administrator],
super_role: :administrator,
default_rule: :default
alias Mobilizon.Applications.ApplicationToken
alias Mobilizon.GraphQL.Authorization.AppScope
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext, only: [dgettext: 3]
@impl true
def has_user_access?(%User{}, _scope, _rule), do: true
def has_user_access?(%ApplicationToken{scope: scope} = _current_app_token, _struct, rule)
when rule != :forbid_app_access do
AppScope.has_app_access?(scope, rule)
end
def has_user_access?(_current_user, _scoped_struct, _rule), do: false
@impl true
def get_current_user(%{current_auth_app_token: app_token}), do: app_token
def get_current_user(%{current_user: current_user}), do: current_user
def get_current_user(_ctx), do: nil
@impl true
def role_authorized?(_user_role, :all), do: true
def role_authorized?(role, _allowed_role) when is_super_role(role), do: true
def role_authorized?(user_role, allowed_role) when is_atom(user_role) and is_atom(allowed_role),
do: user_role === allowed_role
def role_authorized?(user_role, allowed_roles)
when is_atom(user_role) and is_list(allowed_roles),
do: user_role in allowed_roles
@impl true
def get_user_role(%ApplicationToken{user: %{role: role}}), do: role
def get_user_role(%{role: role}), do: role
def get_user_role(nil), do: nil
@impl true
def get_ip(%{ip: ip}), do: ip
@impl true
def unauthorized_message(resolution) do
case Map.get(resolution.context, :current_user) do
nil ->
"unauthenticated"
_ ->
"unauthorized"
end
end
@impl true
def unauthorized_query_scope_message(_resolution, object_type) do
dgettext("errors", "Not authorized to access this %{object_type}",
object_type: replace_underscore(object_type)
)
end
@impl true
def unauthorized_object_scope_message(_result_object, object) do
dgettext("errors", "Not authorized to access object %{object}", object: object.identifier)
end
@impl true
def unauthorized_object_message(_resolution, object) do
dgettext("errors", "Not authorized to access object %{object}", object: object.identifier)
end
@impl true
def unauthorized_field_message(_resolution, field),
do: dgettext("errors", "Not authorized to access field %{field}", field: field)
defp replace_underscore(string) when is_binary(string), do: String.replace(string, "_", " ")
defp replace_underscore(atom) when is_atom(atom) do
atom
|> Atom.to_string()
|> replace_underscore()
end
end

View file

@ -0,0 +1,118 @@
defmodule Mobilizon.GraphQL.Authorization.AppScope do
@moduledoc """
Module referencing all scopes usable in the Mobilizon API
"""
require Logger
@global_scope %{
"write" => [
# Media
:"write:media:upload",
:"write:media:remove",
# Event permissions
:"write:event:create",
:"write:event:update",
:"write:event:delete",
# Comment permissions
:"write:comment:create",
:"write:comment:update",
:"write:comment:delete",
# Event participation permission
:"write:participation",
# User account permissions
:"write:user:settings",
:"write:user:setting:activity",
:"write:user:setting:push",
# Profile permissions
:"write:profile:create",
:"write:profile:update",
:"write:profile:delete",
:"write:profile:feed_token:create",
:"write:feed_token:delete",
# Membership permissions
:"write:group_membership",
# Group permissions
:"write:group:create",
:"write:group:update",
:"write:group:delete",
# Group discussions permissions
:"write:group:discussion:create",
:"write:group:discussion:update",
:"write:group:discussion:delete",
# Group resources permissions
:"write:group:resources:create",
:"write:group:resources:update",
:"write:group:resources:delete",
# Group members
:"write:group:members",
# Post permissions
:"write:group:post:create",
:"write:group:post:update",
:"write:group:post:delete"
],
"read" => [
:"read:event",
:"read:event:participants",
:"read:event:participants:export",
:"read:user:settings",
# Profile permissions
:"read:profile",
:"read:profile:organized_events",
:"read:profile:participations",
:"read:profile:memberships",
:"read:profile:follows",
# Group details permissions
:"read:group",
:"read:group:events",
:"read:group:discussions",
:"read:group:resources",
:"read:group:members",
:"read:group:followers",
:"read:group:todo_lists",
:"read:group:activities"
]
}
@spec get_scopes :: list(atom())
def get_scopes do
@global_scope
|> Map.values()
|> Enum.concat()
|> Enum.concat([:read, :write])
end
@spec scopes_valid?(String.t()) :: boolean()
def scopes_valid?(scopes) do
scopes
|> String.split(" ")
|> Enum.all?(&scope_valid?/1)
end
@spec scope_valid?(String.t() | atom()) :: boolean()
def scope_valid?(scope) when is_binary(scope) do
scope in Enum.map(get_scopes(), &to_string/1)
end
def scope_valid?(scope) when is_atom(scope) do
scope in get_scopes()
end
@spec has_app_access?(binary, atom | binary) :: boolean
def has_app_access?(scope, rule) do
Logger.debug("Has app token access? scope: #{inspect(scope)}, rule: #{inspect(rule)}")
scope = String.split(scope, " ")
scope_acceptable_for_rule?(scope, rule) or global_scopes_acceptable_for_rule?(scope, rule)
end
@spec scope_acceptable_for_rule?(list(String.t() | atom()), String.t() | atom()) :: boolean()
defp scope_acceptable_for_rule?(scope, rule) when is_list(scope) do
to_string(rule) in Enum.map(scope, &to_string/1)
end
defp global_scopes_acceptable_for_rule?(scope, rule),
do: Enum.any?(scope, &global_scope_acceptable_for_rule?(&1, rule))
defp global_scope_acceptable_for_rule?(global_scope, rule),
do: scope_acceptable_for_rule?(Map.get(@global_scope, global_scope, []), rule)
end

View file

@ -30,6 +30,17 @@ defmodule Mobilizon.GraphQL.Error do
handle(reason) handle(reason)
end end
# It's unclear why returned errors are now binaries instead of atoms
# but we can still convert them back
def normalize(string) when is_binary(string) do
string
|> String.to_existing_atom()
|> handle()
rescue
ArgumentError ->
handle(string)
end
# Unhandled errors # Unhandled errors
def normalize(other) do def normalize(other) do
handle(other) handle(other)
@ -65,6 +76,9 @@ defmodule Mobilizon.GraphQL.Error do
end end
defp handle(reason) when is_binary(reason) do defp handle(reason) when is_binary(reason) do
Logger.debug("Unknown error")
Logger.debug(reason)
%Error{ %Error{
code: :unknown_error, code: :unknown_error,
message: reason, message: reason,
@ -101,6 +115,11 @@ defmodule Mobilizon.GraphQL.Error do
defp metadata(:group_not_found), do: {404, dgettext("errors", "Group not found")} defp metadata(:group_not_found), do: {404, dgettext("errors", "Group not found")}
defp metadata(:resource_not_found), do: {404, dgettext("errors", "Resource not found")} defp metadata(:resource_not_found), do: {404, dgettext("errors", "Resource not found")}
defp metadata(:discussion_not_found), do: {404, dgettext("errors", "Discussion not found")} defp metadata(:discussion_not_found), do: {404, dgettext("errors", "Discussion not found")}
defp metadata(:application_not_found), do: {404, dgettext("errors", "Application not found")}
defp metadata(:application_token_not_found),
do: {404, dgettext("errors", "Application token not found")}
defp metadata(:unknown), do: {500, dgettext("errors", "Something went wrong")} defp metadata(:unknown), do: {500, dgettext("errors", "Something went wrong")}
defp metadata(code) do defp metadata(code) do

View file

@ -0,0 +1,153 @@
defmodule Mobilizon.GraphQL.Resolvers.Application do
@moduledoc """
Handles the Application-related GraphQL calls.
"""
alias Mobilizon.Applications, as: ApplicationManager
alias Mobilizon.Applications.{Application, ApplicationDeviceActivation, ApplicationToken}
alias Mobilizon.Service.Auth.Applications
alias Mobilizon.Users.User
import Mobilizon.Web.Gettext, only: [dgettext: 2]
require Logger
@doc """
Authorize an application
"""
@spec authorize(any(), map(), Absinthe.Resolution.t()) :: {:ok, map()} | {:error, String.t()}
def authorize(
_parent,
%{client_id: client_id, redirect_uri: redirect_uri, scope: scope, state: state},
%{context: %{current_user: %User{id: user_id}}}
) do
case Applications.autorize(client_id, redirect_uri, scope, user_id) do
{:ok,
%ApplicationToken{
application: %Application{client_id: client_id},
scope: scope,
authorization_code: code
}} ->
{:ok, %{code: code, state: state, client_id: client_id, scope: scope}}
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
{:error, :application_not_found} ->
{:error,
dgettext(
"errors",
"No application with this client_id was found"
)}
{:error, :redirect_uri_not_in_allowed} ->
{:error,
dgettext(
"errors",
"The given redirect_uri is not in the list of allowed redirect URIs"
)}
end
end
def authorize(_parent, _args, _context) do
{:error, :unauthenticated}
end
@spec get_application(any(), map(), Absinthe.Resolution.t()) ::
{:ok, Application.t()} | {:error, :application_not_found | :unauthenticated}
def get_application(_parent, %{client_id: client_id}, %{context: %{current_user: %User{}}}) do
case ApplicationManager.get_application_by_client_id(client_id) do
%Application{} = application ->
{:ok, application}
nil ->
{:error, :application_not_found}
end
end
def get_application(_parent, _args, _resolution) do
{:error, :unauthenticated}
end
def get_user_applications(_parent, _args, %{context: %{current_user: %User{id: user_id}}}) do
{:ok, ApplicationManager.list_application_tokens_for_user_id(user_id)}
end
def get_user_applications(_parent, _args, _resolution) do
{:error, :unauthenticated}
end
def revoke_application_token(_parent, %{app_token_id: app_token_id}, %{
context: %{current_user: %User{id: user_id}}
}) do
case ApplicationManager.get_application_token(app_token_id) do
%ApplicationToken{user_id: ^user_id} = app_token ->
case Applications.revoke_application_token(app_token) do
{:ok, %{delete_app_token: app_token, delete_guardian_tokens: _delete_guardian_tokens}} ->
{:ok, %{id: app_token.id}}
{:error, _, _, _} ->
{:error, dgettext("errors", "Error while revoking token")}
end
_ ->
{:error, :application_token_not_found}
end
end
def revoke_application_token(_parent, _args, _resolution) do
{:error, :unauthenticated}
end
def activate_device(_parent, %{user_code: user_code}, %{
context: %{current_user: %User{} = user}
}) do
case Applications.activate_device(user_code, user) do
{:ok, %ApplicationDeviceActivation{} = app_device_activation} ->
{:ok, app_device_activation |> Map.from_struct() |> Map.take([:application, :id, :scope])}
{:error, :expired} ->
{:error, dgettext("errors", "The given user code has expired")}
{:error, :not_found} ->
{:error, dgettext("errors", "The given user code is invalid")}
end
end
def activate_device(_parent, _args, _resolution) do
{:error, :unauthenticated}
end
@spec authorize_device_application(any(), map(), Absinthe.Resolution.t()) ::
{:ok, map()} | {:error, String.t()}
def authorize_device_application(
_parent,
%{client_id: client_id, user_code: user_code},
%{context: %{current_user: %User{}}}
) do
case Applications.autorize_device_application(client_id, user_code) do
{:ok, %ApplicationDeviceActivation{application: app}} ->
{:ok, app}
{:error, :not_confirmed} ->
{:error,
dgettext(
"errors",
"The device user code was not provided before approving the application"
)}
{:error, :not_found} ->
{:error,
dgettext(
"errors",
"The given user code is invalid"
)}
{:error, :expired} ->
{:error, dgettext("errors", "The given user code has expired")}
end
end
def authorize_device_application(_parent, _args, _resolution) do
{:error, :unauthenticated}
end
end

View file

@ -44,7 +44,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
def upload_media( def upload_media(
_parent, _parent,
%{file: %Plug.Upload{} = file} = args, %{file: %Plug.Upload{} = file} = args,
%{context: %{current_actor: %Actor{id: actor_id}}} %{context: %{current_actor: %Actor{id: default_actor_id}}}
) do ) do
with {:ok, with {:ok,
%{ %{
@ -62,7 +62,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Media do
{:ok, media = %Media{}} <- {:ok, media = %Media{}} <-
Medias.create_media(%{ Medias.create_media(%{
file: args, file: args,
actor_id: actor_id, actor_id: Map.get(args, :actor_id, default_actor_id),
metadata: Map.take(uploaded, [:width, :height, :blurhash]) metadata: Map.take(uploaded, [:width, :height, :blurhash])
}) do }) do
{:ok, transform_media(media)} {:ok, transform_media(media)}

View file

@ -20,8 +20,8 @@ defmodule Mobilizon.GraphQL.Schema do
alias Mobilizon.Actors.{Actor, Follower, Member} alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.GraphQL.{Authorization, Schema}
alias Mobilizon.GraphQL.Middleware.{CurrentActorProvider, ErrorHandler, OperationNameLogger} alias Mobilizon.GraphQL.Middleware.{CurrentActorProvider, ErrorHandler, OperationNameLogger}
alias Mobilizon.GraphQL.Schema
alias Mobilizon.GraphQL.Schema.Custom alias Mobilizon.GraphQL.Schema.Custom
alias Mobilizon.Storage.Repo alias Mobilizon.Storage.Repo
@ -53,14 +53,17 @@ defmodule Mobilizon.GraphQL.Schema do
import_types(Schema.Users.PushSubscription) import_types(Schema.Users.PushSubscription)
import_types(Schema.Users.ActivitySetting) import_types(Schema.Users.ActivitySetting)
import_types(Schema.FollowedGroupActivityType) import_types(Schema.FollowedGroupActivityType)
import_types(Schema.AuthApplicationType)
@desc "A struct containing the id of the deleted object" @desc "A struct containing the id of the deleted object"
object :deleted_object do object :deleted_object do
meta(:authorize, :all)
field(:id, :id) field(:id, :id)
end end
@desc "A JWT and the associated user ID" @desc "A JWT and the associated user ID"
object :login do object :login do
meta(:authorize, :all)
field(:access_token, non_null(:string), description: "A JWT Token for this session") field(:access_token, non_null(:string), description: "A JWT Token for this session")
field(:refresh_token, non_null(:string), field(:refresh_token, non_null(:string),
@ -74,6 +77,7 @@ defmodule Mobilizon.GraphQL.Schema do
Represents a notification for an user Represents a notification for an user
""" """
object :notification do object :notification do
meta(:authorize, :user)
field(:id, :id, description: "The notification ID") field(:id, :id, description: "The notification ID")
field(:user, :user, description: "The user to transmit the notification to") field(:user, :user, description: "The user to transmit the notification to")
field(:actor, :actor, description: "The notification target profile") field(:actor, :actor, description: "The notification target profile")
@ -132,7 +136,9 @@ defmodule Mobilizon.GraphQL.Schema do
|> Dataloader.add_source(Resources, default_source) |> Dataloader.add_source(Resources, default_source)
|> Dataloader.add_source(Todos, default_source) |> Dataloader.add_source(Todos, default_source)
Map.put(ctx, :loader, loader) ctx
|> Map.put(:loader, loader)
|> Map.put(:authorization, Authorization)
end end
def plugins do def plugins do
@ -161,6 +167,7 @@ defmodule Mobilizon.GraphQL.Schema do
import_fields(:resource_queries) import_fields(:resource_queries)
import_fields(:post_queries) import_fields(:post_queries)
import_fields(:statistics_queries) import_fields(:statistics_queries)
import_fields(:auth_application_queries)
end end
@desc """ @desc """
@ -187,6 +194,7 @@ defmodule Mobilizon.GraphQL.Schema do
import_fields(:follower_mutations) import_fields(:follower_mutations)
import_fields(:push_mutations) import_fields(:push_mutations)
import_fields(:activity_setting_mutations) import_fields(:activity_setting_mutations)
import_fields(:auth_application_mutations)
end end
@desc """ @desc """
@ -198,11 +206,19 @@ defmodule Mobilizon.GraphQL.Schema do
end end
@spec middleware(list(module()), any(), map()) :: list(module()) @spec middleware(list(module()), any(), map()) :: list(module())
def middleware(middleware, _field, %{identifier: type}) when type in [:query, :mutation] do def middleware(middleware, field, %{identifier: type}) when type in [:query, :mutation] do
[CurrentActorProvider] ++ middleware ++ [ErrorHandler, OperationNameLogger] [CurrentActorProvider | middleware]
|> Enum.map(&fix_middleware_format_for_rajska/1)
|> Rajska.add_query_authorization(field, Authorization)
|> Rajska.add_object_authorization()
|> List.insert_at(-1, ErrorHandler)
|> List.insert_at(-1, OperationNameLogger)
end end
def middleware(middleware, _field, _object) do def middleware(middleware, field, object) do
middleware Rajska.add_field_authorization(middleware, field, object)
end end
defp fix_middleware_format_for_rajska({mod, config}), do: {mod, config}
defp fix_middleware_format_for_rajska(mod), do: {mod, nil}
end end

View file

@ -25,11 +25,13 @@ defmodule Mobilizon.GraphQL.Schema.ActivityType do
end end
object :activity_param_item do object :activity_param_item do
meta(:authorize, :user)
field(:key, :string) field(:key, :string)
field(:value, :string) field(:value, :string)
end end
interface :activity_object do interface :activity_object do
meta(:authorize, :user)
field(:id, :id) field(:id, :id)
resolve_type(fn resolve_type(fn
@ -66,11 +68,13 @@ defmodule Mobilizon.GraphQL.Schema.ActivityType do
A paginated list of activity items A paginated list of activity items
""" """
object :paginated_activity_list do object :paginated_activity_list do
meta(:authorize, :user)
field(:elements, list_of(:activity), description: "A list of activities") field(:elements, list_of(:activity), description: "A list of activities")
field(:total, :integer, description: "The total number of elements in the list") field(:total, :integer, description: "The total number of elements in the list")
end end
object :activity do object :activity do
meta(:authorize, :user)
field(:id, :id, description: "The activity item ID") field(:id, :id, description: "The activity item ID")
field(:inserted_at, :datetime, description: "When was the activity inserted") field(:inserted_at, :datetime, description: "When was the activity inserted")
field(:priority, :integer) field(:priority, :integer)

View file

@ -13,6 +13,7 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
@desc "An ActivityPub actor" @desc "An ActivityPub actor"
interface :actor do interface :actor do
meta(:authorize, :all)
field(:id, :id, description: "Internal ID for this actor") field(:id, :id, description: "Internal ID for this actor")
field(:url, :string, description: "The ActivityPub actor's URL") field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
@ -65,18 +66,21 @@ defmodule Mobilizon.GraphQL.Schema.ActorInterface do
@desc "Suspend an actor" @desc "Suspend an actor"
field :suspend_profile, :deleted_object do field :suspend_profile, :deleted_object do
arg(:id, non_null(:id), description: "The remote profile ID to suspend") arg(:id, non_null(:id), description: "The remote profile ID to suspend")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&ActorResolver.suspend_profile/3) resolve(&ActorResolver.suspend_profile/3)
end end
@desc "Unsuspend an actor" @desc "Unsuspend an actor"
field :unsuspend_profile, :actor do field :unsuspend_profile, :actor do
arg(:id, non_null(:id), description: "The remote profile ID to unsuspend") arg(:id, non_null(:id), description: "The remote profile ID to unsuspend")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&ActorResolver.unsuspend_profile/3) resolve(&ActorResolver.unsuspend_profile/3)
end end
@desc "Refresh a profile" @desc "Refresh a profile"
field :refresh_profile, :actor do field :refresh_profile, :actor do
arg(:id, non_null(:id), description: "The remote profile ID to refresh") arg(:id, non_null(:id), description: "The remote profile ID to refresh")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&ActorResolver.refresh_profile/3) resolve(&ActorResolver.refresh_profile/3)
end end
end end

View file

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.ApplicationType do
Represents an application Represents an application
""" """
object :application do object :application do
meta(:authorize, :all)
interfaces([:actor]) interfaces([:actor])
field(:id, :id, description: "Internal ID for this application") field(:id, :id, description: "Internal ID for this application")

View file

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.FollowerType do
Represents an actor's follower Represents an actor's follower
""" """
object :follower do object :follower do
meta(:authorize, :user)
field(:id, :id, description: "The follow ID") field(:id, :id, description: "The follow ID")
field(:target_actor, :actor, description: "What or who the profile follows") field(:target_actor, :actor, description: "What or who the profile follows")
field(:actor, :actor, description: "Which profile follows") field(:actor, :actor, description: "Which profile follows")
@ -30,6 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.FollowerType do
A paginated list of follower objects A paginated list of follower objects
""" """
object :paginated_follower_list do object :paginated_follower_list do
meta(:authorize, :user)
field(:elements, list_of(:follower), description: "A list of followers") field(:elements, list_of(:follower), description: "A list of followers")
field(:total, :integer, description: "The total number of elements in the list") field(:total, :integer, description: "The total number of elements in the list")
end end
@ -43,6 +45,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.FollowerType do
description: "Whether the follower has been approved by the target actor or not" description: "Whether the follower has been approved by the target actor or not"
) )
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Followers.update_follower/3) resolve(&Followers.update_follower/3)
end end
end end

View file

@ -29,6 +29,9 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
Represents a group of actors Represents a group of actors
""" """
object :group do object :group do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:actor, :interactable, :activity_object, :action_log_object, :group_search_result]) interfaces([:actor, :interactable, :activity_object, :action_log_object, :group_search_result])
field(:id, :id, description: "Internal ID for this group") field(:id, :id, description: "Internal ID for this group")
@ -94,7 +97,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A list of the events this actor has organized") description("A list of the events this actor has organized")
end end
field :discussions, :paginated_discussion_list do field :discussions, :paginated_discussion_list,
meta: [private: true, rule: :"read:group:discussions"] do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
description: "The page in the paginated discussion list" description: "The page in the paginated discussion list"
@ -111,7 +115,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description: "Whether the group is opened to all or has restricted access" description: "Whether the group is opened to all or has restricted access"
) )
field :members, :paginated_member_list do field :members, :paginated_member_list, meta: [private: true, rule: :"read:group:members"] do
arg(:name, :string, description: "A name to filter members by") arg(:name, :string, description: "A name to filter members by")
arg(:page, :integer, default_value: 1, description: "The page in the paginated member list") arg(:page, :integer, default_value: 1, description: "The page in the paginated member list")
arg(:limit, :integer, default_value: 10, description: "The limit of members per page") arg(:limit, :integer, default_value: 10, description: "The limit of members per page")
@ -120,7 +124,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of group members") description("A paginated list of group members")
end end
field :resources, :paginated_resource_list do field :resources, :paginated_resource_list,
meta: [private: true, rule: :"read:group:resources"] do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
description: "The page in the paginated resource list" description: "The page in the paginated resource list"
@ -138,7 +143,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of the posts this group has") description("A paginated list of the posts this group has")
end end
field :todo_lists, :paginated_todo_list_list do field :todo_lists, :paginated_todo_list_list,
meta: [private: true, rule: :"read:group:todo_lists"] do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
description: "The page in the paginated todo-lists list" description: "The page in the paginated todo-lists list"
@ -149,7 +155,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of the todo lists this group has") description("A paginated list of the todo lists this group has")
end end
field :followers, :paginated_follower_list do field :followers, :paginated_follower_list,
meta: [private: true, rule: :"read:group:followers"] do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
description: "The page in the paginated followers list" description: "The page in the paginated followers list"
@ -166,7 +173,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description("A paginated list of the followers this group has") description("A paginated list of the followers this group has")
end end
field :activity, :paginated_activity_list do field :activity, :paginated_activity_list,
meta: [private: true, rule: :"read:group:activities"] do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
description: "The page in the paginated activity items list" description: "The page in the paginated activity items list"
@ -204,6 +212,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
A paginated list of groups A paginated list of groups
""" """
object :paginated_group_list do object :paginated_group_list do
meta(:authorize, :all)
field(:elements, list_of(:group), description: "A list of groups") field(:elements, list_of(:group), description: "A list of groups")
field(:total, :integer, description: "The total number of groups in the list") field(:total, :integer, description: "The total number of groups in the list")
end end
@ -215,12 +224,6 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
value(:private, description: "Visible only to people with the link - or invited") value(:private, description: "Visible only to people with the link - or invited")
end end
object :group_follow do
field(:group, :group, description: "The group followed")
field(:profile, :group, description: "The group followed")
field(:notify, :boolean, description: "Whether to notify profile from group activity")
end
object :group_queries do object :group_queries do
@desc "Get all groups" @desc "Get all groups"
field :groups, :paginated_group_list do field :groups, :paginated_group_list do
@ -236,12 +239,25 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
arg(:suspended, :boolean, default_value: false, description: "Filter by suspended status") arg(:suspended, :boolean, default_value: false, description: "Filter by suspended status")
arg(:page, :integer, default_value: 1, description: "The page in the paginated group list") arg(:page, :integer, default_value: 1, description: "The page in the paginated group list")
arg(:limit, :integer, default_value: 10, description: "The limit of groups per page") arg(:limit, :integer, default_value: 10, description: "The limit of groups per page")
middleware(Rajska.QueryAuthorization,
permit: [:administrator, :moderator],
scope: Mobilizon.Actors.Actor,
args: %{}
)
resolve(&Group.list_groups/3) resolve(&Group.list_groups/3)
end end
@desc "Get a group by its ID" @desc "Get a group by its ID"
field :get_group, :group do field :get_group, :group do
arg(:id, non_null(:id), description: "The group ID") arg(:id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization,
permit: [:administrator, :moderator],
scope: Mobilizon.Actors.Actor
)
resolve(&Group.get_group/3) resolve(&Group.get_group/3)
end end
@ -251,15 +267,9 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
description: "The group preferred_username, eventually containing their domain if remote" description: "The group preferred_username, eventually containing their domain if remote"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Group.find_group/3) resolve(&Group.find_group/3)
end end
@desc "Get a group by its preferred username"
field :group_by_id, :group do
arg(:id, non_null(:id), description: "The group local ID")
resolve(&Group.find_group_by_id/3)
end
end end
object :group_mutations do object :group_mutations do
@ -291,7 +301,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
) )
arg(:physical_address, :address_input, description: "The physical address for the group") arg(:physical_address, :address_input, description: "The physical address for the group")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.create_group/3) resolve(&Group.create_group/3)
end end
@ -323,14 +333,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
) )
arg(:physical_address, :address_input, description: "The physical address for the group") arg(:physical_address, :address_input, description: "The physical address for the group")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.update_group/3) resolve(&Group.update_group/3)
end end
@desc "Delete a group" @desc "Delete a group"
field :delete_group, :deleted_object do field :delete_group, :deleted_object do
arg(:group_id, non_null(:id), description: "The group ID") arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.delete_group/3) resolve(&Group.delete_group/3)
end end
@ -343,6 +353,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
default_value: true default_value: true
) )
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.follow_group/3) resolve(&Group.follow_group/3)
end end
@ -355,13 +366,14 @@ defmodule Mobilizon.GraphQL.Schema.Actors.GroupType do
default_value: true default_value: true
) )
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.update_group_follow/3) resolve(&Group.update_group_follow/3)
end end
@desc "Unfollow a group" @desc "Unfollow a group"
field :unfollow_group, :follower do field :unfollow_group, :follower do
arg(:group_id, non_null(:id), description: "The group ID") arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Group.unfollow_group/3) resolve(&Group.unfollow_group/3)
end end
end end

View file

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
Represents a member of a group Represents a member of a group
""" """
object :member do object :member do
meta(:authorize, :user)
interfaces([:activity_object]) interfaces([:activity_object])
field(:id, :id, description: "The member's ID") field(:id, :id, description: "The member's ID")
field(:parent, :group, description: "Of which the profile is member") field(:parent, :group, description: "Of which the profile is member")
@ -37,6 +38,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
A paginated list of members A paginated list of members
""" """
object :paginated_member_list do object :paginated_member_list do
meta(:authorize, :user)
field(:elements, list_of(:member), description: "A list of members") field(:elements, list_of(:member), description: "A list of members")
field(:total, :integer, description: "The total number of elements in the list") field(:total, :integer, description: "The total number of elements in the list")
end end
@ -46,6 +48,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field :join_group, :member do field :join_group, :member do
arg(:group_id, non_null(:id), description: "The group ID") arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership",
args: %{parent_id: :group_id}
)
resolve(&Group.join_group/3) resolve(&Group.join_group/3)
end end
@ -53,9 +62,42 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field :leave_group, :deleted_object do field :leave_group, :deleted_object do
arg(:group_id, non_null(:id), description: "The group ID") arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership",
args: %{parent_id: :group_id}
)
resolve(&Group.leave_group/3) resolve(&Group.leave_group/3)
end end
@desc "Accept an invitation to a group"
field :accept_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership"
)
resolve(&Member.accept_invitation/3)
end
@desc "Reject an invitation to a group"
field :reject_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group_membership"
)
resolve(&Member.reject_invitation/3)
end
@desc "Invite an actor to join the group" @desc "Invite an actor to join the group"
field :invite_member, :member do field :invite_member, :member do
arg(:group_id, non_null(:id), description: "The group ID") arg(:group_id, non_null(:id), description: "The group ID")
@ -64,29 +106,29 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
description: "The targeted person's federated username" description: "The targeted person's federated username"
) )
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :group_id}
)
resolve(&Member.invite_member/3) resolve(&Member.invite_member/3)
end end
@desc "Accept an invitation to a group"
field :accept_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
resolve(&Member.accept_invitation/3)
end
@desc "Reject an invitation to a group"
field :reject_invitation, :member do
arg(:id, non_null(:id), description: "The member ID")
resolve(&Member.reject_invitation/3)
end
@desc """ @desc """
Approve a membership request Approve a membership request
""" """
field :approve_member, :member do field :approve_member, :member do
arg(:member_id, non_null(:id), description: "The member ID") arg(:member_id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.approve_member/3) resolve(&Member.approve_member/3)
end end
@ -96,6 +138,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
field :reject_member, :member do field :reject_member, :member do
arg(:member_id, non_null(:id), description: "The member ID") arg(:member_id, non_null(:id), description: "The member ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.reject_member/3) resolve(&Member.reject_member/3)
end end
@ -106,6 +155,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
arg(:member_id, non_null(:id), description: "The member ID") arg(:member_id, non_null(:id), description: "The member ID")
arg(:role, non_null(:member_role_enum), description: "The new member role") arg(:role, non_null(:member_role_enum), description: "The new member role")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.update_member/3) resolve(&Member.update_member/3)
end end
@ -118,6 +174,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.MemberType do
description: "Whether the member should be excluded from the group" description: "Whether the member should be excluded from the group"
) )
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Member,
rule: :"write:group:members",
args: %{parent_id: :member_id}
)
resolve(&Member.remove_member/3) resolve(&Member.remove_member/3)
end end
end end

View file

@ -16,6 +16,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
Represents a person identity Represents a person identity
""" """
object :person do object :person do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:actor, :action_log_object]) interfaces([:actor, :action_log_object])
field(:id, :id, description: "Internal ID for this person") field(:id, :id, description: "Internal ID for this person")
@ -72,7 +74,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
# This one should have a privacy setting # This one should have a privacy setting
field(:organized_events, :paginated_event_list, field(:organized_events, :paginated_event_list,
description: "A list of the events this actor has organized" description: "A list of the events this actor has organized",
meta: [private: true, rule: :"read:profile:organized_events"]
) do ) do
arg(:page, :integer, default_value: 1, description: "The page in the paginated event list") arg(:page, :integer, default_value: 1, description: "The page in the paginated event list")
arg(:limit, :integer, default_value: 10, description: "The limit of events per page") arg(:limit, :integer, default_value: 10, description: "The limit of events per page")
@ -81,7 +84,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
@desc "The list of events this person goes to" @desc "The list of events this person goes to"
field(:participations, :paginated_participant_list, field(:participations, :paginated_participant_list,
description: "The list of events this person goes to" description: "The list of events this person goes to",
meta: [private: true, rule: :"read:profile:participations"]
) do ) do
arg(:event_id, :id, description: "Filter by event ID") arg(:event_id, :id, description: "Filter by event ID")
@ -97,7 +101,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
@desc "The list of groups this person is member of" @desc "The list of groups this person is member of"
field(:memberships, :paginated_member_list, field(:memberships, :paginated_member_list,
description: "The list of group this person is member of" description: "The list of group this person is member of",
meta: [private: true, rule: :"read:profile:memberships"]
) do ) do
arg(:group, :string, description: "Filter by group federated username") arg(:group, :string, description: "Filter by group federated username")
arg(:group_id, :id, description: "Filter by group ID") arg(:group_id, :id, description: "Filter by group ID")
@ -113,7 +118,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
@desc "The list of groups this person follows" @desc "The list of groups this person follows"
field(:follows, :paginated_follower_list, field(:follows, :paginated_follower_list,
description: "The list of groups this person follows" description: "The list of groups this person follows",
meta: [private: true, rule: :"read:profile:follows"]
) do ) do
arg(:group, :string, description: "Filter by group federated username") arg(:group, :string, description: "Filter by group federated username")
@ -131,6 +137,7 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
A paginated list of persons A paginated list of persons
""" """
object :paginated_person_list do object :paginated_person_list do
meta(:authorize, :all)
field(:elements, list_of(:person), description: "A list of persons") field(:elements, list_of(:person), description: "A list of persons")
field(:total, :integer, description: "The total number of persons in the list") field(:total, :integer, description: "The total number of persons in the list")
end end
@ -138,23 +145,46 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
object :person_queries do object :person_queries do
@desc "Get the current actor for the logged-in user" @desc "Get the current actor for the logged-in user"
field :logged_person, :person do field :logged_person, :person do
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{}
)
resolve(&Person.get_current_person/3) resolve(&Person.get_current_person/3)
end end
@desc "Get a person by its (federated) username" @desc "Get a person by its (federated) username"
field :fetch_person, :person do field :fetch_person, :person do
arg(:preferred_username, non_null(:string), description: "The person's federated username") arg(:preferred_username, non_null(:string), description: "The person's federated username")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{preferred_username: :preferred_username}
)
resolve(&Person.fetch_person/3) resolve(&Person.fetch_person/3)
end end
@desc "Get a person by its ID" @desc "Get a person by its ID"
field :person, :person do field :person, :person do
arg(:id, non_null(:id), description: "The person ID") arg(:id, non_null(:id), description: "The person ID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Person.get_person/3) resolve(&Person.get_person/3)
end end
@desc "Get the persons for an user" @desc "Get the persons for an user"
field :identities, list_of(:person) do field :identities, list_of(:person) do
deprecate("Use the loggedUser query instead")
middleware(Rajska.QueryAuthorization,
permit: [:user, :moderator, :administrator],
scope: Mobilizon.Actors.Actor,
args: %{},
rule: :user_self_identities
)
resolve(&Person.identities/3) resolve(&Person.identities/3)
end end
@ -172,6 +202,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
arg(:suspended, :boolean, default_value: false, description: "Filter by suspended status") arg(:suspended, :boolean, default_value: false, description: "Filter by suspended status")
arg(:page, :integer, default_value: 1, description: "The page in the paginated person list") arg(:page, :integer, default_value: 1, description: "The page in the paginated person list")
arg(:limit, :integer, default_value: 10, description: "The limit of persons per page") arg(:limit, :integer, default_value: 10, description: "The limit of persons per page")
middleware(Rajska.QueryAuthorization,
permit: [:administrator, :moderator],
scope: Mobilizon.Actors.Actor,
args: %{}
)
resolve(&Person.list_persons/3) resolve(&Person.list_persons/3)
end end
end end
@ -195,6 +232,13 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
"The banner for the profile, either as an object or directly the ID of an existing media" "The banner for the profile, either as an object or directly the ID of an existing media"
) )
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{},
rule: :"write:profile:create"
)
resolve(&Person.create_person/3) resolve(&Person.create_person/3)
end end
@ -216,6 +260,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
"The banner for the profile, either as an object or directly the ID of an existing media" "The banner for the profile, either as an object or directly the ID of an existing media"
) )
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
rule: :"write:profile:update"
)
resolve(&Person.update_person/3) resolve(&Person.update_person/3)
end end
@ -223,6 +273,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
field :delete_person, :person do field :delete_person, :person do
arg(:id, non_null(:id), description: "The person's ID") arg(:id, non_null(:id), description: "The person's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
rule: :"write:profile:delete"
)
resolve(&Person.delete_person/3) resolve(&Person.delete_person/3)
end end
@ -245,6 +301,8 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
"The banner for the profile, either as an object or directly the ID of an existing media" "The banner for the profile, either as an object or directly the ID of an existing media"
) )
middleware(Rajska.QueryAuthorization, permit: :all, scope: Mobilizon.Actors.Actor, args: %{})
resolve(&Person.register_person/3) resolve(&Person.register_person/3)
end end
end end
@ -254,6 +312,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
field :event_person_participation_changed, :person do field :event_person_participation_changed, :person do
arg(:person_id, non_null(:id), description: "The person's ID") arg(:person_id, non_null(:id), description: "The person's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{id: :person_id}
)
config(fn args, _ -> config(fn args, _ ->
{:ok, topic: args.person_id} {:ok, topic: args.person_id}
end) end)
@ -264,6 +328,12 @@ defmodule Mobilizon.GraphQL.Schema.Actors.PersonType do
arg(:person_id, non_null(:id), description: "The person's ID") arg(:person_id, non_null(:id), description: "The person's ID")
arg(:group, non_null(:string), description: "The group's federated username") arg(:group, non_null(:string), description: "The group's federated username")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Actors.Actor,
args: %{id: :person_id}
)
config(fn args, _ -> config(fn args, _ ->
{:ok, topic: [args.group, args.person_id]} {:ok, topic: [args.group, args.person_id]}
end) end)

View file

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
An address object An address object
""" """
object :address do object :address do
meta(:authorize, :all)
field(:geom, :point, description: "The geocoordinates for the point where this address is") field(:geom, :point, description: "The geocoordinates for the point where this address is")
field(:street, :string, description: "The address's street name (with number)") field(:street, :string, description: "The address's street name (with number)")
field(:locality, :string, description: "The address's locality") field(:locality, :string, description: "The address's locality")
@ -29,6 +30,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
A phone address A phone address
""" """
object :phone_address do object :phone_address do
meta(:authorize, :all)
field(:phone, :string, description: "The phone number") field(:phone, :string, description: "The phone number")
field(:info, :string, description: "Additional information about the phone number") field(:info, :string, description: "Additional information about the phone number")
end end
@ -37,11 +39,13 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
An online address An online address
""" """
object :online_address do object :online_address do
meta(:authorize, :all)
field(:url, :string) field(:url, :string)
field(:info, :string) field(:info, :string)
end end
object :picture_info_element do object :picture_info_element do
meta(:authorize, :all)
field(:name, :string) field(:name, :string)
field(:url, :string) field(:url, :string)
end end
@ -50,6 +54,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
A picture associated with an address A picture associated with an address
""" """
object :picture_info do object :picture_info do
meta(:authorize, :all)
field(:url, :string) field(:url, :string)
field(:author, :picture_info_element) field(:author, :picture_info_element)
field(:source, :picture_info_element) field(:source, :picture_info_element)
@ -100,7 +105,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
arg(:limit, :integer, default_value: 10, description: "The limit of search results per page") arg(:limit, :integer, default_value: 10, description: "The limit of search results per page")
arg(:type, :address_search_type, description: "Filter by type of results") arg(:type, :address_search_type, description: "Filter by type of results")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Address.search/3) resolve(&Address.search/3)
end end
@ -115,6 +120,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
description: "The user's locale. Geocoding backends will make use of this value." description: "The user's locale. Geocoding backends will make use of this value."
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Address.reverse_geocode/3) resolve(&Address.reverse_geocode/3)
end end
end end

View file

@ -15,6 +15,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
@desc "An action log" @desc "An action log"
object :action_log do object :action_log do
meta(:authorize, :moderator)
field(:id, :id, description: "Internal ID for this comment") field(:id, :id, description: "Internal ID for this comment")
field(:actor, :actor, description: "The actor that acted") field(:actor, :actor, description: "The actor that acted")
field(:object, :action_log_object, description: "The object that was acted upon") field(:object, :action_log_object, description: "The object that was acted upon")
@ -26,6 +27,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
A paginated list of action logs A paginated list of action logs
""" """
object :paginated_action_log_list do object :paginated_action_log_list do
meta(:authorize, :moderator)
field(:elements, list_of(:action_log), description: "A list of action logs") field(:elements, list_of(:action_log), description: "A list of action logs")
field(:total, :integer, description: "The total number of action logs in the list") field(:total, :integer, description: "The total number of action logs in the list")
end end
@ -49,6 +51,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
@desc "The objects that can be in an action log" @desc "The objects that can be in an action log"
interface :action_log_object do interface :action_log_object do
meta(:authorize, [:moderator, :administrator])
field(:id, :id, description: "Internal ID for this object") field(:id, :id, description: "Internal ID for this object")
resolve_type(fn resolve_type(fn
@ -82,6 +85,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Language information Language information
""" """
object :language do object :language do
meta(:authorize, :all)
field(:code, :string, description: "The iso-639-3 language code") field(:code, :string, description: "The iso-639-3 language code")
field(:name, :string, description: "The language name") field(:name, :string, description: "The language name")
end end
@ -90,6 +94,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Dashboard information Dashboard information
""" """
object :dashboard do object :dashboard do
meta(:authorize, :administrator)
field(:last_public_event_published, :event, description: "Last public event published") field(:last_public_event_published, :event, description: "Last public event published")
field(:last_group_created, :group, description: "Last public group created") field(:last_group_created, :group, description: "Last public group created")
field(:number_of_users, :integer, description: "The number of local users") field(:number_of_users, :integer, description: "The number of local users")
@ -109,6 +114,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Admin settings Admin settings
""" """
object :admin_settings do object :admin_settings do
meta(:authorize, :administrator)
field(:instance_name, :string, description: "The instance's name") field(:instance_name, :string, description: "The instance's name")
field(:instance_description, :string, description: "The instance's description") field(:instance_description, :string, description: "The instance's description")
field(:instance_long_description, :string, description: "The instance's long description") field(:instance_long_description, :string, description: "The instance's long description")
@ -184,6 +190,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
An instance representation An instance representation
""" """
object :instance do object :instance do
meta(:authorize, :administrator)
field(:domain, :id, description: "The domain name of the instance") field(:domain, :id, description: "The domain name of the instance")
field(:follower_status, :instance_follow_status, description: "Do we follow this instance") field(:follower_status, :instance_follow_status, description: "Do we follow this instance")
field(:followed_status, :instance_follow_status, description: "Does this instance follow us?") field(:followed_status, :instance_follow_status, description: "Does this instance follow us?")
@ -226,6 +233,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
A paginated list of instances A paginated list of instances
""" """
object :paginated_instance_list do object :paginated_instance_list do
meta(:authorize, :administrator)
field(:elements, list_of(:instance), description: "A list of instances") field(:elements, list_of(:instance), description: "A list of instances")
field(:total, :integer, description: "The total number of instances in the list") field(:total, :integer, description: "The total number of instances in the list")
end end
@ -235,6 +243,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
field :action_logs, type: :paginated_action_log_list do field :action_logs, type: :paginated_action_log_list do
arg(:page, :integer, default_value: 1) arg(:page, :integer, default_value: 1)
arg(:limit, :integer, default_value: 10) arg(:limit, :integer, default_value: 10)
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: false)
resolve(&Admin.list_action_logs/3) resolve(&Admin.list_action_logs/3)
end end
@ -247,6 +256,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
"The user's locale. The list of languages will be translated with this locale" "The user's locale. The list of languages will be translated with this locale"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Admin.get_list_of_languages/3) resolve(&Admin.get_list_of_languages/3)
end end
@ -254,6 +264,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Get dashboard information Get dashboard information
""" """
field :dashboard, type: :dashboard do field :dashboard, type: :dashboard do
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_dashboard/3) resolve(&Admin.get_dashboard/3)
end end
@ -261,6 +272,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
Get admin settings Get admin settings
""" """
field :admin_settings, type: :admin_settings do field :admin_settings, type: :admin_settings do
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_settings/3) resolve(&Admin.get_settings/3)
end end
@ -278,6 +290,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
description: "The limit of relay followers per page" description: "The limit of relay followers per page"
) )
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.list_relay_followers/3) resolve(&Admin.list_relay_followers/3)
end end
@ -301,6 +314,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
) )
arg(:direction, :string, default_value: :desc, description: "The sorting direction") arg(:direction, :string, default_value: :desc, description: "The sorting direction")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.list_relay_followings/3) resolve(&Admin.list_relay_followings/3)
end end
@ -336,6 +350,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
) )
arg(:direction, :string, default_value: :desc, description: "The sorting direction") arg(:direction, :string, default_value: :desc, description: "The sorting direction")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_instances/3) resolve(&Admin.get_instances/3)
end end
@ -344,6 +359,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
""" """
field :instance, :instance do field :instance, :instance do
arg(:domain, non_null(:id), description: "The instance domain") arg(:domain, non_null(:id), description: "The instance domain")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.get_instance/3) resolve(&Admin.get_instance/3)
end end
end end
@ -352,28 +368,28 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
@desc "Add an instance subscription" @desc "Add an instance subscription"
field :add_instance, type: :instance do field :add_instance, type: :instance do
arg(:domain, non_null(:string), description: "The instance domain to add") arg(:domain, non_null(:string), description: "The instance domain to add")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.create_instance/3) resolve(&Admin.create_instance/3)
end end
@desc "Delete a relay subscription" @desc "Delete a relay subscription"
field :remove_relay, type: :follower do field :remove_relay, type: :follower do
arg(:address, non_null(:string), description: "The relay hostname to delete") arg(:address, non_null(:string), description: "The relay hostname to delete")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.remove_relay/3) resolve(&Admin.remove_relay/3)
end end
@desc "Accept a relay subscription" @desc "Accept a relay subscription"
field :accept_relay, type: :follower do field :accept_relay, type: :follower do
arg(:address, non_null(:string), description: "The accepted relay hostname") arg(:address, non_null(:string), description: "The accepted relay hostname")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.accept_subscription/3) resolve(&Admin.accept_subscription/3)
end end
@desc "Reject a relay subscription" @desc "Reject a relay subscription"
field :reject_relay, type: :follower do field :reject_relay, type: :follower do
arg(:address, non_null(:string), description: "The rejected relay hostname") arg(:address, non_null(:string), description: "The rejected relay hostname")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.reject_subscription/3) resolve(&Admin.reject_subscription/3)
end end
@ -402,7 +418,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
arg(:instance_rules, :string, description: "The instance's rules") arg(:instance_rules, :string, description: "The instance's rules")
arg(:registrations_open, :boolean, description: "Whether the registrations are opened") arg(:registrations_open, :boolean, description: "Whether the registrations are opened")
arg(:instance_languages, list_of(:string), description: "The instance's languages") arg(:instance_languages, list_of(:string), description: "The instance's languages")
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.save_settings/3) resolve(&Admin.save_settings/3)
end end
@ -420,6 +436,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
description: "Whether or not to notify the user of the change" description: "Whether or not to notify the user of the change"
) )
middleware(Rajska.QueryAuthorization, permit: :administrator)
resolve(&Admin.update_user/3) resolve(&Admin.update_user/3)
end end
end end

View file

@ -0,0 +1,132 @@
defmodule Mobilizon.GraphQL.Schema.AuthApplicationType do
@moduledoc """
Schema representation for an auth application
"""
use Absinthe.Schema.Notation
alias Mobilizon.GraphQL.Resolvers.Application
@desc "An application"
object :auth_application do
meta(:authorize, :user)
field(:id, :id)
field(:name, :string)
field(:client_id, :string)
field(:scope, :string)
field(:website, :string)
end
@desc "An application"
object :auth_application_token do
meta(:authorize, :user)
field(:id, :id)
field(:inserted_at, :string)
field(:last_used_at, :string)
field(:application, :auth_application)
end
@desc "The informations returned after authorization"
object :application_code_and_state do
meta(:authorize, :user)
field(:code, :string)
field(:state, :string)
field(:client_id, :string)
field(:scope, :string)
end
object :application_device_activation do
meta(:authorize, :user)
field(:id, :id)
field(:application, :auth_application)
field(:scope, :string)
end
object :auth_application_queries do
@desc "Get an application"
field :auth_application, :auth_application do
arg(:client_id, non_null(:string), description: "The application's client_id")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.Application,
rule: :forbid_app_access,
args: %{client_id: :client_id}
)
resolve(&Application.get_application/3)
end
end
object :auth_application_mutations do
@desc "Authorize an application"
field :authorize_application, :application_code_and_state do
arg(:client_id, non_null(:string), description: "The application's client_id")
arg(:redirect_uri, non_null(:string),
description: "The URI to redirect to with the code and state"
)
arg(:scope, non_null(:string), description: "The scope for the authorization")
arg(:state, :string,
description: "A state parameter to check that the request wasn't altered"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.Application,
rule: :forbid_app_access,
args: %{client_id: :client_id}
)
resolve(&Application.authorize/3)
end
@desc "Revoke an authorized application"
field :revoke_application_token, :deleted_object do
arg(:app_token_id, non_null(:string), description: "The application token's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.ApplicationToken,
rule: :forbid_app_access,
args: %{id: :app_token_id}
)
resolve(&Application.revoke_application_token/3)
end
@desc "Activate an user device"
field :device_activation, :application_device_activation do
arg(:user_code, non_null(:string),
description: "The code provided by the application entered by the user"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.ApplicationDeviceActivation,
rule: :forbid_app_access,
args: %{id: :user_code}
)
resolve(&Application.activate_device/3)
end
@desc "Activate an user device"
field :authorize_device_application, :auth_application do
arg(:client_id, non_null(:string), description: "The application's client_id")
arg(:user_code, non_null(:string),
description: "The code provided by the application entered by the user"
)
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Applications.ApplicationDeviceActivation,
rule: :forbid_app_access,
args: %{id: :client_id}
)
resolve(&Application.authorize_device_application/3)
end
end
end

View file

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
@desc "A config object" @desc "A config object"
object :config do object :config do
meta(:authorize, :all)
# Instance name # Instance name
field(:name, :string, description: "The instance's name") field(:name, :string, description: "The instance's name")
field(:description, :string, description: "The instance's short description") field(:description, :string, description: "The instance's short description")
@ -87,6 +88,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's terms configuration The instance's terms configuration
""" """
object :terms do object :terms do
meta(:authorize, :all)
field(:url, :string, description: "The instance's terms URL.") field(:url, :string, description: "The instance's terms URL.")
field(:type, :instance_terms_type, description: "The instance's terms type") field(:type, :instance_terms_type, description: "The instance's terms type")
field(:body_html, :string, description: "The instance's terms body text") field(:body_html, :string, description: "The instance's terms body text")
@ -96,6 +98,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's privacy policy configuration The instance's privacy policy configuration
""" """
object :privacy do object :privacy do
meta(:authorize, :all)
field(:url, :string, description: "The instance's privacy policy URL") field(:url, :string, description: "The instance's privacy policy URL")
field(:type, :instance_privacy_type, description: "The instance's privacy policy type") field(:type, :instance_privacy_type, description: "The instance's privacy policy type")
field(:body_html, :string, description: "The instance's privacy policy body text") field(:body_html, :string, description: "The instance's privacy policy body text")
@ -105,6 +108,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Geographic coordinates Geographic coordinates
""" """
object :lonlat do object :lonlat do
meta(:authorize, :all)
field(:longitude, :float, description: "The coordinates longitude") field(:longitude, :float, description: "The coordinates longitude")
field(:latitude, :float, description: "The coordinates latitude") field(:latitude, :float, description: "The coordinates latitude")
# field(:accuracy_radius, :integer) # field(:accuracy_radius, :integer)
@ -114,6 +118,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance geocoding configuration Instance geocoding configuration
""" """
object :geocoding do object :geocoding do
meta(:authorize, :all)
field(:autocomplete, :boolean, field(:autocomplete, :boolean,
description: "Whether autocomplete in address fields can be enabled" description: "Whether autocomplete in address fields can be enabled"
) )
@ -125,6 +131,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance maps configuration Instance maps configuration
""" """
object :maps do object :maps do
meta(:authorize, :all)
field(:tiles, :tiles, description: "The instance's maps tiles configuration") field(:tiles, :tiles, description: "The instance's maps tiles configuration")
field(:routing, :routing, description: "The instance's maps routing configuration") field(:routing, :routing, description: "The instance's maps routing configuration")
end end
@ -133,6 +140,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance map tiles configuration Instance map tiles configuration
""" """
object :tiles do object :tiles do
meta(:authorize, :all)
field(:endpoint, :string, description: "The instance's tiles endpoint") field(:endpoint, :string, description: "The instance's tiles endpoint")
field(:attribution, :string, description: "The instance's tiles attribution text") field(:attribution, :string, description: "The instance's tiles attribution text")
end end
@ -141,6 +149,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance map routing configuration Instance map routing configuration
""" """
object :routing do object :routing do
meta(:authorize, :all)
field(:type, :routing_type, description: "The instance's routing type") field(:type, :routing_type, description: "The instance's routing type")
end end
@ -153,6 +162,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous configuration Instance anonymous configuration
""" """
object :anonymous do object :anonymous do
meta(:authorize, :all)
field(:participation, :anonymous_participation, field(:participation, :anonymous_participation,
description: "The instance's anonymous participation settings" description: "The instance's anonymous participation settings"
) )
@ -172,6 +183,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation configuration Instance anonymous participation configuration
""" """
object :anonymous_participation do object :anonymous_participation do
meta(:authorize, :all)
field(:allowed, :boolean, description: "Whether anonymous participations are allowed") field(:allowed, :boolean, description: "Whether anonymous participations are allowed")
field(:validation, :anonymous_participation_validation, field(:validation, :anonymous_participation_validation,
@ -183,6 +195,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation validation configuration Instance anonymous participation validation configuration
""" """
object :anonymous_participation_validation do object :anonymous_participation_validation do
meta(:authorize, :all)
field(:email, :anonymous_participation_validation_email, field(:email, :anonymous_participation_validation_email,
description: "The policy to validate anonymous participations by email" description: "The policy to validate anonymous participations by email"
) )
@ -196,6 +210,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation with validation by email configuration Instance anonymous participation with validation by email configuration
""" """
object :anonymous_participation_validation_email do object :anonymous_participation_validation_email do
meta(:authorize, :all)
field(:enabled, :boolean, field(:enabled, :boolean,
description: "Whether anonymous participation validation by email is enabled" description: "Whether anonymous participation validation by email is enabled"
) )
@ -209,6 +225,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous participation with validation by captcha configuration Instance anonymous participation with validation by captcha configuration
""" """
object :anonymous_participation_validation_captcha do object :anonymous_participation_validation_captcha do
meta(:authorize, :all)
field(:enabled, :boolean, field(:enabled, :boolean,
description: "Whether anonymous participation validation by captcha is enabled" description: "Whether anonymous participation validation by captcha is enabled"
) )
@ -218,6 +236,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation configuration Instance anonymous event creation configuration
""" """
object :anonymous_event_creation do object :anonymous_event_creation do
meta(:authorize, :all)
field(:allowed, :boolean, description: "Whether anonymous event creation is enabled") field(:allowed, :boolean, description: "Whether anonymous event creation is enabled")
field(:validation, :anonymous_event_creation_validation, field(:validation, :anonymous_event_creation_validation,
@ -229,6 +248,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation validation configuration Instance anonymous event creation validation configuration
""" """
object :anonymous_event_creation_validation do object :anonymous_event_creation_validation do
meta(:authorize, :all)
field(:email, :anonymous_event_creation_validation_email, field(:email, :anonymous_event_creation_validation_email,
description: "The policy to validate anonymous event creations by email" description: "The policy to validate anonymous event creations by email"
) )
@ -242,6 +263,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation email validation configuration Instance anonymous event creation email validation configuration
""" """
object :anonymous_event_creation_validation_email do object :anonymous_event_creation_validation_email do
meta(:authorize, :all)
field(:enabled, :boolean, field(:enabled, :boolean,
description: "Whether anonymous event creation with email validation is enabled" description: "Whether anonymous event creation with email validation is enabled"
) )
@ -255,6 +278,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous event creation captcha validation configuration Instance anonymous event creation captcha validation configuration
""" """
object :anonymous_event_creation_validation_captcha do object :anonymous_event_creation_validation_captcha do
meta(:authorize, :all)
field(:enabled, :boolean, field(:enabled, :boolean,
description: "Whether anonymous event creation with validation by captcha is enabled" description: "Whether anonymous event creation with validation by captcha is enabled"
) )
@ -264,6 +289,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Instance anonymous reports Instance anonymous reports
""" """
object :anonymous_reports do object :anonymous_reports do
meta(:authorize, :all)
field(:allowed, :boolean, description: "Whether anonymous reports are allowed") field(:allowed, :boolean, description: "Whether anonymous reports are allowed")
end end
@ -271,6 +297,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
A resource provider details A resource provider details
""" """
object :resource_provider do object :resource_provider do
meta(:authorize, :all)
field(:type, :string, description: "The resource provider's type") field(:type, :string, description: "The resource provider's type")
field(:endpoint, :string, description: "The resource provider's endpoint") field(:endpoint, :string, description: "The resource provider's endpoint")
field(:software, :string, description: "The resource provider's software") field(:software, :string, description: "The resource provider's software")
@ -280,17 +307,22 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's features The instance's features
""" """
object :features do object :features do
meta(:authorize, :all)
field(:groups, :boolean, description: "Whether groups are activated on this instance") field(:groups, :boolean, description: "Whether groups are activated on this instance")
field(:event_creation, :boolean, field(:event_creation, :boolean,
description: "Whether event creation is allowed on this instance" description: "Whether event creation is allowed on this instance"
) )
field(:antispam, :boolean, description: "Whether anti-spam is activated on this instance")
end end
@desc """ @desc """
The instance's restrictions The instance's restrictions
""" """
object :restrictions do object :restrictions do
meta(:authorize, :all)
field(:only_admin_can_create_groups, :boolean, field(:only_admin_can_create_groups, :boolean,
description: "Whether groups creation is allowed only for admin, not for all users" description: "Whether groups creation is allowed only for admin, not for all users"
) )
@ -304,6 +336,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
The instance's auth configuration The instance's auth configuration
""" """
object :auth do object :auth do
meta(:authorize, :all)
field(:ldap, :boolean, description: "Whether or not LDAP auth is enabled") field(:ldap, :boolean, description: "Whether or not LDAP auth is enabled")
field(:database_login, :boolean, description: "Whether or not database login is enabled") field(:database_login, :boolean, description: "Whether or not database login is enabled")
field(:oauth_providers, list_of(:oauth_provider), description: "List of oauth providers") field(:oauth_providers, list_of(:oauth_provider), description: "List of oauth providers")
@ -313,6 +346,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
An oAuth Provider An oAuth Provider
""" """
object :oauth_provider do object :oauth_provider do
meta(:authorize, :all)
field(:id, :string, description: "The provider ID") field(:id, :string, description: "The provider ID")
field(:label, :string, description: "The label for the auth provider") field(:label, :string, description: "The label for the auth provider")
end end
@ -321,21 +355,25 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
An upload limits configuration An upload limits configuration
""" """
object :upload_limits do object :upload_limits do
meta(:authorize, :all)
field(:default, :integer, description: "The default limitation, in bytes") field(:default, :integer, description: "The default limitation, in bytes")
field(:avatar, :integer, description: "The avatar limitation, in bytes") field(:avatar, :integer, description: "The avatar limitation, in bytes")
field(:banner, :integer, description: "The banner limitation, in bytes") field(:banner, :integer, description: "The banner limitation, in bytes")
end end
object :instance_feeds do object :instance_feeds do
meta(:authorize, :all)
field(:enabled, :boolean, description: "Whether the instance-wide feeds are enabled") field(:enabled, :boolean, description: "Whether the instance-wide feeds are enabled")
end end
object :web_push do object :web_push do
meta(:authorize, :all)
field(:enabled, :boolean, description: "Whether the WebPush feature is enabled") field(:enabled, :boolean, description: "Whether the WebPush feature is enabled")
field(:public_key, :string, description: "The server's public WebPush VAPID key") field(:public_key, :string, description: "The server's public WebPush VAPID key")
end end
object :analytics do object :analytics do
meta(:authorize, :all)
field(:id, :string, description: "ID of the analytics service") field(:id, :string, description: "ID of the analytics service")
field(:enabled, :boolean, description: "Whether the service is activated or not") field(:enabled, :boolean, description: "Whether the service is activated or not")
@ -352,16 +390,19 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
end end
object :analytics_configuration do object :analytics_configuration do
meta(:authorize, :all)
field(:key, :string, description: "The key for the analytics configuration element") field(:key, :string, description: "The key for the analytics configuration element")
field(:value, :string, description: "The value 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") field(:type, :analytics_configuration_type, description: "The analytics configuration type")
end end
object :search_settings do object :search_settings do
meta(:authorize, :all)
field(:global, :global_search_settings, description: "The instance's global search settings") field(:global, :global_search_settings, description: "The instance's global search settings")
end end
object :global_search_settings do object :global_search_settings do
meta(:authorize, :all)
field(:is_enabled, :boolean, description: "Whether global search is enabled") field(:is_enabled, :boolean, description: "Whether global search is enabled")
field(:is_default, :boolean, description: "Whether global search is the default") field(:is_default, :boolean, description: "Whether global search is the default")
end end
@ -370,6 +411,8 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Export formats configuration Export formats configuration
""" """
object :export_formats do object :export_formats do
meta(:authorize, :all)
field(:event_participants, list_of(:string), field(:event_participants, list_of(:string),
description: "The list of formats the event participants can be exported to" description: "The list of formats the event participants can be exported to"
) )
@ -379,6 +422,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
Event categories list configuration Event categories list configuration
""" """
object :event_category_option do object :event_category_option do
meta(:authorize, :all)
field(:id, :string, description: "The ID of the event category") field(:id, :string, description: "The ID of the event category")
field(:label, :string, description: "The translated name of the event category") field(:label, :string, description: "The translated name of the event category")
end end
@ -386,6 +430,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
object :config_queries do object :config_queries do
@desc "Get the instance config" @desc "Get the instance config"
field :config, :config do field :config, :config do
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Config.get_config/3) resolve(&Config.get_config/3)
end end
end end

View file

@ -11,6 +11,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@desc "A comment" @desc "A comment"
object :comment do object :comment do
meta(:authorize, :all)
interfaces([:action_log_object, :activity_object]) interfaces([:action_log_object, :activity_object])
field(:id, :id, description: "Internal ID for this comment") field(:id, :id, description: "Internal ID for this comment")
field(:uuid, :uuid, description: "An UUID for this comment") field(:uuid, :uuid, description: "An UUID for this comment")
@ -73,6 +74,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@desc "A paginated list of comments" @desc "A paginated list of comments"
object :paginated_comment_list do object :paginated_comment_list do
meta(:authorize, :all)
field(:elements, list_of(:comment), description: "A list of comments") field(:elements, list_of(:comment), description: "A list of comments")
field(:total, :integer, description: "The total number of comments in the list") field(:total, :integer, description: "The total number of comments in the list")
end end
@ -81,6 +83,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
@desc "Get replies for thread" @desc "Get replies for thread"
field :thread, type: list_of(:comment) do field :thread, type: list_of(:comment) do
arg(:id, non_null(:id), description: "The comment ID") arg(:id, non_null(:id), description: "The comment ID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Comment.get_thread/3) resolve(&Comment.get_thread/3)
end end
end end
@ -95,6 +98,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?") arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Comment,
rule: :"write:comment:create",
args: %{event_id: :event_id}
)
resolve(&Comment.create_comment/3) resolve(&Comment.create_comment/3)
end end
@ -106,6 +116,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?") arg(:is_announcement, :boolean, description: "Should this comment be announced to everyone?")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Comment,
rule: :"write:comment:update",
args: %{id: :comment_id}
)
resolve(&Comment.update_comment/3) resolve(&Comment.update_comment/3)
end end
@ -113,6 +130,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.CommentType do
field :delete_comment, type: :comment do field :delete_comment, type: :comment do
arg(:comment_id, non_null(:id), description: "The comment ID") arg(:comment_id, non_null(:id), description: "The comment ID")
middleware(Rajska.QueryAuthorization,
permit: [:user, :moderator],
scope: Mobilizon.Discussions.Comment,
rule: :"write:comment:delete",
args: %{id: :comment_id}
)
resolve(&Comment.delete_comment/3) resolve(&Comment.delete_comment/3)
end end
end end

View file

@ -11,6 +11,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
@desc "A discussion" @desc "A discussion"
object :discussion do object :discussion do
meta(:authorize, :user)
interfaces([:activity_object]) interfaces([:activity_object])
field(:id, :id, description: "Internal ID for this discussion") field(:id, :id, description: "Internal ID for this discussion")
field(:title, :string, description: "The title for this discussion") field(:title, :string, description: "The title for this discussion")
@ -36,6 +37,7 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
@desc "A paginated list of discussions" @desc "A paginated list of discussions"
object :paginated_discussion_list do object :paginated_discussion_list do
meta(:authorize, :user)
field(:elements, list_of(:discussion), description: "A list of discussion") field(:elements, list_of(:discussion), description: "A list of discussion")
field(:total, :integer, description: "The total number of discussions in the list") field(:total, :integer, description: "The total number of discussions in the list")
end end
@ -45,6 +47,14 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :discussion, type: :discussion do field :discussion, type: :discussion do
arg(:id, :id, description: "The discussion's ID") arg(:id, :id, description: "The discussion's ID")
arg(:slug, :string, description: "The discussion's slug") arg(:slug, :string, description: "The discussion's slug")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"read:group:discussions",
args: %{slug: :slug}
)
resolve(&Discussion.get_discussion/3) resolve(&Discussion.get_discussion/3)
end end
end end
@ -56,6 +66,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
arg(:text, non_null(:string), description: "The discussion's first comment body") arg(:text, non_null(:string), description: "The discussion's first comment body")
arg(:actor_id, non_null(:id), description: "The discussion's group ID") arg(:actor_id, non_null(:id), description: "The discussion's group ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:create",
args: %{actor_id: :actor_id}
)
resolve(&Discussion.create_discussion/3) resolve(&Discussion.create_discussion/3)
end end
@ -63,6 +80,14 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :reply_to_discussion, type: :discussion do field :reply_to_discussion, type: :discussion do
arg(:discussion_id, non_null(:id), description: "The discussion's ID") arg(:discussion_id, non_null(:id), description: "The discussion's ID")
arg(:text, non_null(:string), description: "The discussion's reply body") arg(:text, non_null(:string), description: "The discussion's reply body")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:update",
args: %{id: :discussion_id}
)
resolve(&Discussion.reply_to_discussion/3) resolve(&Discussion.reply_to_discussion/3)
end end
@ -70,6 +95,14 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :update_discussion, type: :discussion do field :update_discussion, type: :discussion do
arg(:title, non_null(:string), description: "The updated discussion's title") arg(:title, non_null(:string), description: "The updated discussion's title")
arg(:discussion_id, non_null(:id), description: "The discussion's ID") arg(:discussion_id, non_null(:id), description: "The discussion's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:update",
args: %{id: :discussion_id}
)
resolve(&Discussion.update_discussion/3) resolve(&Discussion.update_discussion/3)
end end
@ -77,6 +110,13 @@ defmodule Mobilizon.GraphQL.Schema.Discussions.DiscussionType do
field :delete_discussion, type: :discussion do field :delete_discussion, type: :discussion do
arg(:discussion_id, non_null(:id), description: "The discussion's ID") arg(:discussion_id, non_null(:id), description: "The discussion's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Discussions.Discussion,
rule: :"write:group:discussion:delete",
args: %{id: :discussion_id}
)
resolve(&Discussion.delete_discussion/3) resolve(&Discussion.delete_discussion/3)
end end
end end

View file

@ -15,8 +15,13 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
import_types(Schema.Events.ParticipantType) import_types(Schema.Events.ParticipantType)
import_types(Schema.TagType) import_types(Schema.TagType)
@env Application.compile_env(:mobilizon, :env)
@event_rate_limiting 60
@desc "An event" @desc "An event"
object :event do object :event do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:action_log_object, :interactable, :activity_object, :event_search_result]) interfaces([:action_log_object, :interactable, :activity_object, :event_search_result])
field(:id, :id, description: "Internal ID for this event") field(:id, :id, description: "Internal ID for this event")
field(:uuid, :uuid, description: "The Event UUID") field(:uuid, :uuid, description: "The Event UUID")
@ -61,10 +66,9 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "The event's organizer (as a person)" description: "The event's organizer (as a person)"
) )
field(:tags, list_of(:tag), field(:tags, list_of(:tag), description: "The event's tags") do
resolve: &Tag.list_tags_for_event/3, resolve(&Tag.list_tags_for_event/3)
description: "The event's tags" end
)
field(:category, :event_category, description: "The event's category") field(:category, :event_category, description: "The event's category")
@ -75,7 +79,10 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
resolve: &Event.stats_participants/3 resolve: &Event.stats_participants/3
) )
field(:participants, :paginated_participant_list, description: "The event's participants") do field(:participants, :paginated_participant_list,
description: "The event's participants",
meta: [private: true, rule: :"read:event:participants"]
) do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
description: "The page in the paginated participants list" description: "The page in the paginated participants list"
@ -134,12 +141,14 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
@desc "A paginated list of events" @desc "A paginated list of events"
object :paginated_event_list do object :paginated_event_list do
meta(:authorize, :all)
field(:elements, list_of(:event), description: "A list of events") field(:elements, list_of(:event), description: "A list of events")
field(:total, :integer, description: "The total number of events in the list") field(:total, :integer, description: "The total number of events in the list")
end end
@desc "Participation statistics" @desc "Participation statistics"
object :participant_stats do object :participant_stats do
meta(:authorize, :all)
field(:going, :integer, description: "The number of approved participants") field(:going, :integer, description: "The number of approved participants")
field(:not_approved, :integer, description: "The number of not approved participants") field(:not_approved, :integer, description: "The number of not approved participants")
field(:not_confirmed, :integer, description: "The number of not confirmed participants") field(:not_confirmed, :integer, description: "The number of not confirmed participants")
@ -158,6 +167,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
An event offer An event offer
""" """
object :event_offer do object :event_offer do
meta(:authorize, :all)
field(:price, :float, description: "The price amount for this offer") field(:price, :float, description: "The price amount for this offer")
field(:price_currency, :string, description: "The currency for this price offer") field(:price_currency, :string, description: "The currency for this price offer")
field(:url, :string, description: "The URL to access to this offer") field(:url, :string, description: "The URL to access to this offer")
@ -167,6 +177,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
An event participation condition An event participation condition
""" """
object :event_participation_condition do object :event_participation_condition do
meta(:authorize, :all)
field(:title, :string, description: "The title for this condition") field(:title, :string, description: "The title for this condition")
field(:content, :string, description: "The content for this condition") field(:content, :string, description: "The content for this condition")
field(:url, :string, description: "The URL to access this condition") field(:url, :string, description: "The URL to access this condition")
@ -201,6 +212,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
Event options Event options
""" """
object :event_options do object :event_options do
meta(:authorize, :all)
field(:maximum_attendee_capacity, :integer, field(:maximum_attendee_capacity, :integer,
description: "The maximum attendee capacity for this event" description: "The maximum attendee capacity for this event"
) )
@ -307,6 +320,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
end end
object :event_metadata do object :event_metadata do
meta(:authorize, :all)
field(:key, :string, description: "The key for the metadata") field(:key, :string, description: "The key for the metadata")
field(:title, :string, description: "The title for the metadata") field(:title, :string, description: "The title for the metadata")
field(:value, :string, description: "The value for the metadata") field(:value, :string, description: "The value for the metadata")
@ -350,12 +364,15 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
description: "Direction for the sort" description: "Direction for the sort"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Event.list_events/3) resolve(&Event.list_events/3)
end end
@desc "Get an event by uuid" @desc "Get an event by uuid"
field :event, :event do field :event, :event do
arg(:uuid, non_null(:uuid), description: "The event's UUID") arg(:uuid, non_null(:uuid), description: "The event's UUID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Event.find_event/3) resolve(&Event.find_event/3)
end end
end end
@ -416,6 +433,15 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
arg(:contacts, list_of(:contact), default_value: [], description: "The events contacts") arg(:contacts, list_of(:contact), default_value: [], description: "The events contacts")
arg(:language, :string, description: "The event language", default_value: "und") arg(:language, :string, description: "The event language", default_value: "und")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Event,
rule: :"write:event:create",
args: %{organizer_actor_id: :organizer_actor_id}
)
middleware(Rajska.RateLimiter, limit: event_rate_limiting(@env))
resolve(&Event.create_event/3) resolve(&Event.create_event/3)
end end
@ -460,6 +486,13 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
arg(:contacts, list_of(:contact), default_value: [], description: "The events contacts") arg(:contacts, list_of(:contact), default_value: [], description: "The events contacts")
arg(:language, :string, description: "The event language", default_value: "und") arg(:language, :string, description: "The event language", default_value: "und")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Event,
args: %{id: :event_id},
rule: :"write:event:update"
)
resolve(&Event.update_event/3) resolve(&Event.update_event/3)
end end
@ -467,7 +500,17 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
field :delete_event, :deleted_object do field :delete_event, :deleted_object do
arg(:event_id, non_null(:id), description: "The event ID to delete") arg(:event_id, non_null(:id), description: "The event ID to delete")
middleware(Rajska.QueryAuthorization,
permit: [:user, :moderator, :administrator],
scope: Mobilizon.Events.Event,
rule: :"write:event:delete",
args: %{id: :event_id}
)
resolve(&Event.delete_event/3) resolve(&Event.delete_event/3)
end end
end end
defp event_rate_limiting(:test), do: @event_rate_limiting * 1000
defp event_rate_limiting(_), do: @event_rate_limiting
end end

View file

@ -17,6 +17,8 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
or an Atom feed for just a profile. or an Atom feed for just a profile.
""" """
object :feed_token do object :feed_token do
meta(:authorize, :user)
field( field(
:actor, :actor,
:actor, :actor,
@ -36,6 +38,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
@desc "Represents a deleted feed_token" @desc "Represents a deleted feed_token"
object :deleted_feed_token do object :deleted_feed_token do
meta(:authorize, :user)
field(:user, :deleted_object, description: "The user that owned the deleted feed token") field(:user, :deleted_object, description: "The user that owned the deleted feed token")
field(:actor, :deleted_object, description: "The actor that owned the deleted feed token") field(:actor, :deleted_object, description: "The actor that owned the deleted feed token")
end end
@ -45,6 +48,13 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
field :create_feed_token, :feed_token do field :create_feed_token, :feed_token do
arg(:actor_id, :id, description: "The actor ID for the feed token") arg(:actor_id, :id, description: "The actor ID for the feed token")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.FeedToken,
rule: :"write:profile:feed_token:create",
args: %{}
)
resolve(&FeedToken.create_feed_token/3) resolve(&FeedToken.create_feed_token/3)
end end
@ -52,6 +62,13 @@ defmodule Mobilizon.GraphQL.Schema.Events.FeedTokenType do
field :delete_feed_token, :deleted_feed_token do field :delete_feed_token, :deleted_feed_token do
arg(:token, non_null(:string), description: "The token to delete") arg(:token, non_null(:string), description: "The token to delete")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.FeedToken,
rule: :"write:feed_token:delete",
args: %{token: :token}
)
resolve(&FeedToken.delete_feed_token/3) resolve(&FeedToken.delete_feed_token/3)
end end
end end

View file

@ -12,6 +12,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
@desc "Represents a participant to an event" @desc "Represents a participant to an event"
object :participant do object :participant do
meta(:authorize, :all)
field(:id, :id, description: "The participation ID") field(:id, :id, description: "The participation ID")
field( field(
@ -41,6 +42,8 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
Metadata about a participant Metadata about a participant
""" """
object :participant_metadata do object :participant_metadata do
meta(:authorize, :all)
field(:cancellation_token, :string, field(:cancellation_token, :string,
description: "The eventual token to leave an event when user is anonymous" description: "The eventual token to leave an event when user is anonymous"
) )
@ -53,6 +56,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
A paginated list of participants A paginated list of participants
""" """
object :paginated_participant_list do object :paginated_participant_list do
meta(:authorize, :user)
field(:elements, list_of(:participant), description: "A list of participants") field(:elements, list_of(:participant), description: "A list of participants")
field(:total, :integer, description: "The total number of participants in the list") field(:total, :integer, description: "The total number of participants in the list")
end end
@ -78,6 +82,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
@desc "Represents a deleted participant" @desc "Represents a deleted participant"
object :deleted_participant do object :deleted_participant do
meta(:authorize, :all)
field(:id, :id, description: "The participant ID") field(:id, :id, description: "The participant ID")
field(:event, :deleted_object, description: "The participant's event") field(:event, :deleted_object, description: "The participant's event")
field(:actor, :deleted_object, description: "The participant's actor") field(:actor, :deleted_object, description: "The participant's actor")
@ -92,7 +97,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
arg(:message, :string, description: "The anonymous participant's message") arg(:message, :string, description: "The anonymous participant's message")
arg(:locale, :string, description: "The anonymous participant's locale") arg(:locale, :string, description: "The anonymous participant's locale")
arg(:timezone, :string, description: "The anonymous participant's timezone") arg(:timezone, :string, description: "The anonymous participant's timezone")
middleware(Rajska.QueryAuthorization, permit: :all, rule: :"write:participation")
resolve(&Participant.actor_join_event/3) resolve(&Participant.actor_join_event/3)
end end
@ -101,7 +106,7 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
arg(:event_id, non_null(:id), description: "The event ID the participant left") arg(:event_id, non_null(:id), description: "The event ID the participant left")
arg(:actor_id, non_null(:id), description: "The actor ID for the participant") arg(:actor_id, non_null(:id), description: "The actor ID for the participant")
arg(:token, :string, description: "The anonymous participant participation token") arg(:token, :string, description: "The anonymous participant participation token")
middleware(Rajska.QueryAuthorization, permit: :all, rule: :"write:participation")
resolve(&Participant.actor_leave_event/3) resolve(&Participant.actor_leave_event/3)
end end
@ -110,12 +115,19 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
arg(:id, non_null(:id), description: "The participant ID") arg(:id, non_null(:id), description: "The participant ID")
arg(:role, non_null(:participant_role_enum), description: "The participant new role") arg(:role, non_null(:participant_role_enum), description: "The participant new role")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Participant,
rule: :"write:participation"
)
resolve(&Participant.update_participation/3) resolve(&Participant.update_participation/3)
end end
@desc "Confirm a participation" @desc "Confirm a participation"
field :confirm_participation, :participant do field :confirm_participation, :participant do
arg(:confirmation_token, non_null(:string), description: "The participation token") arg(:confirmation_token, non_null(:string), description: "The participation token")
middleware(Rajska.QueryAuthorization, permit: :all, rule: :"write:participation")
resolve(&Participant.confirm_participation_from_token/3) resolve(&Participant.confirm_participation_from_token/3)
end end
@ -131,6 +143,14 @@ defmodule Mobilizon.GraphQL.Schema.Events.ParticipantType do
) )
arg(:format, :export_format_enum, description: "The format in which to return the file") arg(:format, :export_format_enum, description: "The format in which to return the file")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Events.Event,
rule: :"read:event:participants:export",
args: %{id: :event_id}
)
resolve(&Participant.export_event_participants/3) resolve(&Participant.export_event_participants/3)
end end
end end

View file

@ -6,12 +6,14 @@ defmodule Mobilizon.GraphQL.Schema.FollowedGroupActivityType do
@desc "A paginated list of follow group events" @desc "A paginated list of follow group events"
object :paginated_followed_group_events do object :paginated_followed_group_events do
meta(:authorize, :user)
field(:elements, list_of(:followed_group_event), description: "A list of follow group events") field(:elements, list_of(:followed_group_event), description: "A list of follow group events")
field(:total, :integer, description: "The total number of follow group events in the list") field(:total, :integer, description: "The total number of follow group events in the list")
end end
@desc "A follow group event" @desc "A follow group event"
object :followed_group_event do object :followed_group_event do
meta(:authorize, :user)
field(:user, :user) field(:user, :user)
field(:profile, :person) field(:profile, :person)
field(:group, :group) field(:group, :group)

View file

@ -6,8 +6,12 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
alias Mobilizon.GraphQL.Resolvers.Media alias Mobilizon.GraphQL.Resolvers.Media
@env Application.compile_env(:mobilizon, :env)
@media_rate_limiting 60
@desc "A media" @desc "A media"
object :media do object :media do
meta(:authorize, :all)
field(:id, :id, description: "The media's ID") field(:id, :id, description: "The media's ID")
field(:alt, :string, description: "The media's alternative text") field(:alt, :string, description: "The media's alternative text")
field(:name, :string, description: "The media's name") field(:name, :string, description: "The media's name")
@ -21,6 +25,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
A paginated list of medias A paginated list of medias
""" """
object :paginated_media_list do object :paginated_media_list do
meta(:authorize, :all)
field(:elements, list_of(:media), description: "The list of medias") field(:elements, list_of(:media), description: "The list of medias")
field(:total, :integer, description: "The total number of medias in the list") field(:total, :integer, description: "The total number of medias in the list")
end end
@ -29,6 +34,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
Some metadata associated with a media Some metadata associated with a media
""" """
object :media_metadata do object :media_metadata do
meta(:authorize, :all)
field(:width, :integer, description: "The media width (if a picture)") field(:width, :integer, description: "The media width (if a picture)")
field(:height, :integer, description: "The media width (if a height)") field(:height, :integer, description: "The media width (if a height)")
field(:blurhash, :string, description: "The media blurhash (if a picture") field(:blurhash, :string, description: "The media blurhash (if a picture")
@ -54,6 +60,7 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
@desc "Get a media" @desc "Get a media"
field :media, :media do field :media, :media do
arg(:id, non_null(:id), description: "The media ID") arg(:id, non_null(:id), description: "The media ID")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Media.media/3) resolve(&Media.media/3)
end end
end end
@ -64,6 +71,17 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
arg(:name, non_null(:string), description: "The media's name") arg(:name, non_null(:string), description: "The media's name")
arg(:alt, :string, description: "The media's alternative text") arg(:alt, :string, description: "The media's alternative text")
arg(:file, non_null(:upload), description: "The media file") arg(:file, non_null(:upload), description: "The media file")
arg(:actor_id, :id, description: "The actor that uploads the media")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Medias.Media,
rule: :"write:media:upload",
args: %{}
)
middleware(Rajska.RateLimiter, limit: media_rate_limiting(@env))
resolve(&Media.upload_media/3) resolve(&Media.upload_media/3)
end end
@ -72,7 +90,17 @@ defmodule Mobilizon.GraphQL.Schema.MediaType do
""" """
field :remove_media, :deleted_object do field :remove_media, :deleted_object do
arg(:id, non_null(:id), description: "The media's ID") arg(:id, non_null(:id), description: "The media's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Medias.Media,
rule: :"write:media:remove"
)
resolve(&Media.remove_media/3) resolve(&Media.remove_media/3)
end end
end end
defp media_rate_limiting(:test), do: @media_rate_limiting * 1000
defp media_rate_limiting(_), do: @media_rate_limiting
end end

View file

@ -7,6 +7,7 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
@desc "A post" @desc "A post"
object :post do object :post do
meta(:authorize, :all)
interfaces([:activity_object]) interfaces([:activity_object])
field(:id, :id, description: "The post's ID") field(:id, :id, description: "The post's ID")
field(:title, :string, description: "The post's title") field(:title, :string, description: "The post's title")
@ -22,21 +23,20 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
field(:updated_at, :datetime, description: "The post's last update date") field(:updated_at, :datetime, description: "The post's last update date")
field(:language, :string, description: "The post language") field(:language, :string, description: "The post language")
field(:tags, list_of(:tag), field(:tags, list_of(:tag), description: "The post's tags") do
resolve: &Tag.list_tags_for_post/3, resolve(&Tag.list_tags_for_post/3)
description: "The post's tags" end
)
field(:picture, :media, field(:picture, :media, description: "The posts's media") do
description: "The posts's media", resolve(&Media.media/3)
resolve: &Media.media/3 end
)
end end
@desc """ @desc """
A paginated list of posts A paginated list of posts
""" """
object :paginated_post_list do object :paginated_post_list do
meta(:authorize, :all)
field(:elements, list_of(:post), description: "A list of posts") field(:elements, list_of(:post), description: "A list of posts")
field(:total, :integer, description: "The total number of posts in the list") field(:total, :integer, description: "The total number of posts in the list")
end end
@ -56,6 +56,7 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
@desc "Get a post" @desc "Get a post"
field :post, :post do field :post, :post do
arg(:slug, non_null(:string), description: "The post's slug") arg(:slug, non_null(:string), description: "The post's slug")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Post.get_post/3) resolve(&Post.get_post/3)
end end
end end
@ -84,6 +85,13 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
"The banner for the post, either as an object or directly the ID of an existing media" "The banner for the post, either as an object or directly the ID of an existing media"
) )
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Posts.Post,
rule: :"write:group:post:create",
args: %{}
)
resolve(&Post.create_post/3) resolve(&Post.create_post/3)
end end
@ -108,12 +116,25 @@ defmodule Mobilizon.GraphQL.Schema.PostType do
"The banner for the post, either as an object or directly the ID of an existing media" "The banner for the post, either as an object or directly the ID of an existing media"
) )
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Posts.Post,
rule: :"write:group:post:update"
)
resolve(&Post.update_post/3) resolve(&Post.update_post/3)
end end
@desc "Delete a post" @desc "Delete a post"
field :delete_post, :deleted_object do field :delete_post, :deleted_object do
arg(:id, non_null(:id), description: "The post's ID") arg(:id, non_null(:id), description: "The post's ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Posts.Post,
rule: :"write:group:post:delete"
)
resolve(&Post.delete_post/3) resolve(&Post.delete_post/3)
end end
end end

View file

@ -11,11 +11,12 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
@desc "A report object" @desc "A report object"
object :report do object :report do
meta(:authorize, :all)
interfaces([:action_log_object]) interfaces([:action_log_object])
field(:id, :id, description: "The internal ID of the report") field(:id, :id, description: "The internal ID of the report")
field(:content, :string, description: "The comment the reporter added about this report") field(:content, :string, description: "The comment the reporter added about this report")
field(:status, :report_status, description: "Whether the report is still active") field(:status, :report_status, description: "Whether the report is still active")
field(:uri, :string, description: "The URI of the report") field(:uri, :string, description: "The URI of the report", meta: [private: true])
field(:reported, :actor, description: "The actor that is being reported") field(:reported, :actor, description: "The actor that is being reported")
field(:reporter, :actor, description: "The actor that created the report") field(:reporter, :actor, description: "The actor that created the report")
field(:event, :event, description: "The event that is being reported") field(:event, :event, description: "The event that is being reported")
@ -23,6 +24,7 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
field(:notes, list_of(:report_note), field(:notes, list_of(:report_note),
description: "The notes made on the event", description: "The notes made on the event",
meta: [private: true],
resolve: dataloader(Reports) resolve: dataloader(Reports)
) )
@ -31,12 +33,14 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
end end
object :paginated_report_list do object :paginated_report_list do
meta(:authorize, :moderator)
field(:elements, list_of(:report), description: "A list of reports") field(:elements, list_of(:report), description: "A list of reports")
field(:total, :integer, description: "The total number of reports in the list") field(:total, :integer, description: "The total number of reports in the list")
end end
@desc "A report note object" @desc "A report note object"
object :report_note do object :report_note do
meta(:authorize, :moderator)
interfaces([:action_log_object]) interfaces([:action_log_object])
field(:id, :id, description: "The internal ID of the report note") field(:id, :id, description: "The internal ID of the report note")
field(:content, :string, description: "The content of the note") field(:content, :string, description: "The content of the note")
@ -73,12 +77,20 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
arg(:limit, :integer, default_value: 10, description: "The limit of reports per page") arg(:limit, :integer, default_value: 10, description: "The limit of reports per page")
arg(:status, :report_status, default_value: :open, description: "Filter reports by status") arg(:status, :report_status, default_value: :open, description: "Filter reports by status")
arg(:domain, :string, default_value: nil, description: "Filter reports by domain name") arg(:domain, :string, default_value: nil, description: "Filter reports by domain name")
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Report,
args: %{}
)
resolve(&Report.list_reports/3) resolve(&Report.list_reports/3)
end end
@desc "Get a report by id" @desc "Get a report by id"
field :report, :report do field :report, :report do
arg(:id, non_null(:id), description: "The report ID") arg(:id, non_null(:id), description: "The report ID")
middleware(Rajska.QueryAuthorization, permit: :moderator, scope: Mobilizon.Reports.Report)
resolve(&Report.get_report/3) resolve(&Report.get_report/3)
end end
end end
@ -101,6 +113,8 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
"Whether to forward the report to the original instance if the content is remote" "Whether to forward the report to the original instance if the content is remote"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Report.create_report/3) resolve(&Report.create_report/3)
end end
@ -113,6 +127,12 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
description: "The feedback to send to the anti-spam system" description: "The feedback to send to the anti-spam system"
) )
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Report,
args: %{id: :report_id}
)
resolve(&Report.update_report/3) resolve(&Report.update_report/3)
end end
@ -120,12 +140,26 @@ defmodule Mobilizon.GraphQL.Schema.ReportType do
field :create_report_note, type: :report_note do field :create_report_note, type: :report_note do
arg(:content, :string, description: "The note's content") arg(:content, :string, description: "The note's content")
arg(:report_id, non_null(:id), description: "The report's ID") arg(:report_id, non_null(:id), description: "The report's ID")
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Report,
args: %{id: :report_id}
)
resolve(&Report.create_report_note/3) resolve(&Report.create_report_note/3)
end end
@desc "Delete a note on a report" @desc "Delete a note on a report"
field :delete_report_note, type: :deleted_object do field :delete_report_note, type: :deleted_object do
arg(:note_id, non_null(:id), description: "The note's ID") arg(:note_id, non_null(:id), description: "The note's ID")
middleware(Rajska.QueryAuthorization,
permit: :moderator,
scope: Mobilizon.Reports.Note,
args: %{id: :note_id}
)
resolve(&Report.delete_report_note/3) resolve(&Report.delete_report_note/3)
end end
end end

View file

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
@desc "A resource" @desc "A resource"
object :resource do object :resource do
meta(:authorize, :user)
interfaces([:activity_object]) interfaces([:activity_object])
field(:id, :id, description: "The resource's ID") field(:id, :id, description: "The resource's ID")
field(:title, :string, description: "The resource's title") field(:title, :string, description: "The resource's title")
@ -44,6 +45,7 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
A paginated list of resources A paginated list of resources
""" """
object :paginated_resource_list do object :paginated_resource_list do
meta(:authorize, :user)
field(:elements, list_of(:resource), description: "A list of resources") field(:elements, list_of(:resource), description: "A list of resources")
field(:total, :integer, description: "The total number of resources in the list") field(:total, :integer, description: "The total number of resources in the list")
end end
@ -52,6 +54,7 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
The metadata associated to the resource The metadata associated to the resource
""" """
object :resource_metadata do object :resource_metadata do
meta(:authorize, :user)
field(:type, :string, description: "The type of the resource") field(:type, :string, description: "The type of the resource")
field(:title, :string, description: "The resource's metadata title") field(:title, :string, description: "The resource's metadata title")
field(:description, :string, description: "The resource's metadata description") field(:description, :string, description: "The resource's metadata description")
@ -84,6 +87,13 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
description: "The federated username for the group resource" description: "The federated username for the group resource"
) )
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"read:group:resources",
args: %{path: :path}
)
resolve(&Resource.get_resource/3) resolve(&Resource.get_resource/3)
end end
end end
@ -101,6 +111,13 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
arg(:resource_url, :string, description: "This resource's own original URL") arg(:resource_url, :string, description: "This resource's own original URL")
arg(:type, :string, default_value: "link", description: "The type for this resource") arg(:type, :string, default_value: "link", description: "The type for this resource")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"write:group:resources:create",
args: %{actor_id: :actor_id}
)
resolve(&Resource.create_resource/3) resolve(&Resource.create_resource/3)
end end
@ -112,18 +129,39 @@ defmodule Mobilizon.GraphQL.Schema.ResourceType do
arg(:parent_id, :id, description: "The new resource parent ID (if the resource is moved)") arg(:parent_id, :id, description: "The new resource parent ID (if the resource is moved)")
arg(:resource_url, :string, description: "The new resource URL") arg(:resource_url, :string, description: "The new resource URL")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"write:group:resources:update"
)
resolve(&Resource.update_resource/3) resolve(&Resource.update_resource/3)
end end
@desc "Delete a resource" @desc "Delete a resource"
field :delete_resource, :deleted_object do field :delete_resource, :deleted_object do
arg(:id, non_null(:id), description: "The resource ID") arg(:id, non_null(:id), description: "The resource ID")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"write:group:resources:delete"
)
resolve(&Resource.delete_resource/3) resolve(&Resource.delete_resource/3)
end end
@desc "Get a preview for a resource link" @desc "Get a preview for a resource link"
field :preview_resource_link, :resource_metadata do field :preview_resource_link, :resource_metadata do
arg(:resource_url, non_null(:string), description: "The link to crawl to get of preview of") arg(:resource_url, non_null(:string), description: "The link to crawl to get of preview of")
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: Mobilizon.Resources.Resource,
rule: :"read:group:resources",
args: %{}
)
resolve(&Resource.preview_resource_link/3) resolve(&Resource.preview_resource_link/3)
end end
end end

View file

@ -10,6 +10,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
alias Mobilizon.Service.GlobalSearch.{EventResult, GroupResult} alias Mobilizon.Service.GlobalSearch.{EventResult, GroupResult}
interface :event_search_result do interface :event_search_result do
meta(:authorize, :all)
field(:id, :id, description: "Internal ID for this event") field(:id, :id, description: "Internal ID for this event")
field(:uuid, :uuid, description: "The Event UUID") field(:uuid, :uuid, description: "The Event UUID")
field(:url, :string, description: "The ActivityPub Event URL") field(:url, :string, description: "The ActivityPub Event URL")
@ -43,6 +44,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
@desc "Search event result" @desc "Search event result"
object :event_result do object :event_result do
meta(:authorize, :all)
interfaces([:event_search_result]) interfaces([:event_search_result])
field(:id, :id, description: "Internal ID for this event") field(:id, :id, description: "Internal ID for this event")
field(:uuid, :uuid, description: "The Event UUID") field(:uuid, :uuid, description: "The Event UUID")
@ -65,6 +67,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
end end
interface :group_search_result do interface :group_search_result do
meta(:authorize, :all)
field(:id, :id, description: "Internal ID for this group") field(:id, :id, description: "Internal ID for this group")
field(:url, :string, description: "The ActivityPub actor's URL") field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
@ -92,6 +95,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
@desc "Search group result" @desc "Search group result"
object :group_result do object :group_result do
meta(:authorize, :all)
interfaces([:group_search_result]) interfaces([:group_search_result])
field(:id, :id, description: "Internal ID for this group") field(:id, :id, description: "Internal ID for this group")
field(:url, :string, description: "The ActivityPub actor's URL") field(:url, :string, description: "The ActivityPub actor's URL")
@ -109,18 +113,21 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
@desc "Search persons result" @desc "Search persons result"
object :persons do object :persons do
meta(:authorize, [:administrator, :moderator])
field(:total, non_null(:integer), description: "Total elements") field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:person)), description: "Person elements") field(:elements, non_null(list_of(:person)), description: "Person elements")
end end
@desc "Search groups result" @desc "Search groups result"
object :groups do object :groups do
meta(:authorize, :all)
field(:total, non_null(:integer), description: "Total elements") field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:group_search_result)), description: "Group elements") field(:elements, non_null(list_of(:group_search_result)), description: "Group elements")
end end
@desc "Search events result" @desc "Search events result"
object :events do object :events do
meta(:authorize, :all)
field(:total, non_null(:integer), description: "Total elements") field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:event_search_result)), description: "Event elements") field(:elements, non_null(list_of(:event_search_result)), description: "Event elements")
end end
@ -179,7 +186,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
arg(:term, :string, default_value: "", description: "Search term") arg(:term, :string, default_value: "", description: "Search term")
arg(:page, :integer, default_value: 1, description: "Result page") arg(:page, :integer, default_value: 1, description: "Result page")
arg(:limit, :integer, default_value: 10, description: "Results limit per page") arg(:limit, :integer, default_value: 10, description: "Results limit per page")
middleware(Rajska.QueryAuthorization, permit: [:administrator, :moderator], scope: false)
resolve(&Search.search_persons/3) resolve(&Search.search_persons/3)
end end
@ -225,6 +232,7 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "How to sort search results" description: "How to sort search results"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Search.search_groups/3) resolve(&Search.search_groups/3)
end end
@ -275,13 +283,14 @@ defmodule Mobilizon.GraphQL.Schema.SearchType do
description: "How to sort search results" description: "How to sort search results"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Search.search_events/3) resolve(&Search.search_events/3)
end end
@desc "Interact with an URI" @desc "Interact with an URI"
field :interact, :interactable do field :interact, :interactable do
arg(:uri, non_null(:string), description: "The URI for to interact with") arg(:uri, non_null(:string), description: "The URI for to interact with")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Search.interact/3) resolve(&Search.interact/3)
end end
end end

View file

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.StatisticsType do
@desc "A statistics object" @desc "A statistics object"
object :statistics do object :statistics do
meta(:authorize, :all)
# Instance name # Instance name
field(:number_of_users, :integer, description: "The number of local users") field(:number_of_users, :integer, description: "The number of local users")
field(:number_of_events, :integer, description: "The total number of events") field(:number_of_events, :integer, description: "The total number of events")
@ -27,6 +28,7 @@ defmodule Mobilizon.GraphQL.Schema.StatisticsType do
end end
object :category_statistics do object :category_statistics do
meta(:authorize, :all)
field(:key, :string, description: "The key for the category") field(:key, :string, description: "The key for the category")
field(:number, :integer, description: "The number of events for the given category") field(:number, :integer, description: "The number of events for the given category")
end end
@ -34,11 +36,13 @@ defmodule Mobilizon.GraphQL.Schema.StatisticsType do
object :statistics_queries do object :statistics_queries do
@desc "Get the instance statistics" @desc "Get the instance statistics"
field :statistics, :statistics do field :statistics, :statistics do
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Statistics.get_statistics/3) resolve(&Statistics.get_statistics/3)
end end
@desc "Get the instance's category statistics" @desc "Get the instance's category statistics"
field :category_statistics, list_of(:category_statistics) do field :category_statistics, list_of(:category_statistics) do
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&Statistics.get_category_statistics/3) resolve(&Statistics.get_category_statistics/3)
end end
end end

View file

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Schema.TagType do
@desc "A tag" @desc "A tag"
object :tag do object :tag do
meta(:authorize, :all)
field(:id, :id, description: "The tag's ID") field(:id, :id, description: "The tag's ID")
field(:slug, :string, description: "The tags's slug") field(:slug, :string, description: "The tags's slug")
field(:title, :string, description: "The tag's title") field(:title, :string, description: "The tag's title")
@ -26,6 +27,7 @@ defmodule Mobilizon.GraphQL.Schema.TagType do
arg(:filter, :string, description: "The filter to apply to the search") arg(:filter, :string, description: "The filter to apply to the search")
arg(:page, :integer, default_value: 1, description: "The page in the paginated tags list") arg(:page, :integer, default_value: 1, description: "The page in the paginated tags list")
arg(:limit, :integer, default_value: 10, description: "The limit of tags per page") arg(:limit, :integer, default_value: 10, description: "The limit of tags per page")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Tag.list_tags/3) resolve(&Tag.list_tags/3)
end end
end end

View file

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
@desc "A todo" @desc "A todo"
object :todo do object :todo do
meta(:authorize, :user)
field(:id, :id, description: "The todo's ID") field(:id, :id, description: "The todo's ID")
field(:title, :string, description: "The todo's title") field(:title, :string, description: "The todo's title")
field(:status, :boolean, description: "The todo's status") field(:status, :boolean, description: "The todo's status")
@ -30,6 +31,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
A paginated list of todos A paginated list of todos
""" """
object :paginated_todo_list do object :paginated_todo_list do
meta(:authorize, :user)
field(:elements, list_of(:todo), description: "A list of todos") field(:elements, list_of(:todo), description: "A list of todos")
field(:total, :integer, description: "The total number of todos in the list") field(:total, :integer, description: "The total number of todos in the list")
end end
@ -38,6 +40,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
@desc "Get a todo" @desc "Get a todo"
field :todo, :todo do field :todo, :todo do
arg(:id, non_null(:id), description: "The todo ID") arg(:id, non_null(:id), description: "The todo ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&TodoResolver.get_todo/3) resolve(&TodoResolver.get_todo/3)
end end
end end
@ -50,6 +53,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
arg(:status, :boolean, description: "The todo status") arg(:status, :boolean, description: "The todo status")
arg(:due_date, :datetime, description: "The todo due date") arg(:due_date, :datetime, description: "The todo due date")
arg(:assigned_to_id, :id, description: "The actor this todo is assigned to") arg(:assigned_to_id, :id, description: "The actor this todo is assigned to")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&TodoResolver.create_todo/3) resolve(&TodoResolver.create_todo/3)
end end
@ -62,7 +66,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoType do
arg(:status, :boolean, description: "The new todo status") arg(:status, :boolean, description: "The new todo status")
arg(:due_date, :datetime, description: "The new todo due date") arg(:due_date, :datetime, description: "The new todo due date")
arg(:assigned_to_id, :id, description: "The new id of the actor this todo is assigned to") arg(:assigned_to_id, :id, description: "The new id of the actor this todo is assigned to")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&TodoResolver.update_todo/3) resolve(&TodoResolver.update_todo/3)
end end

View file

@ -9,6 +9,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
@desc "A todo list" @desc "A todo list"
object :todo_list do object :todo_list do
meta(:authorize, :user)
field(:id, :id, description: "The todo list's ID") field(:id, :id, description: "The todo list's ID")
field(:title, :string, description: "The todo list's title") field(:title, :string, description: "The todo list's title")
@ -37,6 +38,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
A paginated list of todo-lists A paginated list of todo-lists
""" """
object :paginated_todo_list_list do object :paginated_todo_list_list do
meta(:authorize, :user)
field(:elements, list_of(:todo_list), description: "A list of todo lists") field(:elements, list_of(:todo_list), description: "A list of todo lists")
field(:total, :integer, description: "The total number of todo lists in the list") field(:total, :integer, description: "The total number of todo lists in the list")
end end
@ -45,6 +47,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
@desc "Get a todo list" @desc "Get a todo list"
field :todo_list, :todo_list do field :todo_list, :todo_list do
arg(:id, non_null(:id), description: "The todo-list ID") arg(:id, non_null(:id), description: "The todo-list ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Todos.get_todo_list/3) resolve(&Todos.get_todo_list/3)
end end
end end
@ -54,6 +57,7 @@ defmodule Mobilizon.GraphQL.Schema.Todos.TodoListType do
field :create_todo_list, :todo_list do field :create_todo_list, :todo_list do
arg(:title, non_null(:string), description: "The todo list title") arg(:title, non_null(:string), description: "The todo list title")
arg(:group_id, non_null(:id), description: "The group ID") arg(:group_id, non_null(:id), description: "The group ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&Todos.create_todo_list/3) resolve(&Todos.create_todo_list/3)
end end
end end

View file

@ -7,14 +7,21 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
import Absinthe.Resolution.Helpers, only: [dataloader: 2] import Absinthe.Resolution.Helpers, only: [dataloader: 2]
alias Mobilizon.Events alias Mobilizon.Events
alias Mobilizon.GraphQL.Resolvers.Application, as: ApplicationResolver
alias Mobilizon.GraphQL.Resolvers.{Media, User} alias Mobilizon.GraphQL.Resolvers.{Media, User}
alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
alias Mobilizon.GraphQL.Schema alias Mobilizon.GraphQL.Schema
import_types(Schema.SortType) import_types(Schema.SortType)
@env Application.compile_env(:mobilizon, :env)
@user_ip_limit 10
@user_email_limit 5
@desc "A local user of Mobilizon" @desc "A local user of Mobilizon"
object :user do object :user do
meta(:authorize, :all)
meta(:scope_field?, true)
interfaces([:action_log_object]) interfaces([:action_log_object])
field(:id, :id, description: "The user's ID") field(:id, :id, description: "The user's ID")
field(:email, non_null(:string), description: "The user's email") field(:email, non_null(:string), description: "The user's email")
@ -63,7 +70,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field(:disabled, :boolean, description: "Whether the user is disabled") field(:disabled, :boolean, description: "Whether the user is disabled")
field(:participations, :paginated_participant_list, field(:participations, :paginated_participant_list,
description: "The list of participations this user has" description: "The list of participations this user has",
meta: [private: true]
) do ) do
arg(:after_datetime, :datetime, description: "Filter participations by event start datetime") arg(:after_datetime, :datetime, description: "Filter participations by event start datetime")
@ -83,7 +91,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end end
field(:memberships, :paginated_member_list, field(:memberships, :paginated_member_list,
description: "The list of memberships for this user" description: "The list of memberships for this user",
meta: [private: true]
) do ) do
arg(:name, :string, description: "A name to filter members by") arg(:name, :string, description: "A name to filter members by")
@ -97,7 +106,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end end
field(:drafts, :paginated_event_list, field(:drafts, :paginated_event_list,
description: "The list of draft events this user has created" description: "The list of draft events this user has created",
meta: [private: true]
) do ) do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
@ -109,7 +119,8 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end end
field(:followed_group_events, :paginated_followed_group_events, field(:followed_group_events, :paginated_followed_group_events,
description: "The suggested events from the groups this user follows" description: "The suggested events from the groups this user follows",
meta: [private: true]
) do ) do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
@ -128,7 +139,10 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
resolve(&User.user_followed_group_events/3) resolve(&User.user_followed_group_events/3)
end end
field(:settings, :user_settings, description: "The list of settings for this user") do field(:settings, :user_settings,
description: "The list of settings for this user",
meta: [private: true]
) do
resolve(&User.user_settings/3) resolve(&User.user_settings/3)
end end
@ -142,7 +156,10 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "The IP adress the user's currently signed-in with" description: "The IP adress the user's currently signed-in with"
) )
field(:media, :paginated_media_list, description: "The user's media objects") do field(:media, :paginated_media_list,
description: "The user's media objects",
meta: [private: true]
) do
arg(:page, :integer, arg(:page, :integer,
default_value: 1, default_value: 1,
description: "The page in the paginated user media list" description: "The page in the paginated user media list"
@ -158,9 +175,18 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
) )
field(:activity_settings, list_of(:activity_setting), field(:activity_settings, list_of(:activity_setting),
resolve: &ActivitySettings.user_activity_settings/3, description: "The user's activity settings",
description: "The user's activity settings" meta: [private: true]
) ) do
resolve(&ActivitySettings.user_activity_settings/3)
end
field(:auth_authorized_applications, list_of(:auth_application_token),
description: "The user's authorized authentication apps",
meta: [private: true, rule: :forbid_app_access]
) do
resolve(&ApplicationResolver.get_user_applications/3)
end
end end
@desc "The list of roles an user can have" @desc "The list of roles an user can have"
@ -172,12 +198,14 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "Token" @desc "Token"
object :refreshed_token do object :refreshed_token do
meta(:authorize, :all)
field(:access_token, non_null(:string), description: "Generated access token") field(:access_token, non_null(:string), description: "Generated access token")
field(:refresh_token, non_null(:string), description: "Generated refreshed token") field(:refresh_token, non_null(:string), description: "Generated refreshed token")
end end
@desc "Users list" @desc "Users list"
object :users do object :users do
meta(:authorize, [:administrator, :moderator])
field(:total, non_null(:integer), description: "Total elements") field(:total, non_null(:integer), description: "Total elements")
field(:elements, non_null(list_of(:user)), description: "User elements") field(:elements, non_null(list_of(:user)), description: "User elements")
end end
@ -191,6 +219,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
A set of user settings A set of user settings
""" """
object :user_settings do object :user_settings do
meta(:authorize, :user)
field(:timezone, :string, description: "The timezone for this user") field(:timezone, :string, description: "The timezone for this user")
field(:notification_on_day, :boolean, field(:notification_on_day, :boolean,
@ -249,6 +278,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
end end
object :location do object :location do
meta(:authorize, :user)
field(:range, :integer, description: "The range in kilometers the user wants to see events") field(:range, :integer, description: "The range in kilometers the user wants to see events")
field(:geohash, :string, description: "A geohash representing the user's preferred location") field(:geohash, :string, description: "A geohash representing the user's preferred location")
@ -271,11 +301,13 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
@desc "Get an user" @desc "Get an user"
field :user, :user do field :user, :user do
arg(:id, non_null(:id)) arg(:id, non_null(:id))
middleware(Rajska.QueryAuthorization, permit: [:administrator, :moderator], scope: false)
resolve(&User.find_user/3) resolve(&User.find_user/3)
end end
@desc "Get the current user" @desc "Get the current user"
field :logged_user, :user do field :logged_user, :user do
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.get_current_user/3) resolve(&User.get_current_user/3)
end end
@ -292,7 +324,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
arg(:sort, :sortable_user_field, default_value: :id, description: "Sort column") arg(:sort, :sortable_user_field, default_value: :id, description: "Sort column")
arg(:direction, :sort_direction, default_value: :desc, description: "Sort direction") arg(:direction, :sort_direction, default_value: :desc, description: "Sort direction")
middleware(Rajska.QueryAuthorization, permit: [:administrator, :moderator], scope: false)
resolve(&User.list_users/3) resolve(&User.list_users/3)
end end
end end
@ -303,7 +335,9 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
arg(:email, non_null(:string), description: "The new user's email") arg(:email, non_null(:string), description: "The new user's email")
arg(:password, non_null(:string), description: "The new user's password") arg(:password, non_null(:string), description: "The new user's password")
arg(:locale, :string, description: "The new user's locale") arg(:locale, :string, description: "The new user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
resolve(&User.create_user/3) resolve(&User.create_user/3)
end end
@ -313,6 +347,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "The token that will be used to validate the user" description: "The token that will be used to validate the user"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.validate_user/3) resolve(&User.validate_user/3)
end end
@ -320,6 +355,9 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :resend_confirmation_email, type: :string do field :resend_confirmation_email, type: :string do
arg(:email, non_null(:string), description: "The email used to register") arg(:email, non_null(:string), description: "The email used to register")
arg(:locale, :string, description: "The user's locale") arg(:locale, :string, description: "The user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
resolve(&User.resend_confirmation_email/3) resolve(&User.resend_confirmation_email/3)
end end
@ -327,6 +365,9 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :send_reset_password, type: :string do field :send_reset_password, type: :string do
arg(:email, non_null(:string), description: "The user's email") arg(:email, non_null(:string), description: "The user's email")
arg(:locale, :string, description: "The user's locale") arg(:locale, :string, description: "The user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
resolve(&User.send_reset_password/3) resolve(&User.send_reset_password/3)
end end
@ -338,6 +379,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
arg(:password, non_null(:string), description: "The new password") arg(:password, non_null(:string), description: "The new password")
arg(:locale, :string, default_value: "en", description: "The user's locale") arg(:locale, :string, default_value: "en", description: "The user's locale")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.reset_password/3) resolve(&User.reset_password/3)
end end
@ -345,24 +387,30 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :login, type: :login do field :login, type: :login do
arg(:email, non_null(:string), description: "The user's email") arg(:email, non_null(:string), description: "The user's email")
arg(:password, non_null(:string), description: "The user's password") arg(:password, non_null(:string), description: "The user's password")
middleware(Rajska.QueryAuthorization, permit: :all)
middleware(Rajska.RateLimiter, limit: user_ip_limiter(@env))
middleware(Rajska.RateLimiter, keys: :email, limit: user_email_limiter(@env))
resolve(&User.login_user/3) resolve(&User.login_user/3)
end end
@desc "Refresh a token" @desc "Refresh a token"
field :refresh_token, type: :refreshed_token do field :refresh_token, type: :refreshed_token do
arg(:refresh_token, non_null(:string), description: "A refresh token") arg(:refresh_token, non_null(:string), description: "A refresh token")
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.refresh_token/3) resolve(&User.refresh_token/3)
end end
@desc "Logout an user, deleting a refresh token" @desc "Logout an user, deleting a refresh token"
field :logout, :string do field :logout, :string do
arg(:refresh_token, non_null(:string)) arg(:refresh_token, non_null(:string))
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.logout/3) resolve(&User.logout/3)
end end
@desc "Change default actor for user" @desc "Change default actor for user"
field :change_default_actor, :user do field :change_default_actor, :user do
arg(:preferred_username, non_null(:string), description: "The actor preferred_username") arg(:preferred_username, non_null(:string), description: "The actor preferred_username")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.change_default_actor/3) resolve(&User.change_default_actor/3)
end end
@ -370,6 +418,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :change_password, :user do field :change_password, :user do
arg(:old_password, non_null(:string), description: "The user's current password") arg(:old_password, non_null(:string), description: "The user's current password")
arg(:new_password, non_null(:string), description: "The user's new password") arg(:new_password, non_null(:string), description: "The user's new password")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.change_password/3) resolve(&User.change_password/3)
end end
@ -377,6 +426,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :change_email, :user do field :change_email, :user do
arg(:email, non_null(:string), description: "The user's new email") arg(:email, non_null(:string), description: "The user's new email")
arg(:password, non_null(:string), description: "The user's current password") arg(:password, non_null(:string), description: "The user's current password")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.change_email/3) resolve(&User.change_email/3)
end end
@ -386,6 +436,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "The token that will be used to validate the email change" description: "The token that will be used to validate the email change"
) )
middleware(Rajska.QueryAuthorization, permit: :all)
resolve(&User.validate_email/3) resolve(&User.validate_email/3)
end end
@ -393,6 +444,7 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
field :delete_account, :deleted_object do field :delete_account, :deleted_object do
arg(:password, :string, description: "The user's password") arg(:password, :string, description: "The user's password")
arg(:user_id, :id, description: "The user's ID") arg(:user_id, :id, description: "The user's ID")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.delete_account/3) resolve(&User.delete_account/3)
end end
@ -430,13 +482,21 @@ defmodule Mobilizon.GraphQL.Schema.UserType do
description: "A geohash of the user's preferred location, where they want to see events" description: "A geohash of the user's preferred location, where they want to see events"
) )
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.set_user_setting/3) resolve(&User.set_user_setting/3)
end end
@desc "Update the user's locale" @desc "Update the user's locale"
field :update_locale, :user do field :update_locale, :user do
arg(:locale, :string, description: "The user's new locale") arg(:locale, :string, description: "The user's new locale")
middleware(Rajska.QueryAuthorization, permit: :user, scope: false)
resolve(&User.update_locale/3) resolve(&User.update_locale/3)
end end
end end
defp user_ip_limiter(:test), do: @user_ip_limit * 1000
defp user_ip_limiter(_), do: @user_ip_limit
defp user_email_limiter(:test), do: @user_email_limit * 1000
defp user_email_limiter(_), do: @user_email_limit
end end

View file

@ -6,6 +6,7 @@ defmodule Mobilizon.GraphQL.Schema.Users.ActivitySetting do
alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings alias Mobilizon.GraphQL.Resolvers.Users.ActivitySettings
object :activity_setting do object :activity_setting do
meta(:authorize, :user)
field(:key, :string) field(:key, :string)
field(:method, :string) field(:method, :string)
field(:enabled, :boolean) field(:enabled, :boolean)
@ -17,6 +18,13 @@ defmodule Mobilizon.GraphQL.Schema.Users.ActivitySetting do
arg(:key, non_null(:string)) arg(:key, non_null(:string))
arg(:method, non_null(:string)) arg(:method, non_null(:string))
arg(:enabled, non_null(:boolean)) arg(:enabled, non_null(:boolean))
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: false,
rule: :"write:user:setting:activity"
)
resolve(&ActivitySettings.upsert_user_activity_setting/3) resolve(&ActivitySettings.upsert_user_activity_setting/3)
end end
end end

View file

@ -26,11 +26,25 @@ defmodule Mobilizon.GraphQL.Schema.Users.PushSubscription do
arg(:endpoint, non_null(:string)) arg(:endpoint, non_null(:string))
arg(:auth, non_null(:string)) arg(:auth, non_null(:string))
arg(:p256dh, non_null(:string)) arg(:p256dh, non_null(:string))
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: false,
rule: :"write:user:setting:push"
)
resolve(&PushSubscription.register_push_subscription/3) resolve(&PushSubscription.register_push_subscription/3)
end end
field :unregister_push, :string do field :unregister_push, :string do
arg(:endpoint, non_null(:string)) arg(:endpoint, non_null(:string))
middleware(Rajska.QueryAuthorization,
permit: :user,
scope: false,
rule: :"write:user:setting:push"
)
resolve(&PushSubscription.unregister_push_subscription/3) resolve(&PushSubscription.unregister_push_subscription/3)
end end
end end

View file

@ -0,0 +1,435 @@
defmodule Mobilizon.Applications do
@moduledoc """
The Applications context.
"""
import Ecto.Query, warn: false
import EctoEnum
alias Ecto.Multi
alias Mobilizon.Applications.Application
alias Mobilizon.Storage.Repo
defenum(ApplicationDeviceActivationStatus, [
"success",
"pending",
"confirmed",
"incorrect_device_code",
"access_denied"
])
defenum(ApplicationTokenStatus, [
"success",
"pending",
"access_denied"
])
@doc """
Returns the list of applications.
## Examples
iex> list_applications()
[%Application{}, ...]
"""
def list_applications do
Repo.all(Application)
end
@doc """
Gets a single application.
Raises `Ecto.NoResultsError` if the Application does not exist.
## Examples
iex> get_application!(123)
%Application{}
iex> get_application!(456)
** (Ecto.NoResultsError)
"""
def get_application!(id), do: Repo.get!(Application, id)
@doc """
Gets a single application.
Returns nil if the Application does not exist.
## Examples
iex> get_application_by_client_id(123)
%Application{}
iex> get_application_by_client_id(456)
nil
"""
def get_application_by_client_id(client_id), do: Repo.get_by(Application, client_id: client_id)
@doc """
Creates a application.
## Examples
iex> create_application(%{field: value})
{:ok, %Application{}}
iex> create_application(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_application(attrs \\ %{}) do
%Application{}
|> Application.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a application.
## Examples
iex> update_application(application, %{field: new_value})
{:ok, %Application{}}
iex> update_application(application, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_application(%Application{} = application, attrs) do
application
|> Application.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a application.
## Examples
iex> delete_application(application)
{:ok, %Application{}}
iex> delete_application(application)
{:error, %Ecto.Changeset{}}
"""
def delete_application(%Application{} = application) do
Repo.delete(application)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking application changes.
## Examples
iex> change_application(application)
%Ecto.Changeset{data: %Application{}}
"""
def change_application(%Application{} = application, attrs \\ %{}) do
Application.changeset(application, attrs)
end
alias Mobilizon.Applications.ApplicationToken
@doc """
Returns the list of application_tokens.
## Examples
iex> list_application_tokens()
[%ApplicationToken{}, ...]
"""
def list_application_tokens do
ApplicationToken
|> Repo.all()
|> Repo.preload(:application)
end
@doc """
Returns the list of application tokens for a given user_id
"""
@spec list_application_tokens_for_user_id(number() | String.t()) :: list(ApplicationToken.t())
def list_application_tokens_for_user_id(user_id) do
ApplicationToken
|> where(user_id: ^user_id)
|> where([at], is_nil(at.authorization_code))
|> preload(:application)
|> Repo.all()
end
@doc """
Gets a single application_token.
Raises `Ecto.NoResultsError` if the Application token does not exist.
## Examples
iex> get_application_token!(123)
%ApplicationToken{}
iex> get_application_token!(456)
** (Ecto.NoResultsError)
"""
def get_application_token!(id) do
ApplicationToken
|> Repo.get!(id)
|> Repo.preload([:application])
end
@doc """
Gets a single application_token.
## Examples
iex> get_application_token(123)
%ApplicationToken{}
iex> get_application_token(456)
nil
"""
def get_application_token(application_token_id),
do: Repo.get(ApplicationToken, application_token_id)
def get_application_token(app_id, user_id),
do: Repo.get_by(ApplicationToken, application_id: app_id, user_id: user_id)
def get_application_token_by_authorization_code(code),
do: Repo.get_by(ApplicationToken, authorization_code: code)
@doc """
Creates a application_token.
## Examples
iex> create_application_token(%{field: value})
{:ok, %ApplicationToken{}}
iex> create_application_token(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_application_token(attrs \\ %{}) do
%ApplicationToken{}
|> ApplicationToken.changeset(attrs)
|> Repo.insert(on_conflict: :replace_all, conflict_target: [:user_id, :application_id])
|> case do
{:ok, application_token} ->
{:ok, Repo.preload(application_token, :application)}
error ->
error
end
end
@doc """
Updates a application_token.
## Examples
iex> update_application_token(application_token, %{field: new_value})
{:ok, %ApplicationToken{}}
iex> update_application_token(application_token, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_application_token(%ApplicationToken{} = application_token, attrs) do
application_token
|> ApplicationToken.changeset(attrs)
|> Repo.update()
end
@doc """
Deletes a application_token.
## Examples
iex> delete_application_token(application_token)
{:ok, %ApplicationToken{}}
iex> delete_application_token(application_token)
{:error, %Ecto.Changeset{}}
"""
def delete_application_token(%ApplicationToken{} = application_token) do
Repo.delete(application_token)
end
def revoke_application_token(%ApplicationToken{id: app_token_id} = application_token) do
Multi.new()
|> Multi.delete_all(
:delete_guardian_tokens,
from(gt in "guardian_tokens", where: gt.sub == ^"AppToken:#{app_token_id}")
)
|> Multi.delete(:delete_app_token, application_token)
|> Repo.transaction()
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking application_token changes.
## Examples
iex> change_application_token(application_token)
%Ecto.Changeset{data: %ApplicationToken{}}
"""
def change_application_token(%ApplicationToken{} = application_token, attrs \\ %{}) do
ApplicationToken.changeset(application_token, attrs)
end
@spec prune_old_application_tokens(pos_integer()) :: {non_neg_integer(), nil}
def prune_old_application_tokens(lifetime) do
exp = DateTime.add(NaiveDateTime.utc_now(), -lifetime)
ApplicationToken
|> where([at], at.status != :success)
|> where([at], at.inserted_at < ^exp)
|> Repo.delete_all()
end
alias Mobilizon.Applications.ApplicationDeviceActivation
@doc """
Returns the list of application_device_activation.
## Examples
iex> list_application_device_activation()
[%ApplicationDeviceActivation{}, ...]
"""
def list_application_device_activation do
Repo.all(ApplicationDeviceActivation)
end
@doc """
Gets a single application_device_activation.
Raises `Ecto.NoResultsError` if the Application device activation does not exist.
## Examples
iex> get_application_device_activation!(123)
%ApplicationDeviceActivation{}
iex> get_application_device_activation!(456)
** (Ecto.NoResultsError)
"""
def get_application_device_activation!(id), do: Repo.get!(ApplicationDeviceActivation, id)
def get_application_device_activation(id), do: Repo.get(ApplicationDeviceActivation, id)
def get_application_device_activation_by_user_code(user_code) do
ApplicationDeviceActivation
|> where([ada], ada.user_code == ^user_code)
|> preload(:application)
|> Repo.one()
end
def get_application_device_activation_by_device_code(client_id, device_code) do
ApplicationDeviceActivation
|> join(:left, [ada], a in assoc(ada, :application))
|> where([_, a], a.client_id == ^client_id)
|> where([ada], ada.device_code == ^device_code)
|> select([ada], ada)
|> Repo.one()
end
@doc """
Creates a application_device_activation.
## Examples
iex> create_application_device_activation(%{field: value})
{:ok, %ApplicationDeviceActivation{}}
iex> create_application_device_activation(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_application_device_activation(attrs \\ %{}) do
%ApplicationDeviceActivation{}
|> ApplicationDeviceActivation.changeset(attrs)
|> Repo.insert()
end
@doc """
Updates a application_device_activation.
## Examples
iex> update_application_device_activation(application_device_activation, %{field: new_value})
{:ok, %ApplicationDeviceActivation{}}
iex> update_application_device_activation(application_device_activation, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_application_device_activation(
%ApplicationDeviceActivation{} = application_device_activation,
attrs
) do
application_device_activation
|> ApplicationDeviceActivation.changeset(attrs)
|> Repo.update()
|> case do
{:ok, application_device_activation} ->
{:ok, Repo.preload(application_device_activation, :application)}
error ->
error
end
end
@doc """
Deletes a application_device_activation.
## Examples
iex> delete_application_device_activation(application_device_activation)
{:ok, %ApplicationDeviceActivation{}}
iex> delete_application_device_activation(application_device_activation)
{:error, %Ecto.Changeset{}}
"""
def delete_application_device_activation(
%ApplicationDeviceActivation{} = application_device_activation
) do
Repo.delete(application_device_activation)
end
@doc """
Returns an `%Ecto.Changeset{}` for tracking application_device_activation changes.
## Examples
iex> change_application_device_activation(application_device_activation)
%Ecto.Changeset{data: %ApplicationDeviceActivation{}}
"""
def change_application_device_activation(
%ApplicationDeviceActivation{} = application_device_activation,
attrs \\ %{}
) do
ApplicationDeviceActivation.changeset(application_device_activation, attrs)
end
@spec prune_old_application_device_activations(pos_integer()) :: {non_neg_integer(), nil}
def prune_old_application_device_activations(lifetime) do
exp = DateTime.add(NaiveDateTime.utc_now(), -lifetime)
ApplicationDeviceActivation
|> where([at], at.expires_in < ^exp)
|> Repo.delete_all()
end
end

View file

@ -0,0 +1,32 @@
defmodule Mobilizon.Applications.Application do
@moduledoc """
Module representing an application
"""
use Ecto.Schema
import Ecto.Changeset
@required_attrs [:name, :client_id, :client_secret, :redirect_uris, :scope]
@optional_attrs [:website, :owner_type, :owner_id]
@attrs @required_attrs ++ @optional_attrs
schema "applications" do
field(:name, :string)
field(:client_id, :string)
field(:client_secret, :string)
field(:redirect_uris, {:array, :string})
field(:scope, :string)
field(:website, :string)
field(:owner_type, :string)
field(:owner_id, :integer)
timestamps()
end
@doc false
def changeset(application, attrs) do
application
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View file

@ -0,0 +1,32 @@
defmodule Mobilizon.Applications.ApplicationDeviceActivation do
@moduledoc """
Module representing a application device activation
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Applications.{Application, ApplicationDeviceActivationStatus}
alias Mobilizon.Users.User
schema "application_device_activation" do
field(:user_code, :string)
field(:device_code, :string)
field(:scope, :string)
field(:expires_in, :integer)
field(:status, ApplicationDeviceActivationStatus, default: :pending)
belongs_to(:user, User)
belongs_to(:application, Application)
timestamps()
end
@required_attrs [:user_code, :device_code, :expires_in, :application_id, :scope]
@optional_attrs [:status, :user_id]
@attrs @required_attrs ++ @optional_attrs
@doc false
def changeset(application_device_activation, attrs) do
application_device_activation
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View file

@ -0,0 +1,30 @@
defmodule Mobilizon.Applications.ApplicationToken do
@moduledoc """
Module representing an application token
"""
use Ecto.Schema
import Ecto.Changeset
alias Mobilizon.Applications.{Application, ApplicationTokenStatus}
alias Mobilizon.Users.User
schema "application_tokens" do
belongs_to(:user, User)
belongs_to(:application, Application)
field(:authorization_code, :string)
field(:status, ApplicationTokenStatus, default: :pending)
field(:scope, :string)
timestamps()
end
@required_attrs [:user_id, :application_id, :scope]
@optional_attrs [:authorization_code]
@attrs @required_attrs ++ @optional_attrs
@doc false
def changeset(application_token, attrs) do
application_token
|> cast(attrs, @attrs)
|> validate_required(@required_attrs)
end
end

View file

@ -192,7 +192,7 @@ defmodule Mobilizon.Service.Akismet do
{email, ip} -> {email, ip} ->
{preferred_username, email, ip} {preferred_username, email, ip}
err -> _ ->
{:error, :invalid_actor} {:error, :invalid_actor}
end end
end end
@ -205,7 +205,7 @@ defmodule Mobilizon.Service.Akismet do
{nil, preferred_username, "127.0.0.1"} {nil, preferred_username, "127.0.0.1"}
end end
defp actor_details(err) do defp actor_details(_) do
{:error, :invalid_actor} {:error, :invalid_actor}
end end

View file

@ -0,0 +1,413 @@
defmodule Mobilizon.Service.Auth.Applications do
@moduledoc """
Module to handle applications management
"""
alias Mobilizon.Applications
alias Mobilizon.Applications.{Application, ApplicationDeviceActivation, ApplicationToken}
alias Mobilizon.GraphQL.Authorization.AppScope
alias Mobilizon.Service.Auth.Authenticator
alias Mobilizon.Users.User
alias Mobilizon.Web.Auth.Guardian
alias Mobilizon.Web.Router.Helpers, as: Routes
require Logger
@app_access_tokens_ttl {8, :hour}
@app_refresh_tokens_ttl {26, :week}
@device_code_expires_in 900
@device_code_interval 5
@authorization_code_lifetime 60
@application_device_activation_lifetime @device_code_expires_in * 2
@type access_token_details :: %{
required(:access_token) => String.t(),
required(:expires_in) => pos_integer(),
required(:refresh_token) => String.t(),
required(:refresh_token_expires_in) => pos_integer(),
required(:scope) => nil,
required(:token_type) => String.t()
}
@spec create(String.t(), list(String.t()), String.t(), String.t() | nil) ::
{:ok, Application.t()} | {:error, Ecto.Changeset.t()} | {:error, :invalid_scope}
def create(name, redirect_uris, scope, website \\ nil) do
if AppScope.scopes_valid?(scope) do
client_id = :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42)
client_secret = :crypto.strong_rand_bytes(42) |> Base.encode64() |> binary_part(0, 42)
Applications.create_application(%{
name: name,
redirect_uris: redirect_uris,
scope: scope,
website: website,
client_id: client_id,
client_secret: client_secret
})
else
{:error, :invalid_scope}
end
end
@spec autorize(String.t(), String.t(), String.t(), integer()) ::
{:ok, ApplicationToken.t()}
| {:error, :application_not_found}
| {:error, :redirect_uri_not_in_allowed}
| {:error, Ecto.Changeset.t()}
def autorize(client_id, redirect_uri, scope, user_id) do
with %Application{redirect_uris: redirect_uris, id: app_id} <-
Applications.get_application_by_client_id(client_id),
{:redirect_uri, true} <-
{:redirect_uri, redirect_uri in redirect_uris},
code <- :crypto.strong_rand_bytes(16) |> Base.encode64() |> binary_part(0, 16) do
Applications.create_application_token(%{
user_id: user_id,
application_id: app_id,
authorization_code: code,
scope: scope,
status: :pending
})
else
nil ->
{:error, :application_not_found}
{:redirect_uri, _} ->
{:error, :redirect_uri_not_in_allowed}
end
end
@spec autorize_device_application(String.t(), String.t()) ::
{:ok, ApplicationDeviceActivation.t()}
| {:error, Ecto.Changeset.t()}
| {:error, :expired}
| {:error, :access_denied}
| {:error, :not_found}
def autorize_device_application(client_id, user_code) do
Logger.debug(
"Authorizing device application client_id: #{client_id}, user_code: #{user_code}"
)
case Applications.get_application_device_activation_by_user_code(user_code) do
%ApplicationDeviceActivation{
status: :confirmed,
application: %Application{client_id: ^client_id}
} = app_device_activation ->
if device_activation_expired?(app_device_activation) do
{:error, :expired}
else
Applications.update_application_device_activation(app_device_activation, %{
status: :success
})
end
# The device activation is confirmed, but does not match the given app client_id, so we say it's not found
%ApplicationDeviceActivation{status: :confirmed} ->
{:error, :not_found}
%ApplicationDeviceActivation{} ->
{:error, :not_confirmed}
nil ->
{:error, :not_found}
end
end
@spec generate_access_token(String.t(), String.t(), String.t(), String.t(), String.t()) ::
{:ok, access_token_details()}
| {:error,
:application_not_found
| :redirect_uri_not_in_allowed
| :provided_code_does_not_match
| :invalid_client_secret
| :invalid_or_expired
| :scope_not_included
| any()}
def generate_access_token(client_id, client_secret, code, redirect_uri, scope) do
with {:application,
%Application{
id: application_id,
client_secret: app_client_secret,
redirect_uris: redirect_uris,
scope: app_scope
}} <-
{:application, Applications.get_application_by_client_id(client_id)},
{:scope_included, true} <- {:scope_included, request_scope_valid?(app_scope, scope)},
{:redirect_uri, true} <-
{:redirect_uri, redirect_uri in redirect_uris},
{:app_token, %ApplicationToken{} = app_token} <-
{:app_token, Applications.get_application_token_by_authorization_code(code)},
{:expired, false} <- {:expired, authorization_code_expired?(app_token)},
{:ok, %ApplicationToken{application_id: application_id_from_token} = app_token} <-
Applications.update_application_token(app_token, %{
authorization_code: nil,
status: :success
}),
{:same_app, true} <- {:same_app, application_id === application_id_from_token},
{:same_client_secret, true} <- {:same_client_secret, app_client_secret == client_secret},
{:ok, access_token} <-
Authenticator.generate_access_token(app_token, @app_access_tokens_ttl),
{:ok, refresh_token} <-
Authenticator.generate_refresh_token(app_token, @app_refresh_tokens_ttl) do
{:ok,
%{
access_token: access_token,
expires_in: ttl_to_seconds(@app_access_tokens_ttl),
refresh_token: refresh_token,
refresh_token_expires_in: ttl_to_seconds(@app_refresh_tokens_ttl),
scope: scope,
token_type: "bearer"
}}
else
{:application, nil} ->
{:error, :application_not_found}
{:same_app, false} ->
{:error, :provided_code_does_not_match}
{:same_client_secret, _} ->
{:error, :invalid_client_secret}
{:redirect_uri, _} ->
{:error, :redirect_uri_not_in_allowed}
{:app_token, _} ->
{:error, :invalid_or_expired}
{:expired, true} ->
{:error, :invalid_or_expired}
{:scope_included, false} ->
{:error, :scope_not_included}
{:error, err} ->
{:error, err}
end
end
def generate_access_token_for_device_flow(client_id, device_code) do
Logger.debug("Generating access token for application device with",
client_id: client_id,
device_code: device_code
)
case Applications.get_application_device_activation_by_device_code(client_id, device_code) do
%ApplicationDeviceActivation{status: :success, scope: scope, user_id: user_id} =
app_device_activation ->
if device_activation_expired?(app_device_activation) do
{:error, :expired}
else
%Application{id: app_id} = Applications.get_application_by_client_id(client_id)
{:ok, %ApplicationToken{} = app_token} =
Applications.create_application_token(%{
user_id: user_id,
application_id: app_id,
authorization_code: nil,
scope: scope,
status: :success
})
{:ok, access_token} =
Authenticator.generate_access_token(app_token, @app_access_tokens_ttl)
{:ok, refresh_token} =
Authenticator.generate_refresh_token(app_token, @app_refresh_tokens_ttl)
{:ok,
%{
access_token: access_token,
expires_in: ttl_to_seconds(@app_access_tokens_ttl),
refresh_token: refresh_token,
refresh_token_expires_in: ttl_to_seconds(@app_refresh_tokens_ttl),
scope: scope,
token_type: "bearer"
}}
end
%ApplicationDeviceActivation{status: :incorrect_device_code} ->
{:error, :incorrect_device_code}
%ApplicationDeviceActivation{status: :access_denied} ->
{:error, :access_denied}
%ApplicationDeviceActivation{status: :pending} ->
{:error, :pending, @device_code_interval}
nil ->
{:error, :incorrect_device_code}
err ->
Logger.error(inspect(err))
{:error, :incorrect_device_code}
end
end
@chars "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |> String.split("", trim: true)
defp string_of_length(length) do
1..length
|> Enum.reduce([], fn _i, acc ->
[Enum.random(@chars) | acc]
end)
|> Enum.join("")
end
@spec register_device_code(String.t(), String.t() | nil) ::
{:ok, ApplicationDeviceActivation.t()}
| {:error, :application_not_found}
| {:error, :scope_not_included}
| {:error, Ecto.Changeset.t()}
def register_device_code(client_id, scope) do
with {:app, %Application{scope: app_scope} = application} <-
{:app, Applications.get_application_by_client_id(client_id)},
{device_code, user_code, verification_uri} <-
{string_of_length(40), string_of_length(8),
Routes.page_url(Mobilizon.Web.Endpoint, :auth_device)},
{:scope_included, true} <- {:scope_included, request_scope_valid?(app_scope, scope)},
{:ok, %ApplicationDeviceActivation{} = application_device_activation} <-
Applications.create_application_device_activation(%{
device_code: device_code,
user_code: user_code,
expires_in: @device_code_expires_in,
application_id: application.id,
scope: scope
}) do
{:ok,
application_device_activation
|> Map.from_struct()
|> Map.take([:device_code, :user_code, :expires_in])
|> Map.update!(:user_code, &user_code_displayed/1)
|> Map.merge(%{
interval: @device_code_interval,
verification_uri: verification_uri
})}
else
{:error, %Ecto.Changeset{} = err} ->
{:error, err}
{:app, nil} ->
{:error, :application_not_found}
{:scope_included, false} ->
{:error, :scope_not_included}
end
end
@spec activate_device(String.t(), User.t()) ::
{:ok, ApplicationDeviceActivation.t()}
| {:error, Ecto.Changeset.t()}
| {:error, :not_found}
| {:error, :expired}
def activate_device(user_code, user) do
case Applications.get_application_device_activation_by_user_code(user_code) do
%ApplicationDeviceActivation{} = app_device_activation ->
if device_activation_expired?(app_device_activation) do
{:error, :expired}
else
Applications.update_application_device_activation(app_device_activation, %{
status: :confirmed,
user_id: user.id
})
end
_ ->
{:error, :not_found}
end
end
@spec refresh_tokens(String.t(), String.t(), String.t()) ::
{:ok, access_token_details()}
| {:error, :invalid_client_credentials}
| {:error, :invalid_refresh_token}
| {:error, any()}
def refresh_tokens(refresh_token, user_client_id, user_client_secret) do
with {:resource_from_token,
{:ok,
%ApplicationToken{
application: %Application{client_id: app_client_id, client_secret: app_client_secret},
scope: scope
} = app_token,
_claims}} <- {:resource_from_token, Guardian.resource_from_token(refresh_token)},
{:valid_client_credentials, true} <-
{:valid_client_credentials,
app_client_id == user_client_id and app_client_secret == user_client_secret},
{:ok, _old, {exchanged_token, _claims}} <-
Guardian.exchange(refresh_token, "refresh", "access", ttl: @app_access_tokens_ttl),
{:ok, new_refresh_token} <-
Authenticator.generate_refresh_token(app_token, @app_refresh_tokens_ttl),
{:ok, _claims} <- Guardian.revoke(refresh_token) do
{:ok,
%{
access_token: exchanged_token,
expires_in: ttl_to_seconds(@app_access_tokens_ttl),
refresh_token: new_refresh_token,
refresh_token_expires_in: ttl_to_seconds(@app_refresh_tokens_ttl),
scope: scope,
token_type: "bearer"
}}
else
{:valid_client_credentials, false} ->
{:error, :invalid_client_credentials}
{:resource_from_token, _} ->
{:error, :invalid_refresh_token}
end
end
defp user_code_displayed(user_code) do
String.slice(user_code, 0..3) <> "-" <> String.slice(user_code, 4..7)
end
def revoke_application_token(%ApplicationToken{} = app_token) do
Applications.revoke_application_token(app_token)
end
@spec ttl_to_seconds({pos_integer(), :second | :minute | :hour | :week}) :: pos_integer()
defp ttl_to_seconds({value, :second}), do: value
defp ttl_to_seconds({value, :minute}), do: value * 60
defp ttl_to_seconds({value, :hour}), do: value * 3600
defp ttl_to_seconds({value, :week}), do: value * 604_800
@spec device_activation_expired?(ApplicationDeviceActivation.t()) :: boolean()
defp device_activation_expired?(%ApplicationDeviceActivation{
inserted_at: inserted_at,
expires_in: expires_in
}) do
NaiveDateTime.compare(NaiveDateTime.add(inserted_at, expires_in), NaiveDateTime.utc_now()) ==
:lt
end
def prune_old_tokens do
Applications.prune_old_application_tokens(@authorization_code_lifetime)
end
def prune_old_application_device_activations do
Applications.prune_old_application_device_activations(@application_device_activation_lifetime)
end
@spec revoke_token(String.t()) ::
{:ok, map()}
| {:error, any(), any(), any()}
| {:error, :token_not_found}
def revoke_token(token) do
case Guardian.resource_from_token(token) do
{:ok, %ApplicationToken{} = app_token, _claims} ->
Guardian.revoke(token)
revoke_application_token(app_token)
{:error, _err} ->
{:error, :token_not_found}
end
end
defp authorization_code_expired?(%ApplicationToken{inserted_at: inserted_at}) do
NaiveDateTime.compare(NaiveDateTime.add(inserted_at, 60), NaiveDateTime.utc_now()) ==
:lt
end
defp request_scope_valid?(app_scope, request_scope) do
app_scopes = app_scope |> String.split(" ") |> MapSet.new()
request_scopes = request_scope |> String.split(" ") |> MapSet.new()
MapSet.subset?(request_scopes, app_scopes) and AppScope.scopes_valid?(request_scope)
end
end

View file

@ -17,6 +17,11 @@ defmodule Mobilizon.Service.Auth.Authenticator do
required(:user) => User.t() required(:user) => User.t()
} }
@type ttl :: {
pos_integer(),
:second | :minute | :hour | :week
}
def implementation do def implementation do
Mobilizon.Config.get( Mobilizon.Config.get(
Mobilizon.Service.Auth.Authenticator, Mobilizon.Service.Auth.Authenticator,
@ -55,7 +60,7 @@ defmodule Mobilizon.Service.Auth.Authenticator do
@doc """ @doc """
Generates access token and refresh token for an user. Generates access token and refresh token for an user.
""" """
@spec generate_tokens(User.t()) :: {:ok, tokens} @spec generate_tokens(User.t() | ApplicationToken.t()) :: {:ok, tokens} | {:error, any()}
def generate_tokens(user) do def generate_tokens(user) do
with {:ok, access_token} <- generate_access_token(user), with {:ok, access_token} <- generate_access_token(user),
{:ok, refresh_token} <- generate_refresh_token(user) do {:ok, refresh_token} <- generate_refresh_token(user) do
@ -66,10 +71,11 @@ defmodule Mobilizon.Service.Auth.Authenticator do
@doc """ @doc """
Generates access token for an user. Generates access token for an user.
""" """
@spec generate_access_token(User.t()) :: {:ok, String.t()} @spec generate_access_token(User.t() | ApplicationToken.t(), ttl() | nil) ::
def generate_access_token(user) do {:ok, String.t()} | {:error, any()}
def generate_access_token(user, ttl \\ nil) do
with {:ok, access_token, _claims} <- with {:ok, access_token, _claims} <-
Guardian.encode_and_sign(user, %{}, token_type: "access") do Guardian.encode_and_sign(user, %{}, token_type: "access", ttl: ttl) do
{:ok, access_token} {:ok, access_token}
end end
end end
@ -77,10 +83,11 @@ defmodule Mobilizon.Service.Auth.Authenticator do
@doc """ @doc """
Generates refresh token for an user. Generates refresh token for an user.
""" """
@spec generate_refresh_token(User.t()) :: {:ok, String.t()} @spec generate_refresh_token(User.t() | ApplicationToken.t(), ttl() | nil) ::
def generate_refresh_token(user) do {:ok, String.t()} | {:error, any()}
def generate_refresh_token(user, ttl \\ nil) do
with {:ok, refresh_token, _claims} <- with {:ok, refresh_token, _claims} <-
Guardian.encode_and_sign(user, %{}, token_type: "refresh") do Guardian.encode_and_sign(user, %{}, token_type: "refresh", ttl: ttl) do
{:ok, refresh_token} {:ok, refresh_token}
end end
end end

View file

@ -0,0 +1,32 @@
defmodule Mobilizon.Service.Workers.CleanApplicationData do
@moduledoc """
Worker to send activity recaps
"""
use Oban.Worker, queue: "background"
alias Mobilizon.Service.Auth.Applications
require Logger
@impl Oban.Worker
def perform(%Job{args: %{type: :application}}) do
Logger.info("Cleaning expired applications data")
# TODO: Clear unused applications after a while
end
@impl Oban.Worker
def perform(%Job{args: %{type: :application_token}}) do
Logger.info("Cleaning expired application tokens data")
Applications.prune_old_tokens()
:ok
end
@impl Oban.Worker
def perform(%Job{args: %{type: :application_device_activation}}) do
Logger.info("Cleaning expired application device activation data")
Applications.prune_old_application_device_activations()
:ok
end
end

View file

@ -6,6 +6,8 @@ defmodule Mobilizon.Web.Auth.Context do
import Plug.Conn import Plug.Conn
alias Mobilizon.Applications.Application, as: AuthApplication
alias Mobilizon.Applications.ApplicationToken
alias Mobilizon.Users.User alias Mobilizon.Users.User
@spec init(Plug.opts()) :: Plug.opts() @spec init(Plug.opts()) :: Plug.opts()
@ -28,20 +30,15 @@ defmodule Mobilizon.Web.Auth.Context do
{conn, context} = {conn, context} =
case Guardian.Plug.current_resource(conn) do case Guardian.Plug.current_resource(conn) do
%User{id: user_id, email: user_email} = user -> %User{} = user ->
if Application.get_env(:sentry, :dsn) != nil do set_user_context({conn, context}, user)
Sentry.Context.set_user_context(%{
id: user_id,
email: user_email,
ip_address: context.ip
})
end
context = Map.put(context, :current_user, user) %ApplicationToken{user: %User{} = user} = app_token ->
conn = assign(conn, :user_locale, user.locale) conn
{conn, context} |> set_app_token_context(context, app_token)
|> set_user_context(user)
nil -> _ ->
{conn, context} {conn, context}
end end
@ -49,4 +46,35 @@ defmodule Mobilizon.Web.Auth.Context do
put_private(conn, :absinthe, %{context: context}) put_private(conn, :absinthe, %{context: context})
end end
defp set_user_context({conn, context}, %User{id: user_id, email: user_email} = user) do
if Application.get_env(:sentry, :dsn) != nil do
Sentry.Context.set_user_context(%{
id: user_id,
email: user_email,
ip_address: context.ip
})
end
context = Map.put(context, :current_user, user)
conn = assign(conn, :user_locale, user.locale)
{conn, context}
end
defp set_app_token_context(
conn,
context,
%ApplicationToken{application: %AuthApplication{client_id: client_id} = app} = app_token
) do
if Application.get_env(:sentry, :dsn) != nil do
Sentry.Context.set_user_context(%{
app_token_client_id: client_id
})
end
context =
context |> Map.put(:current_auth_app_token, app_token) |> Map.put(:current_auth_app, app)
{conn, context}
end
end end

View file

@ -3,11 +3,12 @@ defmodule Mobilizon.Web.Auth.ErrorHandler do
In case we have an auth error In case we have an auth error
""" """
import Plug.Conn import Plug.Conn
require Logger
# sobelow_skip ["XSS.SendResp"] # sobelow_skip ["XSS.SendResp"]
@spec auth_error(Plug.Conn.t(), any(), any()) :: Plug.Conn.t() @spec auth_error(Plug.Conn.t(), any(), any()) :: Plug.Conn.t()
def auth_error(conn, {type, _reason}, _opts) do def auth_error(conn, {type, reason}, _opts) do
body = Jason.encode!(%{message: to_string(type)}) body = Jason.encode!(%{message: to_string(type), details: inspect(reason)})
send_resp(conn, 401, body) send_resp(conn, 401, body)
end end
end end

View file

@ -10,14 +10,19 @@ defmodule Mobilizon.Web.Auth.Guardian do
user: [:base] user: [:base]
} }
alias Mobilizon.Users alias Mobilizon.{Applications, Users}
alias Mobilizon.Applications.ApplicationToken
alias Mobilizon.Users.User alias Mobilizon.Users.User
require Logger require Logger
@spec subject_for_token(any(), any()) :: {:ok, String.t()} | {:error, :unknown_resource} @spec subject_for_token(any(), any()) :: {:ok, String.t()} | {:error, :unknown_resource}
def subject_for_token(%User{} = user, _claims) do def subject_for_token(%User{id: user_id}, _claims) do
{:ok, "User:" <> to_string(user.id)} {:ok, "User:" <> to_string(user_id)}
end
def subject_for_token(%ApplicationToken{id: app_token_id}, _claims) do
{:ok, "AppToken:" <> to_string(app_token_id)}
end end
def subject_for_token(_, _) do def subject_for_token(_, _) do
@ -38,7 +43,30 @@ defmodule Mobilizon.Web.Auth.Guardian do
{:error, :invalid_id} {:error, :invalid_id}
end end
rescue rescue
Ecto.NoResultsError -> {:error, :no_result} e in Ecto.NoResultsError ->
Logger.warn("Received token claim for non existing user: #{inspect(e)}")
{:error, :no_result}
end
end
def resource_from_claims(%{"sub" => "AppToken:" <> id_str}) do
Logger.debug(fn -> "Receiving claim for app token #{id_str}" end)
try do
case Integer.parse(id_str) do
{id, ""} ->
application_token = Applications.get_application_token!(id)
user = Users.get_user_with_actors!(application_token.user_id)
application = Applications.get_application!(application_token.application_id)
{:ok, application_token |> Map.put(:user, user) |> Map.put(:application, application)}
_ ->
{:error, :invalid_id}
end
rescue
e in Ecto.NoResultsError ->
Logger.info("Received token claim for non existing app token: #{inspect(e.message)}")
{:error, :no_result}
end end
end end
@ -55,6 +83,8 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_verify(any(), any(), any()) :: {:ok, map()} | {:error, :token_not_found} @spec on_verify(any(), any(), any()) :: {:ok, map()} | {:error, :token_not_found}
def on_verify(claims, token, _options) do def on_verify(claims, token, _options) do
Logger.debug("[Guardian] Called on_verify")
with {:ok, _} <- Guardian.DB.on_verify(claims, token) do with {:ok, _} <- Guardian.DB.on_verify(claims, token) do
{:ok, claims} {:ok, claims}
end end
@ -62,6 +92,8 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_revoke(any(), any(), any()) :: {:ok, map()} | {:error, :could_not_revoke_token} @spec on_revoke(any(), any(), any()) :: {:ok, map()} | {:error, :could_not_revoke_token}
def on_revoke(claims, token, _options) do def on_revoke(claims, token, _options) do
Logger.debug("[Guardian] Called on_revoke")
with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do with {:ok, _} <- Guardian.DB.on_revoke(claims, token) do
{:ok, claims} {:ok, claims}
end end
@ -70,6 +102,8 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_refresh({any(), any()}, {any(), any()}, any()) :: @spec on_refresh({any(), any()}, {any(), any()}, any()) ::
{:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()} {:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()}
def on_refresh({old_token, old_claims}, {new_token, new_claims}, _options) do def on_refresh({old_token, old_claims}, {new_token, new_claims}, _options) do
Logger.debug("[Guardian] Called on_refresh")
with {:ok, _, _} <- Guardian.DB.on_refresh({old_token, old_claims}, {new_token, new_claims}) do with {:ok, _, _} <- Guardian.DB.on_refresh({old_token, old_claims}, {new_token, new_claims}) do
{:ok, {old_token, old_claims}, {new_token, new_claims}} {:ok, {old_token, old_claims}, {new_token, new_claims}}
end end
@ -77,7 +111,10 @@ defmodule Mobilizon.Web.Auth.Guardian do
@spec on_exchange(any(), any(), any()) :: @spec on_exchange(any(), any(), any()) ::
{:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()} {:ok, {String.t(), map()}, {String.t(), map()}} | {:error, any()}
def on_exchange(old_stuff, new_stuff, options), do: on_refresh(old_stuff, new_stuff, options) def on_exchange(old_stuff, new_stuff, options) do
Logger.debug("[Guardian] Called on_exchange")
on_refresh(old_stuff, new_stuff, options)
end
# def build_claims(claims, _resource, opts) do # def build_claims(claims, _resource, opts) do
# claims = claims # claims = claims

View file

@ -4,19 +4,16 @@ defmodule Mobilizon.Web.GraphQLSocket do
use Absinthe.Phoenix.Socket, use Absinthe.Phoenix.Socket,
schema: Mobilizon.GraphQL.Schema schema: Mobilizon.GraphQL.Schema
alias Mobilizon.Applications.Application, as: AuthApplication
alias Mobilizon.Applications.ApplicationToken
alias Mobilizon.Users.User alias Mobilizon.Users.User
@spec connect(map, Phoenix.Socket.t()) :: {:ok, Phoenix.Socket.t()} | :error @spec connect(map, Phoenix.Socket.t()) :: {:ok, Phoenix.Socket.t()} | :error
def connect(%{"token" => token}, socket) do def connect(%{"token" => token}, socket) do
with {:ok, authed_socket} <- with {:ok, authed_socket} <-
Guardian.Phoenix.Socket.authenticate(socket, Mobilizon.Web.Auth.Guardian, token), Guardian.Phoenix.Socket.authenticate(socket, Mobilizon.Web.Auth.Guardian, token),
%User{} = user <- Guardian.Phoenix.Socket.current_resource(authed_socket) do resource <- Guardian.Phoenix.Socket.current_resource(authed_socket) do
authed_socket = set_context(authed_socket, resource)
Absinthe.Phoenix.Socket.put_options(socket,
context: %{
current_user: user
}
)
{:ok, authed_socket} {:ok, authed_socket}
else else
@ -29,4 +26,27 @@ defmodule Mobilizon.Web.GraphQLSocket do
@spec id(any) :: nil @spec id(any) :: nil
def id(_socket), do: nil def id(_socket), do: nil
@spec set_context(Phoenix.Socket.t(), User.t() | ApplicationToken.t()) :: Phoenix.Socket.t()
defp set_context(socket, %User{} = user) do
Absinthe.Phoenix.Socket.put_options(socket,
context: %{
current_user: user
}
)
end
defp set_context(
socket,
%ApplicationToken{user: %User{} = user, application: %AuthApplication{} = app} =
app_token
) do
Absinthe.Phoenix.Socket.put_options(socket,
context: %{
current_auth_app_token: app_token,
current_auth_app: app,
current_user: user
}
)
end
end end

View file

@ -0,0 +1,444 @@
defmodule Mobilizon.Web.ApplicationController do
use Mobilizon.Web, :controller
alias Mobilizon.Applications.Application
alias Mobilizon.Service.Auth.Applications
plug(:put_layout, false)
import Mobilizon.Web.Gettext, only: [dgettext: 2]
require Logger
@doc """
Create an application
"""
@spec create_application(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create_application(
conn,
%{"name" => name, "redirect_uris" => redirect_uris, "scope" => scope} = args
) do
ip = conn.remote_ip |> :inet.ntoa() |> to_string()
case Hammer.check_rate(
"create_application:#{ip}",
60_000,
10
) do
{:allow, _} ->
case Applications.create(
name,
String.split(redirect_uris, "\n"),
scope,
Map.get(args, "website")
) do
{:ok, %Application{} = app} ->
conn
|> Plug.Conn.put_resp_header("cache-control", "no-store")
|> json(
Map.take(app, [:name, :website, :redirect_uris, :client_id, :client_secret, :scope])
)
{:error, :invalid_scope} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_scope",
"error_description" =>
dgettext(
"errors",
"The scope parameter is not a space separated list of valid scopes"
)
})
{:error, error} ->
Logger.error(inspect(error))
conn
|> Plug.Conn.put_status(500)
|> json(%{
"error" => "server_error",
"error_description" =>
dgettext(
"errors",
"Impossible to create application."
)
})
end
{:deny, _} ->
conn
|> Plug.Conn.put_status(429)
|> json(%{
"error" => "slow_down",
"error_description" =>
dgettext(
"errors",
"Too many requests"
)
})
end
end
def create_application(conn, _args) do
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_request",
"error_description" =>
dgettext(
"errors",
"All of name, scope and redirect_uri parameters are required to create an application"
)
})
end
@doc """
Authorize
"""
@spec authorize(Plug.Conn.t(), map()) :: Plug.Conn.t()
def authorize(
conn,
_args
) do
conn = fetch_query_params(conn)
client_id = conn.query_params["client_id"]
redirect_uri = conn.query_params["redirect_uri"]
state = conn.query_params["state"]
scope = conn.query_params["scope"]
if is_binary(client_id) and is_binary(redirect_uri) and valid_uri?(redirect_uri) and
is_binary(state) and is_binary(scope) do
redirect(conn,
to:
Routes.page_path(conn, :authorize,
client_id: client_id,
redirect_uri: redirect_uri,
scope: scope,
state: state
)
)
else
if is_binary(redirect_uri) and valid_uri?(redirect_uri) do
redirect(conn,
external:
append_parameters(redirect_uri,
error: "invalid_request",
error_description:
dgettext(
"errors",
"You need to specify client_id, redirect_uri, scope and state to autorize an application"
)
)
)
else
send_resp(
conn,
400,
dgettext(
"errors",
"You need to provide a valid redirect_uri to autorize an application"
)
)
end
end
end
def device_code(conn, %{"client_id" => client_id, "scope" => scope}) do
case Applications.register_device_code(client_id, scope) do
{:ok, res} when is_map(res) ->
conn
|> Plug.Conn.put_resp_header("cache-control", "no-store")
|> json(res)
{:error, :scope_not_included} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_scope",
"error_description" =>
dgettext(
"errors",
"The given scope is not in the list of the app declared scopes"
)
})
{:error, :application_not_found} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_client",
"error_description" =>
dgettext(
"errors",
"No application was found with this client_id"
)
})
{:error, %Ecto.Changeset{} = err} ->
Logger.error(inspect(err))
conn
|> Plug.Conn.put_status(500)
|> json(%{
"error" => "server_error",
"error_description" =>
dgettext(
"errors",
"Unable to produce device code"
)
})
end
end
def device_code(conn, _args) do
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_request",
"error_description" =>
dgettext(
"errors",
"You need to pass both client_id and scope as parameters to obtain a device code"
)
})
end
@spec generate_access_token(Plug.Conn.t(), map()) :: Plug.Conn.t()
def generate_access_token(conn, %{
"client_id" => client_id,
"client_secret" => client_secret,
"code" => code,
"redirect_uri" => redirect_uri,
"scope" => scope,
"grant_type" => "authorization_code"
}) do
case do_generate_access_token(client_id, client_secret, code, redirect_uri, scope) do
{:ok, token} ->
conn
|> Plug.Conn.put_resp_header("cache-control", "no-store")
|> json(token)
{:error, code, msg} ->
Logger.debug(msg)
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => to_string(code),
"error_description" => msg
})
end
end
def generate_access_token(conn, %{
"client_id" => client_id,
"device_code" => device_code,
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
}) do
case Applications.generate_access_token_for_device_flow(client_id, device_code) do
{:ok, res} ->
conn
|> Plug.Conn.put_resp_header("cache-control", "no-store")
|> json(res)
{:error, :incorrect_device_code} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_grant",
"error_description" =>
dgettext(
"errors",
"The client_id provided or the device_code associated is invalid"
)
})
{:error, :pending, interval} ->
case Hammer.check_rate(
"generate_device_access_token:#{client_id}:#{device_code}",
interval * 1_000,
1
) do
{:allow, _} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "authorization_pending",
"error_description" =>
dgettext(
"errors",
"The authorization request is still pending"
)
})
{:deny, _} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "slow_down",
"error_description" =>
dgettext(
"errors",
"Please slow down the rate of your requests"
)
})
end
{:error, :access_denied} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "access_denied",
"error_description" =>
dgettext(
"errors",
"The user rejected the requested authorization"
)
})
{:error, :expired} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "expired_token",
"error_description" =>
dgettext(
"errors",
"The given device_code has expired"
)
})
end
end
def generate_access_token(conn, %{
"refresh_token" => refresh_token,
"grant_type" => "refresh_token",
"client_id" => client_id,
"client_secret" => client_secret
}) do
case Applications.refresh_tokens(refresh_token, client_id, client_secret) do
{:ok, res} ->
conn
|> Plug.Conn.put_resp_header("cache-control", "no-store")
|> json(res)
{:error, :invalid_client_credentials} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_client",
"error_description" => dgettext("errors", "Invalid client credentials provided")
})
{:error, :invalid_refresh_token} ->
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_grant",
"error_description" => dgettext("errors", "Invalid refresh token provided")
})
{:error, err} when is_atom(err) ->
conn
|> Plug.Conn.put_status(500)
|> json(%{
"error" => "server_error",
"error_description" => to_string(err)
})
end
end
def generate_access_token(conn, _args) do
conn
|> Plug.Conn.put_status(400)
|> json(%{
"error" => "invalid_request",
"error_description" =>
dgettext(
"errors",
"Incorrect parameters sent. You need to provide at least the grant_type and client_id parameters, depending on the grant type being used."
)
})
end
def revoke_token(conn, %{"token" => token} = _args) do
case Applications.revoke_token(token) do
{:ok, _res} ->
send_resp(conn, 200, "")
{:error, _, _, _} ->
conn
|> Plug.Conn.put_status(500)
|> json(%{
"error" => "server_error",
"error_description" => dgettext("errors", "Unable to revoke token")
})
{:error, :token_not_found} ->
conn
|> Plug.Conn.put_status(:not_found)
|> json(%{
"error" => "invalid_request",
"error_description" => dgettext("errors", "Token not found")
})
end
end
@spec do_generate_access_token(String.t(), String.t(), String.t(), String.t(), String.t()) ::
{:ok, Applications.access_token_details()} | {:error, atom(), String.t()}
defp do_generate_access_token(client_id, client_secret, code, redirect_uri, scope) do
case Applications.generate_access_token(
client_id,
client_secret,
code,
redirect_uri,
scope
) do
{:ok, token} ->
{:ok, token}
{:error, :application_not_found} ->
{:error, :invalid_request,
dgettext("errors", "No application was found with this client_id")}
{:error, :redirect_uri_not_in_allowed} ->
{:error, :invalid_request, dgettext("errors", "This redirect URI is not allowed")}
{:error, :invalid_or_expired} ->
{:error, :invalid_grant, dgettext("errors", "The provided code is invalid or expired")}
{:error, :provided_code_does_not_match} ->
{:error, :invalid_grant,
dgettext("errors", "The provided client_id does not match the provided code")}
{:error, :invalid_client_secret} ->
{:error, :invalid_client, dgettext("errors", "The provided client_secret is invalid")}
{:error, :scope_not_included} ->
{:error, :invalid_scope,
dgettext(
"errors",
"The provided scope is invalid or not included in the app declared scopes"
)}
end
end
defp valid_uri?(url) do
uri = URI.parse(url)
uri.scheme != nil and uri.host =~ "."
end
@spec append_parameters(String.t(), Enum.t()) :: String.t()
defp append_parameters(uri_str, parameters) do
query_parameters = URI.encode_query(parameters)
case URI.parse(uri_str) do
%URI{query: nil} = uri ->
uri
|> URI.merge(%URI{query: query_parameters})
|> URI.to_string()
uri ->
"#{URI.to_string(uri)}&#{query_parameters}"
end
end
end

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