forked from jordanlambrecht/tracker-tracker
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathroute.ts
More file actions
151 lines (131 loc) · 4.75 KB
/
Copy pathroute.ts
File metadata and controls
151 lines (131 loc) · 4.75 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// src/app/api/trackers/route.ts
//
// Functions: GET, POST
import { NextResponse } from "next/server"
import { CHART_THEME } from "@/components/charts/lib/theme"
import { DEFAULT_API_PATHS, VALID_PLATFORM_TYPES } from "@/lib/adapters"
import {
authenticate,
decodeKey,
parseJsonBody,
validateHexColor,
validateHttpUrl,
validateJoinedAt,
validateMaxLength,
} from "@/lib/api-helpers"
import { encrypt } from "@/lib/crypto"
import { db } from "@/lib/db"
import { trackers } from "@/lib/db/schema"
import { errMsg } from "@/lib/error-utils"
import {
AVISTAZ_TOKEN_MAX,
LONG_STRING_MAX,
TRACKER_NAME_MAX,
TRACKER_TAG_MAX,
TRACKER_TOKEN_MAX,
TRACKER_URL_MAX,
} from "@/lib/limits"
import { log } from "@/lib/logger"
import { getTrackerListForDashboard } from "@/lib/server-data"
export async function GET() {
const auth = await authenticate()
if (auth instanceof NextResponse) return auth
try {
const trackerList = await getTrackerListForDashboard()
return NextResponse.json(trackerList)
} catch (err) {
log.error({ route: "GET /api/trackers", error: errMsg(err) }, "Failed to fetch trackers")
return NextResponse.json({ error: "Failed to load trackers" }, { status: 500 })
}
}
export async function POST(request: Request) {
const auth = await authenticate()
if (auth instanceof NextResponse) return auth
const body = await parseJsonBody(request)
if (body instanceof NextResponse) return body
const { name, baseUrl, apiToken, platformType, color, qbtTag, mouseholeUrl, joinedAt } = body as {
name?: string
baseUrl?: string
apiToken?: string
platformType?: string
color?: string
qbtTag?: string
mouseholeUrl?: string
joinedAt?: string
}
if (
typeof name !== "string" ||
typeof baseUrl !== "string" ||
typeof apiToken !== "string" ||
!name.trim() ||
!baseUrl.trim() ||
!apiToken.trim()
) {
return NextResponse.json(
{ error: "name, baseUrl, and apiToken are required strings" },
{ status: 400 }
)
}
const trimmedName = name.trim()
const trimmedBaseUrl = baseUrl.trim()
const trimmedApiToken = apiToken.trim()
const platform = typeof platformType === "string" ? platformType : "unit3d"
const nameErr = validateMaxLength(trimmedName, TRACKER_NAME_MAX, "Name")
if (nameErr) return nameErr
const urlLenErr = validateMaxLength(trimmedBaseUrl, TRACKER_URL_MAX, "URL")
if (urlLenErr) return urlLenErr
const maxTokenLength = platform === "avistaz" ? AVISTAZ_TOKEN_MAX : TRACKER_TOKEN_MAX
const tokenErr = validateMaxLength(trimmedApiToken, maxTokenLength, "API token")
if (tokenErr) return tokenErr
const urlErr = validateHttpUrl(trimmedBaseUrl)
if (urlErr) return urlErr
if (typeof color === "string") {
const colorErr = validateHexColor(color)
if (colorErr) return colorErr
}
if (typeof qbtTag === "string") {
const qbtTagErr = validateMaxLength(qbtTag, TRACKER_TAG_MAX, "qBittorrent tag")
if (qbtTagErr) return qbtTagErr
}
if (typeof mouseholeUrl === "string" && mouseholeUrl.trim()) {
const mouseholeUrlErr = validateMaxLength(mouseholeUrl.trim(), LONG_STRING_MAX, "Mousehole URL")
if (mouseholeUrlErr) return mouseholeUrlErr
const mouseUrlErr = validateHttpUrl(mouseholeUrl.trim(), "Mousehole URL")
if (mouseUrlErr) return mouseUrlErr
}
if (typeof joinedAt === "string" && joinedAt) {
const joinedAtErr = validateJoinedAt(joinedAt)
if (joinedAtErr) return joinedAtErr
}
if (!VALID_PLATFORM_TYPES.includes(platform as (typeof VALID_PLATFORM_TYPES)[number])) {
return NextResponse.json({ error: "Invalid platform type" }, { status: 400 })
}
try {
const key = decodeKey(auth)
const encryptedApiToken = encrypt(trimmedApiToken, key)
const [tracker] = await db
.insert(trackers)
.values({
name: trimmedName,
baseUrl: trimmedBaseUrl,
apiPath: DEFAULT_API_PATHS[platform] ?? "/api/user",
encryptedApiToken,
platformType: platform,
color: (color as string) || CHART_THEME.accent,
qbtTag: typeof qbtTag === "string" ? qbtTag.trim() : null,
mouseholeUrl:
typeof mouseholeUrl === "string" && mouseholeUrl.trim() ? mouseholeUrl.trim() : null,
joinedAt: typeof joinedAt === "string" && joinedAt ? joinedAt : null,
})
.returning()
// SECURITY: Only return safe fields
log.info(
{ route: "POST /api/trackers", trackerId: tracker.id, trackerName: tracker.name },
`tracker created: ${tracker.name}`
)
return NextResponse.json({ id: tracker.id, name: tracker.name }, { status: 201 })
} catch (err) {
log.error({ route: "POST /api/trackers", error: errMsg(err) }, "Failed to create tracker")
return NextResponse.json({ error: "Failed to create tracker" }, { status: 500 })
}
}