Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ services:
networks:
- daim

sqld:
image: ghcr.io/tursodatabase/libsql-server:latest
ports:
- 127.0.0.1:8080:8080
volumes:
- sqld-data:/var/lib/sqld
networks:
- daim

networks:
daim:
driver: bridge
13 changes: 13 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Config } from 'drizzle-kit';
import * as dotenv from 'dotenv';
dotenv.config();

export default {
out: 'server/database/migrations',
schema: 'server/database/schema',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_DB_URL as string,
authToken: process.env.TURSO_DB_TOKEN as string,
},
} satisfies Config;
1 change: 1 addition & 0 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ export default defineNuxtConfig({
prerender: {
crawlLinks: true,
},
moduleSideEffects: ['lucia/polyfill/node'],
},
sourcemap: true,

Expand Down
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
"icons": "ts-node generate-iconify.ts",
"test": "playwright test",
"test:e2e": "yarn run test",
"drizzle:generate": "drizzle-kit generate:sqlite",
"drizzle:push": "drizzle-kit push:sqlite",
"drizzle:studio": "drizzle-kit studio",
"postinstall": "nuxi prepare"
},
"engines": {
Expand All @@ -44,18 +47,24 @@
"dependencies": {
"@formkit/nuxt": "0.16.2",
"@formkit/themes": "0.16.2",
"@libsql/client": "0.4.0-pre.7",
"@lucia-auth/adapter-sqlite": "2.0.1",
"@nuxt/content": "2.9.0",
"@nuxtjs/robots": "3.0.0",
"@vercel/analytics": "1.0.1",
"buffer": "6.0.3",
"cross-env": "7.0.3",
"crypto-js": "4.1.1",
"dayjs": "1.10.7",
"drizzle-orm": "0.29.3",
"github-markdown-css": "4.0.0",
"js-base64": "3.7.5",
"lodash-es": "4.17.21",
"lucia": "2.7.6",
"mailgo": "0.12.2",
"query-string": "7.1.3"
"qs": "6.11.2",
"query-string": "7.1.3",
"valibot": "0.26.0"
},
"devDependencies": {
"@iconify/tools": "2.2.6",
Expand All @@ -76,6 +85,7 @@
"@types/lodash": "4.14.191",
"@types/lodash-es": "4.17.6",
"@types/node": "20.10.4",
"@types/qs": "^6",
"@types/sharp": "0.31.0",
"@typescript-eslint/eslint-plugin": "5.47.0",
"@typescript-eslint/parser": "5.56.0",
Expand All @@ -84,6 +94,7 @@
"@unocss/nuxt": "0.51.4",
"@vite-pwa/nuxt": "0.0.7",
"cspell": "5.9.0",
"drizzle-kit": "0.20.13",
"eslint": "8.55.0",
"eslint-config-prettier": "8.8.0",
"eslint-import-resolver-typescript": "3.6.1",
Expand Down
77 changes: 77 additions & 0 deletions server/api/contents/[type].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { sortBy } from 'lodash-es';
import {
optional,
object,
integer,
number,
coerce,
safeParse,
array,
string,
} from 'valibot';
import { parse } from 'qs';
import { parseURL } from 'ufo';
import { serverQueryContent } from '#content/server';
import textLength from '~/utils/feature-text-length';
import priceSort from '~/utils/price-sort';

export default defineEventHandler(async (event) => {
const type: string = getRouterParam(event, 'type') ?? '';
const query = parse(parseURL(event.path).search, {
ignoreQueryPrefix: true,
comma: true,
});
const contentQuerySchema = object({
limit: optional(coerce(number([integer()]), Number), 24),
page: optional(coerce(number([integer()]), Number), 1),
fields: optional(
object({
[type]: coerce(array(string()), (value) =>
Array.isArray(value) ? value : Array(value),
),
}),
),
});
const validation = safeParse(contentQuerySchema, query);
if (!validation.success) {
setResponseStatus(event, 400);
return { errors: validation.issues };
}
const input = validation.output;
const limit = input.limit ?? 24;
const page = input.page ?? 1;
const total = await serverQueryContent(event, type)
.where({ deleted_at: { $exists: false } })
.count();

const fields = input?.fields?.[type] ?? [];
if (fields.length) {
fields.push('_path');
}
const contentQuery = serverQueryContent(event, type)
.only(fields)
.where({ deleted_at: { $exists: false } })
.skip((page - 1) * limit)
.limit(limit);

const data = await contentQuery.find();

const sorts = [priceSort, textLength, 'slug'];
const sorted = sortBy(data, sorts).reverse();
const items = sorted.map((item) => {
const slug = item?._path?.replace(`/${type}/`, '');
return {
id: slug,
attributes: {
...item,
url: item.offers ? `/${type}/${slug}` : item.url,
image: slug ? `/img/${type}/${slug}.png` : undefined,
imageColor: item.color,
},
};
});
return {
meta: { pagination: { total, page, limit } },
data: items,
};
});
6 changes: 6 additions & 0 deletions server/api/links.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { links } from '~/server/database/schema/links';

export default defineEventHandler(async () => {
const data = await db.select().from(links).all();
return { data };
});
15 changes: 15 additions & 0 deletions server/api/links.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { links } from '~/server/database/schema/links';

export default defineEventHandler(async (event) => {
const authRequest = auth.handleRequest(event);
const session = await authRequest.validate();
if (!session) {
throw createError({
message: 'Unauthorized',
statusCode: 401,
});
}
const body = await readBody(event);
const result = await db.insert(links).values(body).returning().run();
return { data: result };
});
58 changes: 58 additions & 0 deletions server/api/login.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { LuciaError } from 'lucia';

export default defineEventHandler(async (event) => {
const { username, password } = await readBody<{
username: unknown;
password: unknown;
}>(event);
// basic check
if (
typeof username !== 'string' ||
username.length < 1 ||
username.length > 255
) {
throw createError({
message: 'Invalid username',
statusCode: 400,
});
}
if (
typeof password !== 'string' ||
password.length < 1 ||
password.length > 255
) {
throw createError({
message: 'Invalid password',
statusCode: 400,
});
}
try {
// find user by key
// and validate password
const key = await auth.useKey('email', username.toLowerCase(), password);
const session = await auth.createSession({
userId: key.userId,
attributes: {},
});
const authRequest = auth.handleRequest(event);
authRequest.setSession(session);
return sendRedirect(event, '/api/me'); // redirect to profile page
} catch (e) {
if (
e instanceof LuciaError &&
(e.message === 'AUTH_INVALID_KEY_ID' ||
e.message === 'AUTH_INVALID_PASSWORD')
) {
// user does not exist
// or invalid password
throw createError({
message: 'Incorrect username or password',
statusCode: 400,
});
}
throw createError({
message: 'An unknown error occurred',
statusCode: 500,
});
}
});
7 changes: 7 additions & 0 deletions server/api/me.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default defineEventHandler(async (event) => {
const authRequest = auth.handleRequest(event);
const session = await authRequest.validate();
return {
user: session?.user ?? null,
};
});
60 changes: 60 additions & 0 deletions server/api/signup.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// import { SqliteError } from 'better-sqlite3';

export default defineEventHandler(async (event) => {
const { username, password } = await readBody<{
username: unknown;
password: unknown;
}>(event);
// basic check
if (
typeof username !== 'string' ||
username.length < 4 ||
username.length > 255
) {
throw createError({
message: 'Invalid username',
statusCode: 400,
});
}
if (
typeof password !== 'string' ||
password.length < 6 ||
password.length > 255
) {
throw createError({
message: 'Invalid password',
statusCode: 400,
});
}
// try {
const user = await auth.createUser({
key: {
providerId: 'email', // auth method
providerUserId: username.toLowerCase(), // unique id when using "username" auth method
password, // hashed by Lucia
},
attributes: {
email: username,
},
});
const session = await auth.createSession({
userId: user.userId,
attributes: {},
});
const authRequest = auth.handleRequest(event);
authRequest.setSession(session);
return sendRedirect(event, '/api/me'); // redirect to profile page
// } catch (e) {
// check for unique constraint error in user table
// if (e instanceof SqliteError && e.code === 'SQLITE_CONSTRAINT_UNIQUE') {
// throw createError({
// message: 'Username already taken',
// statusCode: 400,
// });
// }
// throw createError({
// message: 'An unknown error occurred',
// statusCode: 500,
// });
// }
});
14 changes: 14 additions & 0 deletions server/api/users.get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { users } from '~/server/database/schema/users';

export default defineEventHandler(async (event) => {
const authRequest = auth.handleRequest(event);
const session = await authRequest.validate();
if (!session) {
throw createError({
message: 'Unauthorized',
statusCode: 401,
});
}
const data = await db.select().from(users).all();
return { data };
});
38 changes: 38 additions & 0 deletions server/database/migrations/0000_violet_bill_hollister.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
CREATE TABLE IF NOT EXISTS `links` (
`id` integer PRIMARY KEY NOT NULL,
`type` text,
`slug` text,
`name` text,
`description` text,
`url` text,
`image` text,
`color` text,
`created_at` text DEFAULT CURRENT_TIMESTAMP,
`update_at` text,
`deleted_at` text
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `user_key` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`hashed_password` text,
`created_at` text DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `user_session` (
`id` text PRIMARY KEY NOT NULL,
`user_id` text NOT NULL,
`active_expires` blob NOT NULL,
`idle_expires` blob NOT NULL,
`created_at` text DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS `users` (
`id` text PRIMARY KEY NOT NULL,
`first_name` text,
`last_name` text,
`email` text,
`created_at` text DEFAULT CURRENT_TIMESTAMP
);
Loading