-
-
Notifications
You must be signed in to change notification settings - Fork 263
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
909 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
68 changes: 68 additions & 0 deletions
68
packages/backend/src/prisma/migrations/20240417232629_add_url_shortener/migration.sql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
/* | ||
Warnings: | ||
- Added the required column `generatedLinksLength` to the `settings` table without a default value. This is not possible if the table is not empty. | ||
*/ | ||
-- CreateTable | ||
CREATE TABLE "shortenedLinks" ( | ||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||
"uuid" TEXT NOT NULL, | ||
"userId" INTEGER, | ||
"identifier" TEXT NOT NULL, | ||
"destination" TEXT NOT NULL, | ||
"views" INTEGER NOT NULL DEFAULT 0, | ||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP | ||
); | ||
|
||
-- RedefineTables | ||
PRAGMA foreign_keys=OFF; | ||
CREATE TABLE "new_settings" ( | ||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, | ||
"rateLimitWindow" INTEGER NOT NULL, | ||
"rateLimitMax" INTEGER NOT NULL, | ||
"secret" TEXT NOT NULL, | ||
"serviceName" TEXT NOT NULL, | ||
"chunkSize" TEXT NOT NULL, | ||
"chunkedUploadsTimeout" INTEGER NOT NULL, | ||
"maxSize" TEXT NOT NULL, | ||
"generateZips" BOOLEAN NOT NULL, | ||
"generateOriginalFileNameWithIdentifier" BOOLEAN NOT NULL DEFAULT false, | ||
"generatedFilenameLength" INTEGER NOT NULL, | ||
"generatedAlbumLength" INTEGER NOT NULL, | ||
"generatedLinksLength" INTEGER NOT NULL, | ||
"blockedExtensions" TEXT NOT NULL, | ||
"blockNoExtension" BOOLEAN NOT NULL, | ||
"publicMode" BOOLEAN NOT NULL, | ||
"userAccounts" BOOLEAN NOT NULL, | ||
"disableStatisticsCron" BOOLEAN NOT NULL, | ||
"disableUpdateCheck" BOOLEAN NOT NULL DEFAULT false, | ||
"backgroundImageURL" TEXT NOT NULL, | ||
"logoURL" TEXT NOT NULL, | ||
"metaDescription" TEXT NOT NULL, | ||
"metaKeywords" TEXT NOT NULL, | ||
"metaTwitterHandle" TEXT NOT NULL, | ||
"metaDomain" TEXT NOT NULL DEFAULT '', | ||
"serveUploadsFrom" TEXT NOT NULL DEFAULT '', | ||
"enableMixedCaseFilenames" BOOLEAN NOT NULL DEFAULT true, | ||
"usersStorageQuota" TEXT NOT NULL DEFAULT '0', | ||
"useNetworkStorage" BOOLEAN NOT NULL DEFAULT false, | ||
"useMinimalHomepage" BOOLEAN NOT NULL DEFAULT false, | ||
"S3Region" TEXT NOT NULL DEFAULT '', | ||
"S3Bucket" TEXT NOT NULL DEFAULT '', | ||
"S3AccessKey" TEXT NOT NULL DEFAULT '', | ||
"S3SecretKey" TEXT NOT NULL DEFAULT '', | ||
"S3Endpoint" TEXT NOT NULL DEFAULT '', | ||
"S3PublicUrl" TEXT NOT NULL DEFAULT '' | ||
); | ||
INSERT INTO "new_settings" ("S3AccessKey", "S3Bucket", "S3Endpoint", "S3PublicUrl", "S3Region", "S3SecretKey", "backgroundImageURL", "blockNoExtension", "blockedExtensions", "chunkSize", "chunkedUploadsTimeout", "disableStatisticsCron", "disableUpdateCheck", "enableMixedCaseFilenames", "generateOriginalFileNameWithIdentifier", "generateZips", "generatedAlbumLength", "generatedFilenameLength", "id", "logoURL", "maxSize", "metaDescription", "metaDomain", "metaKeywords", "metaTwitterHandle", "publicMode", "rateLimitMax", "rateLimitWindow", "secret", "serveUploadsFrom", "serviceName", "useMinimalHomepage", "useNetworkStorage", "userAccounts", "usersStorageQuota") SELECT "S3AccessKey", "S3Bucket", "S3Endpoint", "S3PublicUrl", "S3Region", "S3SecretKey", "backgroundImageURL", "blockNoExtension", "blockedExtensions", "chunkSize", "chunkedUploadsTimeout", "disableStatisticsCron", "disableUpdateCheck", "enableMixedCaseFilenames", "generateOriginalFileNameWithIdentifier", "generateZips", "generatedAlbumLength", "generatedFilenameLength", "id", "logoURL", "maxSize", "metaDescription", "metaDomain", "metaKeywords", "metaTwitterHandle", "publicMode", "rateLimitMax", "rateLimitWindow", "secret", "serveUploadsFrom", "serviceName", "useMinimalHomepage", "useNetworkStorage", "userAccounts", "usersStorageQuota" FROM "settings"; | ||
DROP TABLE "settings"; | ||
ALTER TABLE "new_settings" RENAME TO "settings"; | ||
PRAGMA foreign_key_check; | ||
PRAGMA foreign_keys=ON; | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "shortenedLinks_uuid_key" ON "shortenedLinks"("uuid"); | ||
|
||
-- CreateIndex | ||
CREATE UNIQUE INDEX "shortenedLinks_identifier_key" ON "shortenedLinks"("identifier"); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import slugify from '@sindresorhus/slugify'; | ||
import type { FastifyReply } from 'fastify'; | ||
import moment from 'moment'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import { z } from 'zod'; | ||
import prisma from '@/structures/database.js'; | ||
import type { RequestWithUser } from '@/structures/interfaces.js'; | ||
import { http4xxErrorSchema } from '@/structures/schemas/HTTP4xxError.js'; | ||
import { http5xxErrorSchema } from '@/structures/schemas/HTTP5xxError.js'; | ||
import { responseMessageSchema } from '@/structures/schemas/ResponseMessage.js'; | ||
import { constructShortLink, getUniqueShortLinkIdentifier } from '@/utils/Link.js'; | ||
|
||
export const schema = { | ||
summary: 'Create link', | ||
description: 'Create a new shortened link', | ||
tags: ['Links'], | ||
body: z.object({ | ||
url: z.string().optional().describe('The destination url.'), | ||
vanity: z.string().optional().describe('The vanity url to use.') | ||
}), | ||
response: { | ||
200: z.object({ | ||
message: responseMessageSchema, | ||
link: z.object({ | ||
uuid: z.string().describe('The uuid of the link.'), | ||
identifier: z.string().describe('The identifier of the link.'), | ||
link: z.string().describe('The shortened link.') | ||
}) | ||
}), | ||
'4xx': http4xxErrorSchema, | ||
'5xx': http5xxErrorSchema | ||
} | ||
}; | ||
|
||
export const options = { | ||
url: '/link/create', | ||
method: 'post', | ||
middlewares: [ | ||
{ | ||
name: 'apiKey' | ||
}, | ||
{ | ||
name: 'auth' | ||
} | ||
] | ||
}; | ||
|
||
export const run = async (req: RequestWithUser, res: FastifyReply) => { | ||
const { url, vanity } = req.body as { url: string; vanity?: string | undefined }; | ||
|
||
console.log(url, vanity); | ||
const now = moment.utc().toDate(); | ||
|
||
let linkToUse; | ||
|
||
if (vanity) { | ||
const sluggifiedVanity = slugify(vanity, { separator: '_', decamelize: false, lowercase: false }); | ||
const existingLink = await prisma.shortenedLinks.findFirst({ | ||
where: { | ||
identifier: sluggifiedVanity | ||
} | ||
}); | ||
|
||
// If the vanity url doesn't exist, we'll use it | ||
if (!existingLink) { | ||
linkToUse = sluggifiedVanity; | ||
} | ||
} | ||
|
||
// If linkToUse is still undefined, we'll generate a unique identifier | ||
if (!linkToUse) { | ||
const uniqueIdentifier = await getUniqueShortLinkIdentifier(); | ||
if (!uniqueIdentifier) { | ||
void res.internalServerError('Couldnt allocate identifier for link'); | ||
return; | ||
} | ||
|
||
linkToUse = uniqueIdentifier; | ||
} | ||
|
||
const link = await prisma.shortenedLinks.create({ | ||
data: { | ||
destination: url, | ||
identifier: linkToUse, | ||
userId: req.user.id, | ||
uuid: uuidv4(), | ||
createdAt: now | ||
} | ||
}); | ||
|
||
return res.send({ | ||
message: 'Successfully created link', | ||
link: { | ||
uuid: link.uuid, | ||
identifier: link.identifier, | ||
link: constructShortLink(req, link.identifier) | ||
} | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import type { FastifyReply } from 'fastify'; | ||
import { z } from 'zod'; | ||
import prisma from '@/structures/database.js'; | ||
import type { RequestWithUser } from '@/structures/interfaces.js'; | ||
import { http4xxErrorSchema } from '@/structures/schemas/HTTP4xxError.js'; | ||
import { http5xxErrorSchema } from '@/structures/schemas/HTTP5xxError.js'; | ||
import { responseMessageSchema } from '@/structures/schemas/ResponseMessage.js'; | ||
|
||
export const schema = { | ||
summary: 'Delete link', | ||
description: 'Deletes a link', | ||
tags: ['Links'], | ||
params: z | ||
.object({ | ||
identifier: z.string().describe('The identifier of the link.') | ||
}) | ||
.required(), | ||
response: { | ||
200: z.object({ | ||
message: responseMessageSchema | ||
}), | ||
'4xx': http4xxErrorSchema, | ||
'5xx': http5xxErrorSchema | ||
} | ||
}; | ||
|
||
export const options = { | ||
url: '/link/:identifier', | ||
method: 'delete', | ||
middlewares: ['apiKey', 'auth'] | ||
}; | ||
|
||
export const run = async (req: RequestWithUser, res: FastifyReply) => { | ||
const { identifier } = req.params as { identifier: string }; | ||
|
||
const link = await prisma.shortenedLinks.findFirst({ | ||
where: { | ||
userId: req.user.id, | ||
identifier | ||
}, | ||
select: { | ||
identifier: true | ||
} | ||
}); | ||
|
||
if (!link) { | ||
void res.badRequest("The shortened link doesn't exist or doesn't belong to the user"); | ||
return; | ||
} | ||
|
||
await prisma.shortenedLinks.delete({ | ||
where: { | ||
identifier | ||
} | ||
}); | ||
|
||
return res.send({ | ||
message: 'Successfully deleted shortened link' | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import type { FastifyReply } from 'fastify'; | ||
import prisma from '@/structures/database.js'; | ||
import type { RequestWithUser } from '@/structures/interfaces.js'; | ||
import { http4xxErrorSchema } from '@/structures/schemas/HTTP4xxError.js'; | ||
import { http5xxErrorSchema } from '@/structures/schemas/HTTP5xxError.js'; | ||
|
||
export const schema = { | ||
summary: 'Get link', | ||
description: 'Get a link and redirect the response', | ||
tags: ['Links'], | ||
response: { | ||
'4xx': http4xxErrorSchema, | ||
'5xx': http5xxErrorSchema | ||
} | ||
}; | ||
|
||
export const options = { | ||
url: '/link/:identifier', | ||
method: 'get' | ||
}; | ||
|
||
export const run = async (req: RequestWithUser, res: FastifyReply) => { | ||
const { identifier } = req.params as { identifier: string }; | ||
|
||
const link = await prisma.shortenedLinks.findFirst({ | ||
where: { | ||
identifier | ||
}, | ||
select: { | ||
destination: true | ||
} | ||
}); | ||
|
||
if (!link) { | ||
void res.notFound('The link could not be found'); | ||
return; | ||
} | ||
|
||
await prisma.shortenedLinks.update({ | ||
where: { | ||
identifier | ||
}, | ||
data: { | ||
views: { | ||
increment: 1 | ||
} | ||
} | ||
}); | ||
|
||
if (!link.destination.startsWith('http')) { | ||
link.destination = `http://${link.destination}`; | ||
} | ||
|
||
return res.redirect(link.destination); | ||
}; |
Oops, something went wrong.