Initial commit
This commit is contained in:
1
server/api/ping.ts
Normal file
1
server/api/ping.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default eventHandler(() => 'pong')
|
||||
13
server/api/preferences.get.ts
Normal file
13
server/api/preferences.get.ts
Normal 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;
|
||||
});
|
||||
18
server/api/preferences.put.ts
Normal file
18
server/api/preferences.put.ts
Normal 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;
|
||||
});
|
||||
19
server/database/migrations/0000_tearful_the_initiative.sql
Normal file
19
server/database/migrations/0000_tearful_the_initiative.sql
Normal 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
|
||||
);
|
||||
14
server/database/migrations/0001_spooky_gravity.sql
Normal file
14
server/database/migrations/0001_spooky_gravity.sql
Normal 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;
|
||||
1
server/database/migrations/0002_plain_shooting_star.sql
Normal file
1
server/database/migrations/0002_plain_shooting_star.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE UNIQUE INDEX `tokens_user_id_unique` ON `tokens` (`user_id`);
|
||||
15
server/database/migrations/0003_perpetual_bill_hollister.sql
Normal file
15
server/database/migrations/0003_perpetual_bill_hollister.sql
Normal 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`);
|
||||
@@ -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`);
|
||||
144
server/database/migrations/meta/0000_snapshot.json
Normal file
144
server/database/migrations/meta/0000_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
143
server/database/migrations/meta/0001_snapshot.json
Normal file
143
server/database/migrations/meta/0001_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
151
server/database/migrations/meta/0002_snapshot.json
Normal file
151
server/database/migrations/meta/0002_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
151
server/database/migrations/meta/0003_snapshot.json
Normal file
151
server/database/migrations/meta/0003_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
204
server/database/migrations/meta/0004_snapshot.json
Normal file
204
server/database/migrations/meta/0004_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
41
server/database/migrations/meta/_journal.json
Normal file
41
server/database/migrations/meta/_journal.json
Normal 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
89
server/database/schema.ts
Normal 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
34
server/plugins/webhook.ts
Normal 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!");
|
||||
});
|
||||
});
|
||||
65
server/routes/auth/strava.ts
Normal file
65
server/routes/auth/strava.ts
Normal 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, "/");
|
||||
},
|
||||
});
|
||||
58
server/routes/webhooks/strava/activity-create.post.ts
Normal file
58
server/routes/webhooks/strava/activity-create.post.ts
Normal 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"),
|
||||
},
|
||||
});
|
||||
});
|
||||
17
server/routes/webhooks/strava/athelete-update.post.ts
Normal file
17
server/routes/webhooks/strava/athelete-update.post.ts
Normal 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);
|
||||
});
|
||||
5
server/routes/webhooks/strava/index.get.ts
Normal file
5
server/routes/webhooks/strava/index.get.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
|
||||
return { "hub.challenge": query["hub.challenge"] };
|
||||
});
|
||||
14
server/routes/webhooks/strava/index.post.ts
Normal file
14
server/routes/webhooks/strava/index.post.ts
Normal 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
3
server/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
||||
13
server/utils/drizzle.ts
Normal file
13
server/utils/drizzle.ts
Normal 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;
|
||||
66
server/utils/strava-client.ts
Normal file
66
server/utils/strava-client.ts
Normal 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}`);
|
||||
},
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user