Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1447882
feat: add NEO Link Shortener application with database migration and …
khangronky Mar 19, 2026
125fe38
Create neo shortener page
tinlam-hws Mar 23, 2026
eec0f42
Merge branch 'feat/neo-shortener' of https://github.com/rmit-nct/hub …
tinlam-hws Mar 23, 2026
26dc18b
style: apply biome formatting
tinlam-hws Mar 23, 2026
280eca6
slug encoding logic
tinlam-hws Mar 24, 2026
dab43c7
Merge branch 'feat/neo-shortener' of https://github.com/rmit-nct/hub …
tinlam-hws Mar 24, 2026
61edb57
style: apply biome formatting
tinlam-hws Mar 24, 2026
601f9aa
Merge pull request #352 from rmit-nct/fix/biome-formatting-feat/neo-s…
tinlam-hws Mar 24, 2026
87e3db8
update function for link shorterner
dat-nix Mar 25, 2026
fa80ce3
test
dat-nix Mar 25, 2026
ceda452
fix redirect logic and enhance UI
dat-nix Mar 26, 2026
ac74ee8
removed unsed shortenerBaseUrl constant
dat-nix Mar 26, 2026
cec6a08
test
dat-nix Mar 26, 2026
94141c1
undo
dat-nix Mar 26, 2026
b0dccd7
improve normalize url function to handle edge case (security)
dat-nix Mar 26, 2026
c57ba32
add dependencies for react
tinlam-hws Mar 27, 2026
7eb88dd
Merge branch 'main' into feat/neo-shortener
khangronky Mar 31, 2026
5494c2e
chore(deps): Regenerare bun.lock file
khangronky Mar 31, 2026
1f63d1d
refactor: update lucide-react dependency to version 0.563.0 and clean…
khangronky Mar 31, 2026
ac05ea1
refactor: remove Mention route and update user feedbacks type definition
khangronky Mar 31, 2026
1533e0a
Merge branch 'main' into feat/neo-shortener
khangronky Apr 2, 2026
7732318
Merge branch 'main' into feat/neo-shortener
khangronky Apr 6, 2026
8f5d136
redirect to login page if unauthenticated user press create shortlink
tinlam-hws Apr 7, 2026
23557d6
Merge branch 'feat/neo-shortener' of https://github.com/rmit-nct/hub …
tinlam-hws Apr 7, 2026
2bb0efa
dark mode and light mode UI adaptation
tinlam-hws Apr 7, 2026
76b79ed
style: apply biome formatting
tinlam-hws Apr 7, 2026
aec892c
Merge pull request #368 from rmit-nct/fix/biome-formatting-feat/neo-s…
tinlam-hws Apr 7, 2026
6402665
add new table to limit user link creation
dat-nix Apr 7, 2026
1b08959
fix schema db to limit user, enhance UI, add CRUD feature
dat-nix Apr 7, 2026
cc0d796
fix format
dat-nix Apr 7, 2026
98a28af
format again
dat-nix Apr 7, 2026
77aaf9e
fix format
dat-nix Apr 7, 2026
6bc7825
Merge branch 'main' into feat/neo-shortener
khangronky Apr 8, 2026
3a052c5
add password form, debounced for slug and custom 404 page
dat-nix Apr 23, 2026
18f60d9
fix format
dat-nix Apr 23, 2026
c45bef1
update password form UI
dat-nix Apr 23, 2026
c54e7f7
Merge branch 'main' into feat/neo-shortener
khangronky Apr 25, 2026
1e05652
fix: remove duplicate type export for CarouselApi
khangronky Apr 25, 2026
380ccd0
Revert "fix format"
khangronky Apr 25, 2026
33b404d
chore: remove unused configuration and empty file
khangronky Apr 25, 2026
e6252a8
Refactor UI components for improved structure and consistency
khangronky Apr 25, 2026
d398727
refactor: optimize imports and improve component structure
khangronky Apr 25, 2026
773323f
refactor: reorganize imports and improve component structure across m…
khangronky Apr 25, 2026
1f8979b
refactor: update import statement for ReactNode type in gradient-head…
khangronky Apr 25, 2026
f9d81ea
refactor: update imports to use type-only imports for better clarity
khangronky Apr 25, 2026
ca1e486
refactor: update supabase version and clean up dependencies in bun.lock
khangronky Apr 25, 2026
1fa48a6
Pop up message for unauthorized user
tinlam-hws Apr 30, 2026
55cffff
Fixing pop up dialog. Reapply the function to navi-bar (utilities)
tinlam-hws Apr 30, 2026
b2280e5
style: apply biome formatting
tinlam-hws Apr 30, 2026
e2ef4c1
Merge pull request #386 from rmit-nct/fix/biome-formatting-feat/neo-s…
tinlam-hws Apr 30, 2026
0280413
Merge branch 'main' into feat/neo-shortener
khangronky May 13, 2026
1573546
refactor: update toast import and simplify login notification
khangronky May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 180 additions & 0 deletions apps/db/supabase/migrations/20260320000000_add_link_shortener.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
create table "public"."shortened_links" (
"id" uuid not null default gen_random_uuid(),
"link" text not null,
"slug" text not null,
"domain" text not null,
"password_hash" text,
"password_hint" text,
"creator_id" uuid not null,
"created_at" timestamp with time zone not null default now()
);

alter table "public"."shortened_links" enable row level security;

create or replace function extract_domain(url text)
returns text as $$
begin
return case
when url ~ '^https?://' then
regexp_replace(regexp_replace(url, '^https?://', ''), '/.*$', '')
when url ~ '^//' then
regexp_replace(regexp_replace(url, '^//', ''), '/.*$', '')
else
regexp_replace(url, '/.*$', '')
end;
end;
$$ language plpgsql immutable;

create unique index "shortened_links_pkey"
on "public"."shortened_links" using btree (id);

create unique index "shortened_links_slug_key"
on "public"."shortened_links" using btree (slug);

create index "idx_shortened_links_creator_id"
on "public"."shortened_links" using btree (creator_id)
where creator_id is not null;

alter table "public"."shortened_links"
add constraint "shortened_links_pkey"
primary key using index "shortened_links_pkey";

alter table "public"."shortened_links"
add constraint "shortened_links_slug_key"
unique using index "shortened_links_slug_key";

alter table "public"."shortened_links"
add constraint "shortened_links_creator_id_fkey"
foreign key (creator_id)
references "public"."users"(id)
on update cascade
on delete set default;

alter table "public"."shortened_links"
add constraint "shortened_links_password_hint_length"
check (password_hint is null or char_length(password_hint) <= 200);

create or replace function set_shortened_links_domain()
returns trigger
language plpgsql
as $$
begin
new.domain := regexp_replace(
regexp_replace(new.link, '^https?://|^//', ''),
'/.*$',
''
);
return new;
end;
$$;

create trigger "trg_set_shortened_links_domain"
before insert or update of link
on "public"."shortened_links"
for each row
execute function set_shortened_links_domain();

create or replace function "public"."enforce_shortened_links_limit"()
returns trigger
language plpgsql
security definer
set search_path = public, pg_temp
as $$
declare
current_link_count integer;
begin
if new.creator_id is null then
raise exception 'creator_id is required';
end if;

-- prevent race condition
perform pg_advisory_xact_lock(hashtext(new.creator_id::text));

select count(*)::integer
into current_link_count
from "public"."shortened_links"
where "creator_id" = new.creator_id;

if current_link_count >= 30 then
raise exception 'You have reached the 30-link limit for your account'
using errcode = 'P0001';
end if;

return new;
end;
$$;

create trigger "trg_enforce_shortened_links_limit"
before insert
on "public"."shortened_links"
for each row
execute function "public"."enforce_shortened_links_limit"();

grant delete on table "public"."shortened_links" to "anon";
grant insert on table "public"."shortened_links" to "anon";
grant references on table "public"."shortened_links" to "anon";
grant select on table "public"."shortened_links" to "anon";
grant trigger on table "public"."shortened_links" to "anon";
grant truncate on table "public"."shortened_links" to "anon";
grant update on table "public"."shortened_links" to "anon";

grant delete on table "public"."shortened_links" to "authenticated";
grant insert on table "public"."shortened_links" to "authenticated";
grant references on table "public"."shortened_links" to "authenticated";
grant select on table "public"."shortened_links" to "authenticated";
grant trigger on table "public"."shortened_links" to "authenticated";
grant truncate on table "public"."shortened_links" to "authenticated";
grant update on table "public"."shortened_links" to "authenticated";

grant delete on table "public"."shortened_links" to "service_role";
grant insert on table "public"."shortened_links" to "service_role";
grant references on table "public"."shortened_links" to "service_role";
grant select on table "public"."shortened_links" to "service_role";
grant trigger on table "public"."shortened_links" to "service_role";
grant truncate on table "public"."shortened_links" to "service_role";
grant update on table "public"."shortened_links" to "service_role";

create policy "Allow users to select"
on "public"."shortened_links"
as permissive
for select
to authenticated
using (
creator_id = auth.uid()
);

create policy "Allow users to insert"
on "public"."shortened_links"
as permissive
for insert
to authenticated
with check (
creator_id = auth.uid()
);

create policy "Allow users to update"
on "public"."shortened_links"
as permissive
for update
to authenticated
using (
creator_id = auth.uid()
)
with check (
creator_id = auth.uid()
);

create policy "Allow users to delete"
on "public"."shortened_links"
as permissive
for delete
to authenticated
using (
creator_id = auth.uid()
);

comment on column "public"."shortened_links"."password_hash" is
'bcrypt-hashed password for link protection. NULL means no password protection.';

comment on column "public"."shortened_links"."password_hint" is
'Optional plaintext hint to help users remember the password. Max 200 characters.';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
revoke all privileges on table "public"."shortened_links" from "anon";

revoke references, trigger, truncate on table "public"."shortened_links"
from "authenticated";
5 changes: 5 additions & 0 deletions apps/shortener/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Required environment variables
# for both development and production
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
SUPABASE_SERVICE_KEY=YOUR_SUPABASE_SERVICE_KEY
3 changes: 3 additions & 0 deletions apps/shortener/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Sentry Auth Token
.sentryclirc
certificates
34 changes: 34 additions & 0 deletions apps/shortener/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).

## Getting Started

First, run the development server:

```bash
npm run dev
# or
yarn dev
# or
bun dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
10 changes: 10 additions & 0 deletions apps/shortener/biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"root": false,
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
"extends": "//",
"linter": {
"domains": {
"next": "recommended"
}
}
}
86 changes: 86 additions & 0 deletions apps/shortener/next.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import type { NextConfig } from 'next';

const isDev = process.env.NODE_ENV === 'development';

const nextConfig: NextConfig = {
reactStrictMode: true,
poweredByHeader: false,
transpilePackages: ['@ncthub/ui'],
images: {
remotePatterns: [
{
protocol: 'http',
hostname: 'localhost',
},
{
protocol: 'http',
hostname: '127.0.0.1',
},
{
protocol: 'https',
hostname: '**.supabase.co',
},
{
protocol: 'https',
hostname: 'avatars.githubusercontent.com',
},
],
},

async headers() {
return [
{
// Setting CORS headers for all routes
source: '/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
// Use wildcard in development, which is more permissive
{
key: 'Access-Control-Allow-Origin',
value: isDev ? '*' : 'https://nct.gg',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET,DELETE,PATCH,POST,PUT,OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value:
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, rsc, RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url',
},
{
key: 'Access-Control-Max-Age',
value: '86400',
},
],
},
{
// Setting CORS headers for API routes
source: '/api/:path*',
headers: [
{ key: 'Access-Control-Allow-Credentials', value: 'true' },
// Use wildcard in development, which is more permissive
{
key: 'Access-Control-Allow-Origin',
value: isDev ? '*' : 'https://nct.gg',
},
{
key: 'Access-Control-Allow-Methods',
value: 'GET,DELETE,PATCH,POST,PUT,OPTIONS',
},
{
key: 'Access-Control-Allow-Headers',
value:
'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, rsc, RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url',
},
{
key: 'Access-Control-Max-Age',
value: '86400',
},
],
},
];
},
};

export default nextConfig;
53 changes: 53 additions & 0 deletions apps/shortener/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@ncthub/shortener",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3002 --turbopack",
"devx": "bun sb:stop && bun sb:start && bun dev",
"devrs": "bun sb:stop && bun sb:start && bun sb:reset && bun dev",
"build": "next build --turbopack",
"start": "next start",
"preview": "next build --turbopack && next start -p 3002 --turbopack",
"type-check": "tsc --build",
"stop": "cd ../db && bun sb:stop",
"sb:status": "cd ../db && bun sb:status",
"sb:start": "cd ../db && bun sb:start",
"sb:stop": "cd ../db && bun sb:stop",
"sb:sync": "cd ../db && bun sb:sync",
"sb:reset": "cd ../db && bun sb:reset",
"sb:diff": "cd ../db && bun sb:diff",
"sb:new": "cd ../db && bun sb:new",
"sb:up": "cd ../db && bun sb:up",
"sb:typegen": "cd ../db && bun sb:typegen",
"ui:add": "bunx shadcn-ui@latest add",
"ui:diff": "bunx shadcn-ui@latest diff"
},
"dependencies": {
"@ncthub/ai": "workspace:*",
"@ncthub/supabase": "workspace:*",
"@ncthub/types": "workspace:*",
"@ncthub/ui": "workspace:*",
"@ncthub/utils": "workspace:*",
"@vercel/analytics": "^2.0.1",
"bcrypt": "^6.0.0",
"dayjs": "^1.11.20",
"nanoid": "^5.1.7",
"next": "^16.2.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"zod": "^4.3.6"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.2",
"@ncthub/typescript-config": "workspace:*",
"@types/bcrypt": "^6.0.0",
"@types/node": "^25.5.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"postcss": "^8.5.8",
"typescript": "^5.9.3"
},
"packageManager": "bun@1.3.11"
}
1 change: 1 addition & 0 deletions apps/shortener/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@ncthub/ui/postcss.config';
9 changes: 9 additions & 0 deletions apps/shortener/src/app/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import ServerPage from './server-page';

interface RedirectPageProps {
params: Promise<{ slug: string }>;
}

export default async function RedirectPage({ params }: RedirectPageProps) {
return <ServerPage params={params} />;
}
Loading
Loading