Compare commits
11 Commits
561fb56419
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f34730b609 | |||
| e72f7aa839 | |||
| e858825749 | |||
| 90a81f353d | |||
| 8f4be48fd1 | |||
| ffba95a411 | |||
| 5d5d691bab | |||
| b763eb70db | |||
| 783ee1b334 | |||
| 73c6bdb89b | |||
| 1d5fb09eb0 |
12
Dockerfile
12
Dockerfile
@@ -1,10 +1,9 @@
|
|||||||
FROM node:22-alpine AS base
|
FROM node:22-slim AS base
|
||||||
RUN corepack enable pnpm
|
RUN corepack enable
|
||||||
|
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache python3 make g++
|
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
FROM base AS build
|
FROM base AS build
|
||||||
@@ -16,7 +15,6 @@ RUN pnpm build
|
|||||||
FROM base AS runtime
|
FROM base AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=build /app/.output ./.output
|
COPY --from=build /app/.output ./.output
|
||||||
|
COPY --from=build /app/server/db/migrations ./server/db/migrations
|
||||||
ENV NODE_ENV=production
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["node", "./.output/server/index.mjs"]
|
CMD ["node", ".output/server/index.mjs"]
|
||||||
|
|||||||
39
README.md
39
README.md
@@ -1 +1,38 @@
|
|||||||
# pihka.al
|
<div align="center">
|
||||||
|
<h1>pihka.al</h1>
|
||||||
|
<img src="docs/demo.gif" alt="demo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Personal dashboard for managing my [pihka.al](https://pihka.al) domain. I'm building it for my own use, it's not designed as a generic tool and not meant to be reused as-is.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- Manage short URLs served from [pihka.al](https://pihka.al)
|
||||||
|
|
||||||
|
## Next features
|
||||||
|
|
||||||
|
- **API**: Create an API token that can be used to manage short URLs from anywere
|
||||||
|
- **File management**: Upload and manage files directly in the dasboard
|
||||||
|
- **Text service**: Pastebin-like service, including burner messages
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- [Nuxt 4](https://nuxt.com) + [Nuxt UI](https://ui.nuxt.com)
|
||||||
|
- [SQLite](https://sqlite.org) via [Drizzle ORM](https://orm.drizzle.team)
|
||||||
|
|
||||||
|
## Running locally
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pnpm install
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
# .env.example
|
||||||
|
DATABASE_URL=sqlite.db
|
||||||
|
ADMIN_USERNAME=admin
|
||||||
|
ADMIN_PASSWORD=strong_password
|
||||||
|
REDIRECT_DOMAIN=pihka.al
|
||||||
|
|
||||||
|
NUXT_SESSION_PASSWORD=strong_password
|
||||||
|
```
|
||||||
|
|||||||
10
app/app.config.ts
Normal file
10
app/app.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
colors: {
|
||||||
|
primary: "neutral",
|
||||||
|
neutral: "zinc",
|
||||||
|
success: "emerald",
|
||||||
|
error: "rose",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@nuxt/ui";
|
@import "@nuxt/ui";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ui-radius: 0rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const emit = defineEmits<{ close: [confirmed: boolean] }>();
|
|||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="flex gap-2 justify-end w-full">
|
<div class="flex gap-2 justify-end w-full">
|
||||||
<UButton variant="ghost" color="neutral" @click="emit('close', false)">Cancel</UButton>
|
<UButton variant="ghost" color="neutral" @click="emit('close', false)">Cancel</UButton>
|
||||||
<UButton color="error" @click="emit('close', true)">Delete</UButton>
|
<UButton color="error" icon="i-lucide-trash-2" @click="emit('close', true)">Delete</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</UModal>
|
</UModal>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const onSubmit = async (event: FormSubmitEvent<Schema>) => {
|
|||||||
|
|
||||||
<div class="flex justify-end gap-2 pt-2">
|
<div class="flex justify-end gap-2 pt-2">
|
||||||
<UButton variant="ghost" @click="emit('close', false)">Cancel</UButton>
|
<UButton variant="ghost" @click="emit('close', false)">Cancel</UButton>
|
||||||
<UButton type="submit" :loading="submitting">{{ link ? "Save" : "Create" }}</UButton>
|
<UButton type="submit" variant="subtle" :loading="submitting" :icon="link ? 'i-lucide-save' : 'i-lucide-plus'">{{ link ? "Save" : "Create" }}</UButton>
|
||||||
</div>
|
</div>
|
||||||
</UForm>
|
</UForm>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { TableColumn } from "@nuxt/ui";
|
|||||||
|
|
||||||
const UBadge = resolveComponent("UBadge");
|
const UBadge = resolveComponent("UBadge");
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
data: Link[];
|
data: Link[];
|
||||||
status: "pending" | "idle" | "success" | "error";
|
status: "pending" | "idle" | "success" | "error";
|
||||||
}>();
|
}>();
|
||||||
|
|||||||
20
app/error.vue
Normal file
20
app/error.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { NuxtError } from "#app"
|
||||||
|
|
||||||
|
defineProps<{ error: NuxtError }>();
|
||||||
|
useHead({ title: 'Not found' });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UApp>
|
||||||
|
<div class="min-h-screen flex items-center justify-center">
|
||||||
|
<div class="text-center space-y-2">
|
||||||
|
<p class="text-8xl font-black tracking-tight">404</p>
|
||||||
|
<p class="text-lg font-medium">This link doesn't exist.</p>
|
||||||
|
<p class="text-muted text-sm">
|
||||||
|
Maybe check out <a href="https://pihkaal.me" class="underline underline-offset-2 hover:text-default transition-colors">pihkaal.me</a> instead?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UApp>
|
||||||
|
</template>
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import type { AuthFormField, FormSubmitEvent } from "@nuxt/ui";
|
import type { AuthFormField, FormSubmitEvent } from "@nuxt/ui";
|
||||||
|
|
||||||
|
useHead({ title: 'Sign in' });
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
middleware: () => {
|
middleware: () => {
|
||||||
const { loggedIn } = useUserSession();
|
const { loggedIn } = useUserSession();
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ const category = computed(() => {
|
|||||||
return "all";
|
return "all";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const categoryTitle = computed(() => {
|
||||||
|
if (category.value === "active") return "Active links";
|
||||||
|
if (category.value === "disabled") return "Disabled links";
|
||||||
|
return "All links";
|
||||||
|
});
|
||||||
|
useHead({ title: categoryTitle });
|
||||||
|
|
||||||
const filteredLinks = computed(() => {
|
const filteredLinks = computed(() => {
|
||||||
if (!links.value) return [];
|
if (!links.value) return [];
|
||||||
if (category.value === "active") return links.value.filter((l) => !l.disabled);
|
if (category.value === "active") return links.value.filter((l) => !l.disabled);
|
||||||
@@ -54,9 +61,9 @@ const deleteLink = async (link: Link) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardGroup>
|
<UDashboardGroup>
|
||||||
<UDashboardSidebar :toggle="false">
|
<UDashboardSidebar :toggle="false" :ui="{ header: 'border-b border-default' }">
|
||||||
<template #header>
|
<template #header>
|
||||||
<UDashboardNavbar title="pihka.al" :toggle="false" />
|
<span class="font-semibold text-highlighted text-lg">pihka.al</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<UNavigationMenu
|
<UNavigationMenu
|
||||||
@@ -88,9 +95,7 @@ const deleteLink = async (link: Link) => {
|
|||||||
class="px-2"
|
class="px-2"
|
||||||
/>
|
/>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<UButton variant="ghost" icon="i-lucide-log-out" block @click="signOut">
|
<UButton variant="link" icon="i-lucide-log-out" block @click="signOut">Sign out</UButton>
|
||||||
Sign out
|
|
||||||
</UButton>
|
|
||||||
</template>
|
</template>
|
||||||
</UDashboardSidebar>
|
</UDashboardSidebar>
|
||||||
|
|
||||||
@@ -98,9 +103,7 @@ const deleteLink = async (link: Link) => {
|
|||||||
<template #header>
|
<template #header>
|
||||||
<UDashboardNavbar :title="category === 'all' ? 'All links' : category === 'active' ? 'Active links' : 'Disabled links'">
|
<UDashboardNavbar :title="category === 'all' ? 'All links' : category === 'active' ? 'Active links' : 'Disabled links'">
|
||||||
<template #right>
|
<template #right>
|
||||||
<UButton icon="i-lucide-plus" @click="openModal(null)">
|
<UButton icon="i-lucide-plus" variant="subtle" @click="openModal(null)">New link</UButton>
|
||||||
New link
|
|
||||||
</UButton>
|
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
6
app/plugins/redirect-error.server.ts
Normal file
6
app/plugins/redirect-error.server.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default defineNuxtPlugin(() => {
|
||||||
|
const event = useRequestEvent()
|
||||||
|
if (event?.context.redirectNotFound) {
|
||||||
|
showError({ statusCode: 404, message: 'Link not found' })
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import type { InternalApi } from "nitropack/types";
|
||||||
|
|
||||||
|
export type Link = InternalApi["/api/links"]["get"][number];
|
||||||
|
|
||||||
const apiErrorSchema = z.object({
|
const apiErrorSchema = z.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
|
|||||||
22
docker-compose.dev.yml
Normal file
22
docker-compose.dev.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
container_name: pihka-al-dev
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
target: build
|
||||||
|
command: pnpm dev
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- DATABASE_URL=/data/db.sqlite
|
||||||
|
- ADMIN_USERNAME=admin
|
||||||
|
- ADMIN_PASSWORD=admin
|
||||||
|
- REDIRECT_DOMAIN=localhost
|
||||||
|
- NUXT_SESSION_PASSWORD=dev-session-password-32-chars-min
|
||||||
|
volumes:
|
||||||
|
- .:/app
|
||||||
|
- /app/node_modules
|
||||||
|
- db:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db:
|
||||||
@@ -20,13 +20,13 @@ services:
|
|||||||
# dashboard domain
|
# dashboard domain
|
||||||
- traefik.http.routers.pihka-al-dashboard.rule=Host(`${DASHBOARD_DOMAIN}`)
|
- traefik.http.routers.pihka-al-dashboard.rule=Host(`${DASHBOARD_DOMAIN}`)
|
||||||
- traefik.http.routers.pihka-al-dashboard.tls.certresolver=myresolver
|
- traefik.http.routers.pihka-al-dashboard.tls.certresolver=myresolver
|
||||||
- traefik.http.routers.pihkaal-me.tls=true
|
- traefik.http.routers.pihka-al.tls=true
|
||||||
- traefik.http.routers.pihka-al-dashboard.service=pihka-al
|
- traefik.http.routers.pihka-al-dashboard.service=pihka-al
|
||||||
|
|
||||||
# redirect domain
|
# redirect domain
|
||||||
- traefik.http.routers.pihka-al-redirect.rule=Host(`${REDIRECT_DOMAIN}`)
|
- traefik.http.routers.pihka-al-redirect.rule=Host(`${REDIRECT_DOMAIN}`)
|
||||||
- traefik.http.routers.pihka-al-redirect.tls.certresolver=myresolver
|
- traefik.http.routers.pihka-al-redirect.tls.certresolver=myresolver
|
||||||
- traefik.http.routers.pihkaal-me.tls=true
|
- traefik.http.routers.pihka-al.tls=true
|
||||||
- traefik.http.routers.pihka-al-redirect.service=pihka-al
|
- traefik.http.routers.pihka-al-redirect.service=pihka-al
|
||||||
- traefik.http.routers.pihka-al.middlewares=umami-middleware@file
|
- traefik.http.routers.pihka-al.middlewares=umami-middleware@file
|
||||||
|
|
||||||
|
|||||||
BIN
docs/demo.gif
Normal file
BIN
docs/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 577 KiB |
@@ -10,20 +10,24 @@ export default defineNuxtConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
titleTemplate: '%s · pihka.al',
|
||||||
|
link: [{ rel: 'icon', type: 'image/svg+xml', href: '/favicon.svg' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
devtools: {
|
devtools: {
|
||||||
enabled: true
|
enabled: true
|
||||||
},
|
},
|
||||||
|
|
||||||
css: ['~/assets/css/main.css'],
|
css: ['~/assets/css/main.css'],
|
||||||
|
|
||||||
compatibilityDate: '2025-01-15',
|
nitro: {
|
||||||
|
externals: {
|
||||||
|
external: ['better-sqlite3'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
eslint: {
|
compatibilityDate: '2025-01-15',
|
||||||
config: {
|
|
||||||
stylistic: {
|
|
||||||
commaDangle: 'never',
|
|
||||||
braceStyle: '1tbs'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|||||||
6
pnpm-workspace.yml
Normal file
6
pnpm-workspace.yml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- "@parcel/watcher"
|
||||||
|
- better-sqlite3
|
||||||
|
- esbuild
|
||||||
|
- unrs-resolver
|
||||||
|
- vue-demi
|
||||||
51
public/favicon.svg
Normal file
51
public/favicon.svg
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 18">
|
||||||
|
<style>
|
||||||
|
.bg { fill: #000000; }
|
||||||
|
.fg { fill: #ffffff; }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<rect x="0" y="11" width="5" height="5" class="bg" />
|
||||||
|
<rect x="1" y="10" width="7" height="5" class="bg" />
|
||||||
|
<rect x="6" y="10" width="22" height="6" class="bg" />
|
||||||
|
<rect x="9" y="5" width="18" height="12" class="bg" />
|
||||||
|
<rect x="10" y="16" width="16" height="2" class="bg" />
|
||||||
|
<rect x="7" y="7" width="16" height="4" class="bg" />
|
||||||
|
<rect x="8" y="6" width="2" height="2" class="bg" />
|
||||||
|
<rect x="26" y="9" width="2" height="2" class="bg" />
|
||||||
|
<rect x="11" y="4" width="16" height="2" class="bg" />
|
||||||
|
<rect x="12" y="3" width="15" height="2" class="bg" />
|
||||||
|
<rect x="13" y="2" width="6" height="2" class="bg" />
|
||||||
|
<rect x="14" y="1" width="5" height="2" class="bg" />
|
||||||
|
<rect x="15" y="0" width="3" height="2" class="bg" />
|
||||||
|
<rect x="21" y="2" width="6" height="2" class="bg" />
|
||||||
|
<rect x="22" y="1" width="5" height="2" class="bg" />
|
||||||
|
<rect x="23" y="0" width="3" height="2" class="bg" />
|
||||||
|
|
||||||
|
<rect x="2" y="11" width="5" height="3" class="fg" />
|
||||||
|
<rect x="1" y="12" width="3" height="3" class="fg" />
|
||||||
|
<rect x="10" y="6" width="16" height="10" class="fg" />
|
||||||
|
<rect x="8" y="10" width="19" height="5" class="fg" />
|
||||||
|
<rect x="7" y="12" width="2" height="3" class="fg" />
|
||||||
|
<rect x="6" y="12" width="2" height="2" class="fg" />
|
||||||
|
<rect x="8" y="8" width="18" height="3" class="fg" />
|
||||||
|
<rect x="9" y="7" width="2" height="2" class="fg" />
|
||||||
|
<rect x="12" y="15" width="13" height="2" class="fg" />
|
||||||
|
<rect x="25" y="10" width="2" height="5" class="fg" />
|
||||||
|
<rect x="15" y="11" width="3" height="1" class="bg" />
|
||||||
|
<rect x="22" y="11" width="3" height="1" class="bg" />
|
||||||
|
<rect x="12" y="5" width="14" height="2" class="fg" />
|
||||||
|
<rect x="13" y="4" width="6" height="2" class="fg" />
|
||||||
|
<rect x="14" y="3" width="4" height="2" class="fg" />
|
||||||
|
<rect x="15" y="2" width="3" height="2" class="fg" />
|
||||||
|
<rect x="16" y="1" width="1" height="2" class="fg" />
|
||||||
|
<rect x="21" y="4" width="5" height="2" class="fg" />
|
||||||
|
<rect x="22" y="3" width="4" height="2" class="fg" />
|
||||||
|
<rect x="23" y="2" width="3" height="2" class="fg" />
|
||||||
|
<rect x="24" y="1" width="1" height="2" class="fg" />
|
||||||
|
<rect x="16" y="3" width="1" height="2" class="bg" />
|
||||||
|
<rect x="15" y="4" width="1" height="2" class="bg" />
|
||||||
|
<rect x="14" y="5" width="2" height="1" class="bg" />
|
||||||
|
<rect x="15" y="4" width="2" height="1" class="bg" />
|
||||||
|
<rect x="24" y="3" width="1" height="3" class="bg" />
|
||||||
|
<rect x="23" y="4" width="2" height="1" class="bg" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.6 KiB |
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
1
server/db/migrations/0002_narrow_raider.sql
Normal file
1
server/db/migrations/0002_narrow_raider.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX `links_url_unique`;
|
||||||
79
server/db/migrations/meta/0002_snapshot.json
Normal file
79
server/db/migrations/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "a78f922b-eacd-483e-b14d-5cdfadaac74b",
|
||||||
|
"prevId": "0e8befef-3e4f-43f1-a031-95c2ca514e30",
|
||||||
|
"tables": {
|
||||||
|
"links": {
|
||||||
|
"name": "links",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"name": "url",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"disabled": {
|
||||||
|
"name": "disabled",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"links_name_unique": {
|
||||||
|
"name": "links_name_unique",
|
||||||
|
"columns": [
|
||||||
|
"name"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"links_path_unique": {
|
||||||
|
"name": "links_path_unique",
|
||||||
|
"columns": [
|
||||||
|
"path"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1773792538248,
|
"when": 1773792538248,
|
||||||
"tag": "0001_tense_grey_gargoyle",
|
"tag": "0001_tense_grey_gargoyle",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1774463081828,
|
||||||
|
"tag": "0002_narrow_raider",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -19,12 +19,9 @@ export default defineEventHandler(async (event) => {
|
|||||||
where: eq(tables.links.path, path),
|
where: eq(tables.links.path, path),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!link) {
|
if (!link || link.disabled) {
|
||||||
throw createError({ statusCode: 404, message: "Not found" });
|
event.context.redirectNotFound = true;
|
||||||
}
|
return;
|
||||||
|
|
||||||
if (link.disabled) {
|
|
||||||
throw createError({ statusCode: 410, message: "This link has been disabled" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendRedirect(event, link.url, 302);
|
return sendRedirect(event, link.url, 302);
|
||||||
|
|||||||
6
server/plugins/migrations.ts
Normal file
6
server/plugins/migrations.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
|
||||||
|
import { db } from '../db'
|
||||||
|
|
||||||
|
export default defineNitroPlugin(() => {
|
||||||
|
migrate(db, { migrationsFolder: 'server/db/migrations' })
|
||||||
|
})
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import type { InternalApi } from "nitropack/types";
|
|
||||||
|
|
||||||
export type Link = InternalApi["/api/links"]["get"][number];
|
|
||||||
Reference in New Issue
Block a user