diff --git a/apps/linkos/bun.lockb b/apps/linkos/bun.lockb index 2d67bc6..55288c5 100755 Binary files a/apps/linkos/bun.lockb and b/apps/linkos/bun.lockb differ diff --git a/apps/linkos/package.json b/apps/linkos/package.json index db51fe1..0dd5342 100644 --- a/apps/linkos/package.json +++ b/apps/linkos/package.json @@ -13,6 +13,7 @@ "@clickhouse/client-web": "^0.2.10", "@types/inquirer": "^9.0.7", "@types/nanoid": "^3.0.0", + "@types/nunjucks": "^3.2.6", "@types/ua-parser-js": "^0.7.39", "chalk": "^5.3.0", "clickhouse": "^2.6.0", @@ -23,6 +24,7 @@ "kafkajs": "^2.2.4", "nanoid": "^5.0.6", "node-appwrite": "^12.0.0", + "nunjucks": "^3.2.4", "pg": "^8.11.3", "redis": "^4.6.13", "ua-parser-js": "^1.0.37" diff --git a/apps/linkos/src/assets/I.html b/apps/linkos/src/assets/I.html new file mode 100644 index 0000000..3a8c1f8 --- /dev/null +++ b/apps/linkos/src/assets/I.html @@ -0,0 +1,82 @@ + + + + + + + {{ title }} + + + + +
+ + Links + +

{{ title }}

+
{{ description }}
+ +
+ {{ goto_text }} +
+
+ + + + \ No newline at end of file diff --git a/apps/linkos/src/assets/password.html b/apps/linkos/src/assets/password.html new file mode 100644 index 0000000..d79ebc9 --- /dev/null +++ b/apps/linkos/src/assets/password.html @@ -0,0 +1,104 @@ + + + + + + + {{ title }} + + + + +
+ + Links + +

{{ title }}

+
+ +
{{ enter_password_text }}
+ + + +
+ +
+
+ + +
+ + + + \ No newline at end of file diff --git a/apps/linkos/src/assets/plus.html b/apps/linkos/src/assets/plus.html new file mode 100644 index 0000000..49bd4fd --- /dev/null +++ b/apps/linkos/src/assets/plus.html @@ -0,0 +1,90 @@ + + + + + + + {{ title }} + + + + +
+ + Links + +

{{ title }}

+
{{ description }}
+ + + + +
+ {{ goto_text }} +
+
+ + + + \ No newline at end of file diff --git a/apps/linkos/src/http/api/LinkAPI.ts b/apps/linkos/src/http/api/LinkAPI.ts index 59f7d9b..d2f9b40 100644 --- a/apps/linkos/src/http/api/LinkAPI.ts +++ b/apps/linkos/src/http/api/LinkAPI.ts @@ -9,7 +9,7 @@ import Log from "@/utils/Log.ts"; import Global from "@/utils/Global.ts"; import Env from "@/utils/Env.ts"; import ClickMessage from "@/models/ClickMessage.ts"; - +import nunjucks from 'nunjucks'; export default class LinkAPI { private static producer: Producer | false; @@ -27,31 +27,38 @@ export default class LinkAPI { return LinkAPI.getRedirect(c, true) } - private static async getRedirect(c: Context, qr: boolean = false) { + public static async getWithPassword(c: Context) { const {link} = c.req.param(); - try { - let shortLink: false | Link; + const body = await c.req.formData() + const password = body.get('password'); - const linkFormRedis = await RedisProvider.getClient().get(link); - if (linkFormRedis !== null) { - shortLink = Global.ParseOrFalse(linkFormRedis); - } else { - shortLink = await Link.getLink(link); + let shortLink = await LinkAPI.getLink(link); - await RedisProvider.getClient().set(link, JSON.stringify(shortLink)); - } + if (shortLink !== false && shortLink.password === password) { + return LinkAPI.redirect(shortLink.dest, c); + } + + return c.redirect(`${Env.MAIN_DOMAIN}/${link}`); + } + + private static async getRedirect(c: Context, qr: boolean = false) { + const {link} = c.req.param(); + + try { + let shortLink = await LinkAPI.getLink(link); if (shortLink) { await LinkAPI.missions(shortLink, qr, c); - return LinkAPI.redirect(this.appendQuery(shortLink.dest, c), c); + return LinkAPI.getLinkAction(shortLink, c, link); } } catch (e: any) { Log.debug(e); } - return c.html(await new Response(Bun.file(LinkAPI.path + "404.html")).text(), 404); + + return LinkAPI.its404(c); } public static async getPublic(c: Context) { @@ -82,6 +89,7 @@ export default class LinkAPI { const start = +new Date(); if (LinkAPI.producer) { + // TODO: check for informal, password, etc. await LinkAPI.producer.send({ topic : Analytics.TOPIC_CLICKHOUSE, messages: [{value: (new AnalyticsMessage(link.id, qr, c.req.header())).toString()}] @@ -114,4 +122,68 @@ export default class LinkAPI { } + private static async getLinkAction(link: Link, c: Context, linkId: string) { + if (link.expiring_link && link.expiration_date !== undefined) { + if (+new Date(link.expiration_date) < +new Date()) { + return LinkAPI.its404(c); + } + } + + if (link.password_protected) { + return c.html(LinkAPI.render('password', link)); + } else if (link.plus_enabled && linkId.endsWith('+')) { + return c.html(LinkAPI.render('plus', link)); + } else if (link.informal_redirection) { + return c.html(LinkAPI.render('i', link)); + } + + return LinkAPI.redirect(this.appendQuery(link.dest, c), c) + } + + private static async render(page: string, link: Link) { + const text = await (new Response(Bun.file(`${LinkAPI.path}${page}.html`))).text(); + + return nunjucks.renderString(text, LinkAPI.linksContext(link)); + + } + + + private static linksContext(link: Link) { + return { + // TODO: Make language dynamic + lang : 'en', + dir : 'lrt', + goto_text : 'Go to', + link_target_text : 'Link destination', + password_placeholder_text: 'e.g. Aa123456', + enter_password_text : 'Password is required to access this short link destination', + form_target : `${Env.MAIN_DOMAIN}/password/${link.id}`, + + + title : link.title, + description: link.description, + link : link.dest, + }; + } + + private static async getLink(linkID: string) { + linkID = linkID.replace(/\+$/, ''); + + let shortLink: false | Link = false; + + const linkFormRedis = await RedisProvider.getClient().get(linkID); + if (linkFormRedis !== null) { + shortLink = Global.ParseOrFalse(linkFormRedis); + } else { + shortLink = await Link.getLink(linkID); + + await RedisProvider.getClient().set(linkID, JSON.stringify(shortLink)); + } + + return shortLink; + } + + private static async its404(c: Context) { + return c.html(await new Response(Bun.file(LinkAPI.path + "404.html")).text(), 404); + } } \ No newline at end of file diff --git a/apps/linkos/src/services/GetLink.ts b/apps/linkos/src/services/GetLink.ts index c3aa4bf..57f877e 100644 --- a/apps/linkos/src/services/GetLink.ts +++ b/apps/linkos/src/services/GetLink.ts @@ -10,6 +10,7 @@ export default class GetLink { app.get('/', LinkAPI.getPublic); app.get('/:link', LinkAPI.get); app.get('/qr/:link', LinkAPI.getQr); + app.post('/password/:link', LinkAPI.getWithPassword); Log.info('Starting serving Linkos getlink endpoint') return app;