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 }}
+
+
+
+
+
+
+
+
\ 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 }}
+
{{ description }}
+
+
+
{{ link_target_text }}: {{ link }}
+
+
+
+
+
+
+
\ 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;