adds strava integration

This commit is contained in:
Christian Beutel
2025-02-04 11:43:48 +01:00
parent 3ab591177a
commit a0b6480854
32 changed files with 1737 additions and 57 deletions

View File

@@ -0,0 +1,23 @@
import { IntegrationCreateSchema } from "$lib/models/api/integration_schema";
import type { Integration } from "$lib/models/integration";
import { Collection, create, handleError, list } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await list<Integration>(event, Collection.integrations);
return json(r)
} catch (e) {
throw handleError(e)
}
}
export async function PUT(event: RequestEvent) {
try {
const r = await create<Comment>(event, IntegrationCreateSchema, Collection.integrations)
return json(r);
} catch (e) {
throw handleError(e)
}
}

View File

@@ -0,0 +1,32 @@
import { IntegrationUpdateSchema } from "$lib/models/api/integration_schema";
import type { Integration } from "$lib/models/integration";
import { pb } from "$lib/pocketbase";
import { Collection, handleError, remove, show, update } from "$lib/util/api_util";
import { json, type RequestEvent } from "@sveltejs/kit";
export async function GET(event: RequestEvent) {
try {
const r = await show<Integration>(event, Collection.integrations)
return json(r)
} catch (e: any) {
throw handleError(e)
}
}
export async function POST(event: RequestEvent) {
try {
const r = await update<Integration>(event, IntegrationUpdateSchema, Collection.integrations)
return json(r);
} catch (e: any) {
throw handleError(e)
}
}
export async function DELETE(event: RequestEvent) {
try {
const r = await remove(event, Collection.integrations)
return json(r);
} catch (e: any) {
throw handleError(e)
}
}

View File

@@ -21,6 +21,7 @@
},
{ text: $_("notifications"), value: "/settings/notifications" },
{ text: $_("map"), value: "/settings/map" },
{ text: $_("integrations"), value: "/settings/integrations" },
{ text: `${$_("import")}/${$_("export")}`, value: "/settings/export" },
{
text: $_("help"),

View File

@@ -0,0 +1,201 @@
<script lang="ts">
import { page } from "$app/state";
import Button from "$lib/components/base/button.svelte";
import Modal from "$lib/components/base/modal.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import Toggle from "$lib/components/base/toggle.svelte";
import { StravaSchema } from "$lib/models/api/integration_schema.js";
import { Integration } from "$lib/models/integration.js";
import {
integrations_create,
integrations_update,
} from "$lib/stores/integration_store.js";
import { show_toast } from "$lib/stores/toast_store.js";
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { _ } from "svelte-i18n";
let { data } = $props();
let integration = $state(data.integration);
const scope = "read_all,activity:read_all";
const redirectUri = page.url.href + "/callback/strava";
let stravaSettingsModal: Modal;
let stravaToggleValue: boolean = $derived(
integration?.strava?.active || false,
);
const { form, errors, data: d } = createForm({
initialValues: {
clientId: data.integration?.strava?.clientId ?? "",
clientSecret: data.integration?.strava?.clientSecret ?? "",
routes: data.integration?.strava?.routes ?? true,
activities: data.integration?.strava?.activities ?? true,
active: data.integration?.strava?.active ?? false,
},
extend: validator({
schema: StravaSchema,
}),
onSubmit: async (form) => {
stravaSettingsModal.closeModal();
try {
if (integration) {
integration.strava = {
clientId: form.clientId,
clientSecret: form.clientSecret,
routes: form.routes,
activities: form.activities,
active: integration.strava?.active ?? false,
};
integration = await integrations_update(integration);
} else {
const newIntegration = new Integration("", {
routes: form.routes,
activities: form.activities,
clientId: form.clientId,
clientSecret: form.clientSecret,
active: false,
});
integration = await integrations_create(newIntegration);
}
} catch (e) {
show_toast({
text: $_("error-setting-up-strava-integration"),
icon: "close",
type: "error",
});
}
},
});
async function onStravaToggle(value: boolean) {
if (!integration?.strava) {
return;
}
if (value) {
const authUrl = `https://www.strava.com/oauth/authorize?client_id=${integration.strava.clientId}&response_type=code&redirect_uri=${redirectUri}&scope=${scope}&approval_prompt=auto`;
window.location.href = authUrl;
} else {
const deauthUrl = `https://www.strava.com/oauth/deauthorize`;
const r = await fetch(deauthUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${integration.strava.accessToken}`,
},
});
if (!r.ok) {
show_toast({
text: $_("error-disabling-strava-integration"),
icon: "close",
type: "error",
});
return;
}
integration.strava = {
clientId: integration.strava.clientId,
clientSecret: integration.strava.clientSecret,
routes: integration.strava.routes,
activities: integration.strava.activities,
accessToken: undefined,
refreshToken: undefined,
expiresAt: undefined,
active: false,
};
try {
integration = await integrations_update(integration);
} catch (e) {
show_toast({
text: $_("error-disabling-strava-integration"),
icon: "close",
type: "error",
});
}
}
}
</script>
<svelte:head>
<title>{$_("settings")} | wanderer</title>
</svelte:head>
<h3 class="text-2xl font-semibold">{$_("integrations")}</h3>
<hr class="mt-4 mb-6 border-input-border" />
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<div class="border border-input-border rounded-lg p-4 space-y-4">
<img
src="https://upload.wikimedia.org/wikipedia/commons/c/cb/Strava_Logo.svg"
alt="strava logo"
/>
<div>
<h5 class="text-xl font-semibold">strava</h5>
<p class="text-sm text-gray-500">
Syncs your strava routes with wanderer in regular intervals.
</p>
</div>
<div class="flex items-center justify-between">
<button
class="btn-secondary"
onclick={() => stravaSettingsModal.openModal()}
><i class="fa fa-cogs mr-2"></i>{$_("settings")}</button
>
<Toggle
value={stravaToggleValue}
onchange={onStravaToggle}
disabled={!integration?.strava}
></Toggle>
</div>
</div>
</div>
<Modal
id="strava-settings-modal"
size="max-w-lg"
title={"strava " + $_("settings")}
bind:this={stravaSettingsModal}
>
{#snippet content()}
<form id="strava-settings-form" class="space-y-2" use:form>
<TextField
label="Client-ID"
placeholder="000000"
name="clientId"
error={$errors.clientId}
></TextField>
<TextField
label="Client Secret"
placeholder="de8b3789bd7116d..."
name="clientSecret"
type="password"
error={$errors.clientSecret}
></TextField>
<div class="flex gap-x-4">
<Toggle name="routes" label={$_("route", { values: { n: 2 } })}
></Toggle>
<Toggle
name="activities"
label={$_("activity", { values: { n: 2 } })}
></Toggle>
</div>
</form>
{/snippet}
{#snippet footer()}
<div class="flex items-center gap-4">
<button
class="btn-secondary"
onclick={() => stravaSettingsModal.closeModal()}
>{$_("cancel")}</button
>
<button
class="btn-primary"
form="strava-settings-form"
type="submit"
name="save">{$_("save")}</button
>
</div>
{/snippet}</Modal
>

View File

@@ -0,0 +1,7 @@
import { integrations_index } from "$lib/stores/integration_store";
import { type Load } from "@sveltejs/kit";
export const load: Load = async ({ params, fetch }) => {
const integrations = await integrations_index(fetch)
return { integration: integrations.at(0) }
};

View File

@@ -0,0 +1,55 @@
import { integrations_index, integrations_update } from "$lib/stores/integration_store";
import { error, redirect, type RequestEvent, type ServerLoad } from "@sveltejs/kit";
export const load: ServerLoad = async ({ url, fetch }) => {
const oauthError = url.searchParams.get('error');
if (oauthError) {
return error(400, {
message: oauthError
});
}
const code = url.searchParams.get('code');
if (!code) {
return error(400, {
message: "No code provided"
});
}
const integrations = await integrations_index(fetch);
if (!integrations.length || !integrations[0].strava) {
return error(400, {
message: "Missing integration record"
});
}
const integration = integrations[0]
const tokenResponse = await fetch('https://www.strava.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: integration.strava?.clientId,
client_secret: integration.strava?.clientSecret,
code,
grant_type: 'authorization_code'
})
});
if (!tokenResponse.ok) {
const r = await tokenResponse.json()
console.error(r)
return error(500, 'Failed to get access token');
}
const { access_token, refresh_token, expires_at } = await tokenResponse.json();
integration.strava!.accessToken = access_token
integration.strava!.refreshToken = refresh_token
integration.strava!.expiresAt = expires_at
integration.strava!.active = true
await integrations_update(integration, fetch);
return redirect(302, '/settings/integrations')
}