Initial commit

This commit is contained in:
2025-04-07 12:59:20 +03:00
commit d47a21fcbf
47 changed files with 12696 additions and 0 deletions

1
server/api/ping.ts Normal file
View File

@@ -0,0 +1 @@
export default eventHandler(() => 'pong')

View File

@@ -0,0 +1,13 @@
export default defineEventHandler(async (event) => {
const session = await requireUserSession(event);
const db = useDrizzle();
const user = await db.query.users.findFirst({
where: (f, o) => o.eq(f.id, session.user.id),
with: {
preferences: true,
},
});
return user?.preferences.data;
});

View File

@@ -0,0 +1,18 @@
export default defineEventHandler(async (event) => {
const session = await requireUserSession(event);
const db = useDrizzle();
const body = await readBody(event);
const [preferences] = await db
.update(tables.preferences)
.set({
data: {
enabled: body.enabled,
language: body.language,
},
})
.where(eq(tables.preferences.userId, session.user.id))
.returning();
return preferences.data;
});

View File

@@ -0,0 +1,19 @@
CREATE TABLE `tokens` (
`user_id` integer PRIMARY KEY NOT NULL,
`refresh_token` text,
`access_token` text,
`expires_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE UNIQUE INDEX `tokens_user_id_unique` ON `tokens` (`user_id`);--> statement-breakpoint
CREATE TABLE `users` (
`id` integer PRIMARY KEY NOT NULL,
`name` text NOT NULL,
`avatar` text NOT NULL,
`city` text,
`country` text,
`sex` text,
`weight` integer,
`created_at` integer
);

View File

@@ -0,0 +1,14 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_tokens` (
`id` integer PRIMARY KEY NOT NULL,
`user_id` integer,
`refresh_token` text,
`access_token` text,
`expires_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
INSERT INTO `__new_tokens`("id", "user_id", "refresh_token", "access_token", "expires_at") SELECT "id", "user_id", "refresh_token", "access_token", "expires_at" FROM `tokens`;--> statement-breakpoint
DROP TABLE `tokens`;--> statement-breakpoint
ALTER TABLE `__new_tokens` RENAME TO `tokens`;--> statement-breakpoint
PRAGMA foreign_keys=ON;

View File

@@ -0,0 +1 @@
CREATE UNIQUE INDEX `tokens_user_id_unique` ON `tokens` (`user_id`);

View File

@@ -0,0 +1,15 @@
PRAGMA foreign_keys=OFF;--> statement-breakpoint
CREATE TABLE `__new_tokens` (
`id` integer PRIMARY KEY NOT NULL,
`user_id` integer,
`refresh_token` text,
`access_token` text,
`expires_at` integer NOT NULL,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
INSERT INTO `__new_tokens`("id", "user_id", "refresh_token", "access_token", "expires_at") SELECT "id", "user_id", "refresh_token", "access_token", "expires_at" FROM `tokens`;--> statement-breakpoint
DROP TABLE `tokens`;--> statement-breakpoint
ALTER TABLE `__new_tokens` RENAME TO `tokens`;--> statement-breakpoint
PRAGMA foreign_keys=ON;--> statement-breakpoint
CREATE UNIQUE INDEX `tokens_user_id_unique` ON `tokens` (`user_id`);

View File

@@ -0,0 +1,8 @@
CREATE TABLE `preferences` (
`id` integer PRIMARY KEY NOT NULL,
`user_id` integer,
`data` jsonb,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE UNIQUE INDEX `preferences_user_id_unique` ON `preferences` (`user_id`);

View File

@@ -0,0 +1,144 @@
{
"version": "6",
"dialect": "sqlite",
"id": "7cffca6e-97cc-4159-bf48-2f8bbd7aded4",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"tokens": {
"name": "tokens",
"columns": {
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"tokens_user_id_unique": {
"name": "tokens_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"tokens_user_id_users_id_fk": {
"name": "tokens_user_id_users_id_fk",
"tableFrom": "tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"city": {
"name": "city",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sex": {
"name": "sex",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"weight": {
"name": "weight",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,143 @@
{
"version": "6",
"dialect": "sqlite",
"id": "9b994045-7d5b-470e-bd5a-bf4bf93bb270",
"prevId": "7cffca6e-97cc-4159-bf48-2f8bbd7aded4",
"tables": {
"tokens": {
"name": "tokens",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"tokens_user_id_users_id_fk": {
"name": "tokens_user_id_users_id_fk",
"tableFrom": "tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"city": {
"name": "city",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sex": {
"name": "sex",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"weight": {
"name": "weight",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,151 @@
{
"version": "6",
"dialect": "sqlite",
"id": "2aec8d0e-ddc1-49ff-983c-ac0f5be3d10e",
"prevId": "9b994045-7d5b-470e-bd5a-bf4bf93bb270",
"tables": {
"tokens": {
"name": "tokens",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"tokens_user_id_unique": {
"name": "tokens_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"tokens_user_id_users_id_fk": {
"name": "tokens_user_id_users_id_fk",
"tableFrom": "tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"city": {
"name": "city",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sex": {
"name": "sex",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"weight": {
"name": "weight",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,151 @@
{
"version": "6",
"dialect": "sqlite",
"id": "828a623e-bbd1-4a3e-adcf-1ac58e81c9b3",
"prevId": "2aec8d0e-ddc1-49ff-983c-ac0f5be3d10e",
"tables": {
"tokens": {
"name": "tokens",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"tokens_user_id_unique": {
"name": "tokens_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"tokens_user_id_users_id_fk": {
"name": "tokens_user_id_users_id_fk",
"tableFrom": "tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"city": {
"name": "city",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sex": {
"name": "sex",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"weight": {
"name": "weight",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,204 @@
{
"version": "6",
"dialect": "sqlite",
"id": "b1388198-f0bd-4d77-ba71-d5740f1a09a7",
"prevId": "828a623e-bbd1-4a3e-adcf-1ac58e81c9b3",
"tables": {
"preferences": {
"name": "preferences",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"data": {
"name": "data",
"type": "jsonb",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"preferences_user_id_unique": {
"name": "preferences_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"preferences_user_id_users_id_fk": {
"name": "preferences_user_id_users_id_fk",
"tableFrom": "preferences",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"tokens": {
"name": "tokens",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"user_id": {
"name": "user_id",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"tokens_user_id_unique": {
"name": "tokens_user_id_unique",
"columns": [
"user_id"
],
"isUnique": true
}
},
"foreignKeys": {
"tokens_user_id_users_id_fk": {
"name": "tokens_user_id_users_id_fk",
"tableFrom": "tokens",
"tableTo": "users",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"avatar": {
"name": "avatar",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"city": {
"name": "city",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"country": {
"name": "country",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"sex": {
"name": "sex",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"weight": {
"name": "weight",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"created_at": {
"name": "created_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@@ -0,0 +1,41 @@
{
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1743665160397,
"tag": "0000_tearful_the_initiative",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1743691140050,
"tag": "0001_spooky_gravity",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1743691375389,
"tag": "0002_plain_shooting_star",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1743691967975,
"tag": "0003_perpetual_bill_hollister",
"breakpoints": true
},
{
"idx": 4,
"version": "6",
"when": 1743776869050,
"tag": "0004_cultured_doctor_strange",
"breakpoints": true
}
]
}

89
server/database/schema.ts Normal file
View File

@@ -0,0 +1,89 @@
import { relations } from "drizzle-orm";
import {
sqliteTable,
text,
integer,
customType,
} from "drizzle-orm/sqlite-core";
const customJsonb = <TData>(name: string) =>
customType<{ data: TData; driverData: string }>({
dataType() {
return "jsonb";
},
toDriver(value: TData): string {
return JSON.stringify(value);
},
fromDriver(value: string): TData {
return JSON.parse(value);
},
})(name);
export const users = sqliteTable("users", {
id: integer("id").primaryKey(),
name: text("name").notNull(),
avatar: text("avatar").notNull(),
city: text("city"),
country: text("country"),
sex: text("sex"),
weight: integer("weight"),
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
() => new Date()
),
});
export const preferences = sqliteTable("preferences", {
id: integer("id").primaryKey(),
userId: integer("user_id")
.references(() => users.id, {
onDelete: "cascade",
})
.unique(),
data: customJsonb("data")
.$type<{
enabled: boolean;
language: string;
}>()
.$defaultFn(() => ({
enabled: true,
language: "English",
})),
});
export const tokens = sqliteTable("tokens", {
id: integer("id").primaryKey(),
userId: integer("user_id")
.references(() => users.id, {
onDelete: "cascade",
})
.unique(),
refreshToken: text("refresh_token"),
accessToken: text("access_token"),
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
});
// Define relationships
export const usersRelations = relations(users, ({ one }) => ({
tokens: one(tokens, {
fields: [users.id],
references: [tokens.userId],
}),
preferences: one(preferences, {
fields: [users.id],
references: [preferences.userId],
}),
}));
export const referencesRelations = relations(preferences, ({ one }) => ({
user: one(users, {
fields: [preferences.userId],
references: [users.id],
}),
}));
export const tokensRelations = relations(tokens, ({ one }) => ({
user: one(users, {
fields: [tokens.userId],
references: [users.id],
}),
}));

34
server/plugins/webhook.ts Normal file
View File

@@ -0,0 +1,34 @@
import { isEmpty } from "radash";
import { URLSearchParams } from "url";
export default defineNitroPlugin(() => {
onHubReady(async () => {
const config = useRuntimeConfig();
const webhooks = await $fetch(
"https://www.strava.com/api/v3/push_subscriptions",
{
params: {
client_id: config.oauth.strava.clientId,
client_secret: config.oauth.strava.clientSecret,
},
},
);
if (!isEmpty(webhooks)) {
return;
}
await $fetch("https://www.strava.com/api/v3/push_subscriptions", {
method: "post",
body: new URLSearchParams({
client_id: config.oauth.strava.clientId,
client_secret: config.oauth.strava.clientSecret,
callback_url: config.webhooksUrl,
verify_token: config.stravaVerifyToken,
}),
});
console.log("Webhook registered successfully!");
});
});

View File

@@ -0,0 +1,65 @@
import { omit } from "radash";
export default defineOAuthStravaEventHandler({
config: {
scope: ["read,activity:read,activity:write"],
},
onSuccess: async (event, auth) => {
const userPayload = {
id: auth.user.id,
name: `${auth.user.firstname} ${auth.user.lastname}`,
city: auth.user.city,
country: auth.user.country,
sex: auth.user.sex,
weight: auth.user.weight,
avatar: auth.user.profile,
};
await setUserSession(event, {
user: userPayload,
});
const db = useDrizzle();
const [user] = await db
.insert(tables.users)
.values(userPayload)
.onConflictDoUpdate({
target: tables.users.id,
set: omit(userPayload, ["id"]),
})
.returning();
const tokenExpiration = new Date(auth.tokens.expires_at * 1000);
await db
.insert(tables.tokens)
.values({
userId: user.id,
refreshToken: auth.tokens.refresh_token,
accessToken: auth.tokens.access_token,
expiresAt: tokenExpiration,
})
.onConflictDoUpdate({
target: tables.tokens.userId,
set: {
refreshToken: auth.tokens.refresh_token,
accessToken: auth.tokens.access_token,
expiresAt: tokenExpiration,
},
});
await db
.insert(tables.preferences)
.values({
userId: user.id,
data: {
enabled: true,
language: "english",
},
})
.onConflictDoNothing();
sendRedirect(event, "/");
},
});

View File

@@ -0,0 +1,58 @@
import { get } from "radash";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const db = useDrizzle();
const ai = hubAI();
const user = await db.query.users.findFirst({
where: (f, o) => o.eq(f.id, body.owner_id),
with: {
preferences: true,
},
});
if (!user?.preferences.data?.enabled) {
return;
}
const strava = await useStrava(body.owner_id);
const activity = await strava!(`/activities/${body.object_id}`);
const aiResponse = await ai.run("@cf/meta/llama-3-8b-instruct", {
response_format: {
type: "json_schema",
json_schema: {
type: "object",
properties: {
title: "string",
description: "string",
},
required: ["title", "description"],
},
},
prompt: `
Generate a title and a short description for my strava activity. Use my preferred language. Make sure to include emojis and make it fun.
My user profile:
Sex: ${user?.sex}
City: ${user?.city}
Country: ${user?.country}
Weight: ${user?.weight}
Language: ${user?.preferences.data.language}
The activity data in json format:
${JSON.stringify(activity)}
`,
});
await strava!(`activities/${body.object_id}`, {
method: "PUT",
body: {
name: get(aiResponse, "response.title"),
description: get(aiResponse, "response.description"),
},
});
});

View File

@@ -0,0 +1,17 @@
import { get, isEmpty } from "radash";
import { eq } from "drizzle-orm";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const db = useDrizzle();
if (get(body, "updates.authorized") !== false) {
return;
}
await db
.delete(tables.users)
.where(eq(tables.users.id, get(body, "object_id")));
sendNoContent(event);
});

View File

@@ -0,0 +1,5 @@
export default defineEventHandler(async (event) => {
const query = getQuery(event);
return { "hub.challenge": query["hub.challenge"] };
});

View File

@@ -0,0 +1,14 @@
import { get } from "radash";
export default defineEventHandler(async (event) => {
const body = await readBody(event);
const db = useDrizzle();
const aspectType = get(body, "aspect_type");
const objectType = get(body, "object_type");
await $fetch(`/webhooks/strava/${objectType}-${aspectType}`, {
method: "post",
body,
});
});

3
server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

13
server/utils/drizzle.ts Normal file
View File

@@ -0,0 +1,13 @@
import { drizzle } from "drizzle-orm/d1";
export { sql, eq, and, or } from "drizzle-orm";
import * as schema from "../database/schema";
export const tables = schema;
export function useDrizzle() {
return drizzle(hubDatabase(), { schema });
}
export type User = typeof schema.users.$inferSelect;
export type Tokens = typeof schema.tokens.$inferSelect;

View File

@@ -0,0 +1,66 @@
import { get, isEmpty } from "radash";
import { isAfter } from "@formkit/tempo";
import { eq } from "drizzle-orm";
import { URLSearchParams } from "url";
const refreshStravaToken = async (refreshToken: string) => {
const config = useRuntimeConfig();
const tokensResponse = await $fetch("https://www.strava.com/oauth/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: config.oauth.strava.clientId,
client_secret: config.oauth.strava.clientSecret,
grant_type: "refresh_token",
refresh_token: refreshToken,
}),
});
return {
refreshToken: get(tokensResponse, "refresh_token"),
accessToken: get(tokensResponse, "access_token"),
expiresAt: new Date(get(tokensResponse, "expires_at", 0) * 1000),
needsUpdate: true,
};
};
export const useStrava = async (userId: number) => {
const db = useDrizzle();
const now = new Date();
const user = await db.query.users.findFirst({
where: (f, o) => o.eq(f.id, userId),
with: {
tokens: true,
},
});
if (isEmpty(user)) {
console.error("No user found.");
return;
}
const tokens = isAfter(now, user?.tokens?.expiresAt!)
? await refreshStravaToken(user?.tokens?.refreshToken!)
: user?.tokens;
if (get(tokens, "needsUpdate", false)) {
db.update(tables.tokens)
.set({
refreshToken: tokens?.refreshToken as string,
accessToken: tokens?.accessToken as string,
expiresAt: tokens?.expiresAt,
})
.where(eq(tables.tokens.userId, userId));
}
return $fetch.create({
baseURL: "https://www.strava.com/api/v3/",
onRequest({ options }) {
options.headers.set("Authorization", `Bearer ${tokens?.accessToken}`);
},
});
};