├── .env.example
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── README.md
├── drizzle.config.ts
├── drizzle
├── 0000_burly_colonel_america.sql
├── 0001_little_mercury.sql
├── 0002_smiling_sphinx.sql
├── 0003_high_quasar.sql
├── 0004_clammy_prowler.sql
├── 0005_puzzling_vampiro.sql
├── 0006_stale_purple_man.sql
└── meta
│ ├── 0000_snapshot.json
│ ├── 0001_snapshot.json
│ ├── 0002_snapshot.json
│ ├── 0003_snapshot.json
│ ├── 0004_snapshot.json
│ ├── 0005_snapshot.json
│ ├── 0006_snapshot.json
│ └── _journal.json
├── eslint.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── src
├── app.css
├── app.d.ts
├── app.html
├── global.d.ts
├── hooks.server.ts
├── lib
│ ├── components
│ │ ├── buttons
│ │ │ └── google-login-button.svelte
│ │ ├── cards
│ │ │ ├── image-picker-card.svelte
│ │ │ └── result-card.svelte
│ │ ├── compare-image.svelte
│ │ ├── file-dropzone.svelte
│ │ ├── footer.svelte
│ │ ├── header.svelte
│ │ ├── heading.svelte
│ │ ├── privacy-policy.svx
│ │ └── sidebar.svelte
│ ├── constants
│ │ └── price.ts
│ ├── downloads.ts
│ ├── icons
│ │ ├── bg-replace-icon.svelte
│ │ ├── biji-icon.svelte
│ │ ├── bill-icon.svelte
│ │ ├── buy-icon.svelte
│ │ ├── circle-icon.svelte
│ │ ├── close-icon.svelte
│ │ ├── download-icon.svelte
│ │ ├── download-multiple-icon.svelte
│ │ ├── facebook-icon.svelte
│ │ ├── file-upload-icon.svelte
│ │ ├── github-icon.svelte
│ │ ├── google-icon.svelte
│ │ ├── hamburger-icon.svelte
│ │ ├── home-icon.svelte
│ │ ├── instagram-icon.svelte
│ │ ├── invoice-icon.svelte
│ │ ├── linkedin-icon.svelte
│ │ ├── loading-icon.svelte
│ │ ├── logout-icon.svelte
│ │ ├── money-icon.svelte
│ │ ├── reset-icon.svelte
│ │ ├── trash-icon.svelte
│ │ ├── twitter-icon.svelte
│ │ ├── view-icon.svelte
│ │ └── youtube-icon.svelte
│ ├── image.ts
│ ├── rate-limiter.ts
│ ├── remove-bg.ts
│ ├── schema
│ │ ├── image-schema.ts
│ │ └── payment-schema.ts
│ ├── server
│ │ ├── auth.ts
│ │ ├── db
│ │ │ ├── index.ts
│ │ │ └── schema.ts
│ │ ├── oauth.ts
│ │ ├── payment.ts
│ │ └── user.ts
│ ├── stores
│ │ └── credit.svelte.ts
│ ├── types
│ │ └── tripay.ts
│ └── utils.ts
└── routes
│ ├── (app)
│ ├── +layout.server.ts
│ ├── +layout.svelte
│ ├── app
│ │ ├── +page.server.ts
│ │ └── +page.svelte
│ ├── invoices
│ │ ├── +page.server.ts
│ │ └── +page.svelte
│ ├── pricing
│ │ └── +page.svelte
│ ├── privacy-policy
│ │ └── +page.svelte
│ └── topup
│ │ ├── +page.server.ts
│ │ └── +page.svelte
│ ├── (landing-page)
│ ├── +layout.svelte
│ ├── +page.server.ts
│ └── +page.svelte
│ ├── +error.svelte
│ ├── +layout.server.ts
│ ├── +layout.svelte
│ ├── api
│ ├── payment
│ │ └── callback
│ │ │ └── +server.ts
│ └── remove-biji
│ │ └── +server.ts
│ └── auth
│ ├── login
│ └── google
│ │ ├── +server.ts
│ │ └── callback
│ │ └── +server.ts
│ └── logout
│ └── +server.ts
├── static
├── after.webp
├── before.webp
└── favicon.ico
├── svelte.config.js
├── tailwind.config.ts
├── tsconfig.json
└── vite.config.ts
/.env.example:
--------------------------------------------------------------------------------
1 | # Replace with your DB credentials!
2 | DATABASE_URL="postgres://user:password@host:port/db-name"
3 |
4 | PUBLIC_PLAUSIBLE_DATA_DOMAIN=your-domain.com
5 |
6 | BASE_URL=http://your-domain.com
7 | ORIGIN=https://your-domain.com
8 | BODY_SIZE_LIMIT=500M
9 |
10 | GOOGLE_CLIENT_ID=
11 | GOOGLE_CLIENT_SECRET=
12 |
13 | TRIPAY_API_KEY=
14 | TRIPAY_PRIVATE_KEY=
15 | TRIPAY_MERCHANT_CODE=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Output
4 | .output
5 | .vercel
6 | /.svelte-kit
7 | /build
8 |
9 | # OS
10 | .DS_Store
11 | Thumbs.db
12 |
13 | # Env
14 | .env
15 | .env.*
16 | !.env.example
17 | !.env.test
18 |
19 | # Vite
20 | vite.config.js.timestamp-*
21 | vite.config.ts.timestamp-*
22 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 | @jsr:registry=https://npm.jsr.io
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Package Managers
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7 | "overrides": [
8 | {
9 | "files": "*.svelte",
10 | "options": {
11 | "parser": "svelte"
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sv
2 |
3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
4 |
5 | ## Creating a project
6 |
7 | If you're seeing this, you've probably already done this step. Congrats!
8 |
9 | ```bash
10 | # create a new project in the current directory
11 | npx sv create
12 |
13 | # create a new project in my-app
14 | npx sv create my-app
15 | ```
16 |
17 | ## Developing
18 |
19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
20 |
21 | ```bash
22 | npm run dev
23 |
24 | # or start the server and open the app in a new browser tab
25 | npm run dev -- --open
26 | ```
27 |
28 | ## Building
29 |
30 | To create a production version of your app:
31 |
32 | ```bash
33 | npm run build
34 | ```
35 |
36 | You can preview the production build with `npm run preview`.
37 |
38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
39 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'drizzle-kit';
2 | if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
3 |
4 | export default defineConfig({
5 | schema: './src/lib/server/db/schema.ts',
6 |
7 | dbCredentials: {
8 | url: process.env.DATABASE_URL
9 | },
10 |
11 | verbose: true,
12 | strict: true,
13 | dialect: 'postgresql',
14 | casing: 'snake_case'
15 | });
16 |
--------------------------------------------------------------------------------
/drizzle/0000_burly_colonel_america.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "sessions" (
2 | "id" text PRIMARY KEY NOT NULL,
3 | "userId" integer NOT NULL,
4 | "expiresAt" timestamp with time zone NOT NULL
5 | );
6 | --> statement-breakpoint
7 | CREATE TABLE IF NOT EXISTS "users" (
8 | "id" serial PRIMARY KEY NOT NULL,
9 | "googleId" varchar NOT NULL,
10 | "email" varchar NOT NULL,
11 | "name" varchar,
12 | "picture" text,
13 | CONSTRAINT "users_googleId_unique" UNIQUE("googleId"),
14 | CONSTRAINT "users_email_unique" UNIQUE("email")
15 | );
16 | --> statement-breakpoint
17 | DO $$ BEGIN
18 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_userId_users_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
19 | EXCEPTION
20 | WHEN duplicate_object THEN null;
21 | END $$;
22 |
--------------------------------------------------------------------------------
/drizzle/0001_little_mercury.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "sessions" RENAME COLUMN "userId" TO "user_id";--> statement-breakpoint
2 | ALTER TABLE "sessions" RENAME COLUMN "expiresAt" TO "expires_at";--> statement-breakpoint
3 | ALTER TABLE "users" RENAME COLUMN "googleId" TO "google_id";--> statement-breakpoint
4 | ALTER TABLE "users" DROP CONSTRAINT "users_googleId_unique";--> statement-breakpoint
5 | ALTER TABLE "sessions" DROP CONSTRAINT "sessions_userId_users_id_fk";
6 | --> statement-breakpoint
7 | DO $$ BEGIN
8 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
9 | EXCEPTION
10 | WHEN duplicate_object THEN null;
11 | END $$;
12 | --> statement-breakpoint
13 | ALTER TABLE "users" ADD CONSTRAINT "users_googleId_unique" UNIQUE("google_id");
--------------------------------------------------------------------------------
/drizzle/0002_smiling_sphinx.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "credits" (
2 | "id" integer PRIMARY KEY NOT NULL,
3 | "amount" integer DEFAULT 5 NOT NULL
4 | );
5 | --> statement-breakpoint
6 | DO $$ BEGIN
7 | ALTER TABLE "credits" ADD CONSTRAINT "credits_id_users_id_fk" FOREIGN KEY ("id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
8 | EXCEPTION
9 | WHEN duplicate_object THEN null;
10 | END $$;
11 |
--------------------------------------------------------------------------------
/drizzle/0003_high_quasar.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "invoices" (
2 | "id" varchar PRIMARY KEY NOT NULL,
3 | "user_id" integer NOT NULL,
4 | "status" varchar DEFAULT 'UNPAID' NOT NULL,
5 | "expired_time" timestamp with time zone NOT NULL,
6 | "package" integer NOT NULL,
7 | "amount" integer NOT NULL
8 | );
9 | --> statement-breakpoint
10 | DO $$ BEGIN
11 | ALTER TABLE "invoices" ADD CONSTRAINT "invoices_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
12 | EXCEPTION
13 | WHEN duplicate_object THEN null;
14 | END $$;
15 |
--------------------------------------------------------------------------------
/drizzle/0004_clammy_prowler.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "invoices" ADD COLUMN "paid_at" timestamp with time zone;
--------------------------------------------------------------------------------
/drizzle/0005_puzzling_vampiro.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "invoices" ADD COLUMN "created_at" timestamp with time zone DEFAULT now();
--------------------------------------------------------------------------------
/drizzle/0006_stale_purple_man.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "invoices" ADD COLUMN "checkout_url" text;
--------------------------------------------------------------------------------
/drizzle/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "5cf321e1-5954-4adc-b17b-f442807367ad",
3 | "prevId": "00000000-0000-0000-0000-000000000000",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.sessions": {
8 | "name": "sessions",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "text",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "userId": {
18 | "name": "userId",
19 | "type": "integer",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "expiresAt": {
24 | "name": "expiresAt",
25 | "type": "timestamp with time zone",
26 | "primaryKey": false,
27 | "notNull": true
28 | }
29 | },
30 | "indexes": {},
31 | "foreignKeys": {
32 | "sessions_userId_users_id_fk": {
33 | "name": "sessions_userId_users_id_fk",
34 | "tableFrom": "sessions",
35 | "tableTo": "users",
36 | "columnsFrom": ["userId"],
37 | "columnsTo": ["id"],
38 | "onDelete": "no action",
39 | "onUpdate": "no action"
40 | }
41 | },
42 | "compositePrimaryKeys": {},
43 | "uniqueConstraints": {},
44 | "policies": {},
45 | "checkConstraints": {},
46 | "isRLSEnabled": false
47 | },
48 | "public.users": {
49 | "name": "users",
50 | "schema": "",
51 | "columns": {
52 | "id": {
53 | "name": "id",
54 | "type": "serial",
55 | "primaryKey": true,
56 | "notNull": true
57 | },
58 | "googleId": {
59 | "name": "googleId",
60 | "type": "varchar",
61 | "primaryKey": false,
62 | "notNull": true
63 | },
64 | "email": {
65 | "name": "email",
66 | "type": "varchar",
67 | "primaryKey": false,
68 | "notNull": true
69 | },
70 | "name": {
71 | "name": "name",
72 | "type": "varchar",
73 | "primaryKey": false,
74 | "notNull": false
75 | },
76 | "picture": {
77 | "name": "picture",
78 | "type": "text",
79 | "primaryKey": false,
80 | "notNull": false
81 | }
82 | },
83 | "indexes": {},
84 | "foreignKeys": {},
85 | "compositePrimaryKeys": {},
86 | "uniqueConstraints": {
87 | "users_googleId_unique": {
88 | "name": "users_googleId_unique",
89 | "nullsNotDistinct": false,
90 | "columns": ["googleId"]
91 | },
92 | "users_email_unique": {
93 | "name": "users_email_unique",
94 | "nullsNotDistinct": false,
95 | "columns": ["email"]
96 | }
97 | },
98 | "policies": {},
99 | "checkConstraints": {},
100 | "isRLSEnabled": false
101 | }
102 | },
103 | "enums": {},
104 | "schemas": {},
105 | "sequences": {},
106 | "roles": {},
107 | "policies": {},
108 | "views": {},
109 | "_meta": {
110 | "columns": {},
111 | "schemas": {},
112 | "tables": {}
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/drizzle/meta/0001_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "5dc43137-fb62-40b9-a5b3-dccafcdbbc0a",
3 | "prevId": "5cf321e1-5954-4adc-b17b-f442807367ad",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.sessions": {
8 | "name": "sessions",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "text",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "user_id": {
18 | "name": "user_id",
19 | "type": "integer",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "expires_at": {
24 | "name": "expires_at",
25 | "type": "timestamp with time zone",
26 | "primaryKey": false,
27 | "notNull": true
28 | }
29 | },
30 | "indexes": {},
31 | "foreignKeys": {
32 | "sessions_user_id_users_id_fk": {
33 | "name": "sessions_user_id_users_id_fk",
34 | "tableFrom": "sessions",
35 | "tableTo": "users",
36 | "columnsFrom": ["user_id"],
37 | "columnsTo": ["id"],
38 | "onDelete": "no action",
39 | "onUpdate": "no action"
40 | }
41 | },
42 | "compositePrimaryKeys": {},
43 | "uniqueConstraints": {},
44 | "policies": {},
45 | "checkConstraints": {},
46 | "isRLSEnabled": false
47 | },
48 | "public.users": {
49 | "name": "users",
50 | "schema": "",
51 | "columns": {
52 | "id": {
53 | "name": "id",
54 | "type": "serial",
55 | "primaryKey": true,
56 | "notNull": true
57 | },
58 | "google_id": {
59 | "name": "google_id",
60 | "type": "varchar",
61 | "primaryKey": false,
62 | "notNull": true
63 | },
64 | "email": {
65 | "name": "email",
66 | "type": "varchar",
67 | "primaryKey": false,
68 | "notNull": true
69 | },
70 | "name": {
71 | "name": "name",
72 | "type": "varchar",
73 | "primaryKey": false,
74 | "notNull": false
75 | },
76 | "picture": {
77 | "name": "picture",
78 | "type": "text",
79 | "primaryKey": false,
80 | "notNull": false
81 | }
82 | },
83 | "indexes": {},
84 | "foreignKeys": {},
85 | "compositePrimaryKeys": {},
86 | "uniqueConstraints": {
87 | "users_googleId_unique": {
88 | "name": "users_googleId_unique",
89 | "nullsNotDistinct": false,
90 | "columns": ["google_id"]
91 | },
92 | "users_email_unique": {
93 | "name": "users_email_unique",
94 | "nullsNotDistinct": false,
95 | "columns": ["email"]
96 | }
97 | },
98 | "policies": {},
99 | "checkConstraints": {},
100 | "isRLSEnabled": false
101 | }
102 | },
103 | "enums": {},
104 | "schemas": {},
105 | "sequences": {},
106 | "roles": {},
107 | "policies": {},
108 | "views": {},
109 | "_meta": {
110 | "columns": {},
111 | "schemas": {},
112 | "tables": {}
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/drizzle/meta/0002_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "9c8c6f4d-92b3-4ddb-b34c-23d12cd9b21d",
3 | "prevId": "5dc43137-fb62-40b9-a5b3-dccafcdbbc0a",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.credits": {
8 | "name": "credits",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "integer",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "amount": {
18 | "name": "amount",
19 | "type": "integer",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "default": 5
23 | }
24 | },
25 | "indexes": {},
26 | "foreignKeys": {
27 | "credits_id_users_id_fk": {
28 | "name": "credits_id_users_id_fk",
29 | "tableFrom": "credits",
30 | "tableTo": "users",
31 | "columnsFrom": [
32 | "id"
33 | ],
34 | "columnsTo": [
35 | "id"
36 | ],
37 | "onDelete": "no action",
38 | "onUpdate": "no action"
39 | }
40 | },
41 | "compositePrimaryKeys": {},
42 | "uniqueConstraints": {},
43 | "policies": {},
44 | "checkConstraints": {},
45 | "isRLSEnabled": false
46 | },
47 | "public.sessions": {
48 | "name": "sessions",
49 | "schema": "",
50 | "columns": {
51 | "id": {
52 | "name": "id",
53 | "type": "text",
54 | "primaryKey": true,
55 | "notNull": true
56 | },
57 | "user_id": {
58 | "name": "user_id",
59 | "type": "integer",
60 | "primaryKey": false,
61 | "notNull": true
62 | },
63 | "expires_at": {
64 | "name": "expires_at",
65 | "type": "timestamp with time zone",
66 | "primaryKey": false,
67 | "notNull": true
68 | }
69 | },
70 | "indexes": {},
71 | "foreignKeys": {
72 | "sessions_user_id_users_id_fk": {
73 | "name": "sessions_user_id_users_id_fk",
74 | "tableFrom": "sessions",
75 | "tableTo": "users",
76 | "columnsFrom": [
77 | "user_id"
78 | ],
79 | "columnsTo": [
80 | "id"
81 | ],
82 | "onDelete": "no action",
83 | "onUpdate": "no action"
84 | }
85 | },
86 | "compositePrimaryKeys": {},
87 | "uniqueConstraints": {},
88 | "policies": {},
89 | "checkConstraints": {},
90 | "isRLSEnabled": false
91 | },
92 | "public.users": {
93 | "name": "users",
94 | "schema": "",
95 | "columns": {
96 | "id": {
97 | "name": "id",
98 | "type": "serial",
99 | "primaryKey": true,
100 | "notNull": true
101 | },
102 | "google_id": {
103 | "name": "google_id",
104 | "type": "varchar",
105 | "primaryKey": false,
106 | "notNull": true
107 | },
108 | "email": {
109 | "name": "email",
110 | "type": "varchar",
111 | "primaryKey": false,
112 | "notNull": true
113 | },
114 | "name": {
115 | "name": "name",
116 | "type": "varchar",
117 | "primaryKey": false,
118 | "notNull": false
119 | },
120 | "picture": {
121 | "name": "picture",
122 | "type": "text",
123 | "primaryKey": false,
124 | "notNull": false
125 | }
126 | },
127 | "indexes": {},
128 | "foreignKeys": {},
129 | "compositePrimaryKeys": {},
130 | "uniqueConstraints": {
131 | "users_googleId_unique": {
132 | "name": "users_googleId_unique",
133 | "nullsNotDistinct": false,
134 | "columns": [
135 | "google_id"
136 | ]
137 | },
138 | "users_email_unique": {
139 | "name": "users_email_unique",
140 | "nullsNotDistinct": false,
141 | "columns": [
142 | "email"
143 | ]
144 | }
145 | },
146 | "policies": {},
147 | "checkConstraints": {},
148 | "isRLSEnabled": false
149 | }
150 | },
151 | "enums": {},
152 | "schemas": {},
153 | "sequences": {},
154 | "roles": {},
155 | "policies": {},
156 | "views": {},
157 | "_meta": {
158 | "columns": {},
159 | "schemas": {},
160 | "tables": {}
161 | }
162 | }
--------------------------------------------------------------------------------
/drizzle/meta/0003_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "2feeb464-36dc-468b-adfa-5cbb4a409899",
3 | "prevId": "9c8c6f4d-92b3-4ddb-b34c-23d12cd9b21d",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.credits": {
8 | "name": "credits",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "integer",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "amount": {
18 | "name": "amount",
19 | "type": "integer",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "default": 5
23 | }
24 | },
25 | "indexes": {},
26 | "foreignKeys": {
27 | "credits_id_users_id_fk": {
28 | "name": "credits_id_users_id_fk",
29 | "tableFrom": "credits",
30 | "tableTo": "users",
31 | "columnsFrom": [
32 | "id"
33 | ],
34 | "columnsTo": [
35 | "id"
36 | ],
37 | "onDelete": "no action",
38 | "onUpdate": "no action"
39 | }
40 | },
41 | "compositePrimaryKeys": {},
42 | "uniqueConstraints": {},
43 | "policies": {},
44 | "checkConstraints": {},
45 | "isRLSEnabled": false
46 | },
47 | "public.invoices": {
48 | "name": "invoices",
49 | "schema": "",
50 | "columns": {
51 | "id": {
52 | "name": "id",
53 | "type": "varchar",
54 | "primaryKey": true,
55 | "notNull": true
56 | },
57 | "user_id": {
58 | "name": "user_id",
59 | "type": "integer",
60 | "primaryKey": false,
61 | "notNull": true
62 | },
63 | "status": {
64 | "name": "status",
65 | "type": "varchar",
66 | "primaryKey": false,
67 | "notNull": true,
68 | "default": "'UNPAID'"
69 | },
70 | "expired_time": {
71 | "name": "expired_time",
72 | "type": "timestamp with time zone",
73 | "primaryKey": false,
74 | "notNull": true
75 | },
76 | "package": {
77 | "name": "package",
78 | "type": "integer",
79 | "primaryKey": false,
80 | "notNull": true
81 | },
82 | "amount": {
83 | "name": "amount",
84 | "type": "integer",
85 | "primaryKey": false,
86 | "notNull": true
87 | }
88 | },
89 | "indexes": {},
90 | "foreignKeys": {
91 | "invoices_user_id_users_id_fk": {
92 | "name": "invoices_user_id_users_id_fk",
93 | "tableFrom": "invoices",
94 | "tableTo": "users",
95 | "columnsFrom": [
96 | "user_id"
97 | ],
98 | "columnsTo": [
99 | "id"
100 | ],
101 | "onDelete": "no action",
102 | "onUpdate": "no action"
103 | }
104 | },
105 | "compositePrimaryKeys": {},
106 | "uniqueConstraints": {},
107 | "policies": {},
108 | "checkConstraints": {},
109 | "isRLSEnabled": false
110 | },
111 | "public.sessions": {
112 | "name": "sessions",
113 | "schema": "",
114 | "columns": {
115 | "id": {
116 | "name": "id",
117 | "type": "text",
118 | "primaryKey": true,
119 | "notNull": true
120 | },
121 | "user_id": {
122 | "name": "user_id",
123 | "type": "integer",
124 | "primaryKey": false,
125 | "notNull": true
126 | },
127 | "expires_at": {
128 | "name": "expires_at",
129 | "type": "timestamp with time zone",
130 | "primaryKey": false,
131 | "notNull": true
132 | }
133 | },
134 | "indexes": {},
135 | "foreignKeys": {
136 | "sessions_user_id_users_id_fk": {
137 | "name": "sessions_user_id_users_id_fk",
138 | "tableFrom": "sessions",
139 | "tableTo": "users",
140 | "columnsFrom": [
141 | "user_id"
142 | ],
143 | "columnsTo": [
144 | "id"
145 | ],
146 | "onDelete": "no action",
147 | "onUpdate": "no action"
148 | }
149 | },
150 | "compositePrimaryKeys": {},
151 | "uniqueConstraints": {},
152 | "policies": {},
153 | "checkConstraints": {},
154 | "isRLSEnabled": false
155 | },
156 | "public.users": {
157 | "name": "users",
158 | "schema": "",
159 | "columns": {
160 | "id": {
161 | "name": "id",
162 | "type": "serial",
163 | "primaryKey": true,
164 | "notNull": true
165 | },
166 | "google_id": {
167 | "name": "google_id",
168 | "type": "varchar",
169 | "primaryKey": false,
170 | "notNull": true
171 | },
172 | "email": {
173 | "name": "email",
174 | "type": "varchar",
175 | "primaryKey": false,
176 | "notNull": true
177 | },
178 | "name": {
179 | "name": "name",
180 | "type": "varchar",
181 | "primaryKey": false,
182 | "notNull": false
183 | },
184 | "picture": {
185 | "name": "picture",
186 | "type": "text",
187 | "primaryKey": false,
188 | "notNull": false
189 | }
190 | },
191 | "indexes": {},
192 | "foreignKeys": {},
193 | "compositePrimaryKeys": {},
194 | "uniqueConstraints": {
195 | "users_googleId_unique": {
196 | "name": "users_googleId_unique",
197 | "nullsNotDistinct": false,
198 | "columns": [
199 | "google_id"
200 | ]
201 | },
202 | "users_email_unique": {
203 | "name": "users_email_unique",
204 | "nullsNotDistinct": false,
205 | "columns": [
206 | "email"
207 | ]
208 | }
209 | },
210 | "policies": {},
211 | "checkConstraints": {},
212 | "isRLSEnabled": false
213 | }
214 | },
215 | "enums": {},
216 | "schemas": {},
217 | "sequences": {},
218 | "roles": {},
219 | "policies": {},
220 | "views": {},
221 | "_meta": {
222 | "columns": {},
223 | "schemas": {},
224 | "tables": {}
225 | }
226 | }
--------------------------------------------------------------------------------
/drizzle/meta/0004_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "956c2f32-007a-43db-a459-323edb41f43f",
3 | "prevId": "2feeb464-36dc-468b-adfa-5cbb4a409899",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.credits": {
8 | "name": "credits",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "integer",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "amount": {
18 | "name": "amount",
19 | "type": "integer",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "default": 5
23 | }
24 | },
25 | "indexes": {},
26 | "foreignKeys": {
27 | "credits_id_users_id_fk": {
28 | "name": "credits_id_users_id_fk",
29 | "tableFrom": "credits",
30 | "tableTo": "users",
31 | "columnsFrom": [
32 | "id"
33 | ],
34 | "columnsTo": [
35 | "id"
36 | ],
37 | "onDelete": "no action",
38 | "onUpdate": "no action"
39 | }
40 | },
41 | "compositePrimaryKeys": {},
42 | "uniqueConstraints": {},
43 | "policies": {},
44 | "checkConstraints": {},
45 | "isRLSEnabled": false
46 | },
47 | "public.invoices": {
48 | "name": "invoices",
49 | "schema": "",
50 | "columns": {
51 | "id": {
52 | "name": "id",
53 | "type": "varchar",
54 | "primaryKey": true,
55 | "notNull": true
56 | },
57 | "user_id": {
58 | "name": "user_id",
59 | "type": "integer",
60 | "primaryKey": false,
61 | "notNull": true
62 | },
63 | "status": {
64 | "name": "status",
65 | "type": "varchar",
66 | "primaryKey": false,
67 | "notNull": true,
68 | "default": "'UNPAID'"
69 | },
70 | "expired_time": {
71 | "name": "expired_time",
72 | "type": "timestamp with time zone",
73 | "primaryKey": false,
74 | "notNull": true
75 | },
76 | "paid_at": {
77 | "name": "paid_at",
78 | "type": "timestamp with time zone",
79 | "primaryKey": false,
80 | "notNull": false
81 | },
82 | "package": {
83 | "name": "package",
84 | "type": "integer",
85 | "primaryKey": false,
86 | "notNull": true
87 | },
88 | "amount": {
89 | "name": "amount",
90 | "type": "integer",
91 | "primaryKey": false,
92 | "notNull": true
93 | }
94 | },
95 | "indexes": {},
96 | "foreignKeys": {
97 | "invoices_user_id_users_id_fk": {
98 | "name": "invoices_user_id_users_id_fk",
99 | "tableFrom": "invoices",
100 | "tableTo": "users",
101 | "columnsFrom": [
102 | "user_id"
103 | ],
104 | "columnsTo": [
105 | "id"
106 | ],
107 | "onDelete": "no action",
108 | "onUpdate": "no action"
109 | }
110 | },
111 | "compositePrimaryKeys": {},
112 | "uniqueConstraints": {},
113 | "policies": {},
114 | "checkConstraints": {},
115 | "isRLSEnabled": false
116 | },
117 | "public.sessions": {
118 | "name": "sessions",
119 | "schema": "",
120 | "columns": {
121 | "id": {
122 | "name": "id",
123 | "type": "text",
124 | "primaryKey": true,
125 | "notNull": true
126 | },
127 | "user_id": {
128 | "name": "user_id",
129 | "type": "integer",
130 | "primaryKey": false,
131 | "notNull": true
132 | },
133 | "expires_at": {
134 | "name": "expires_at",
135 | "type": "timestamp with time zone",
136 | "primaryKey": false,
137 | "notNull": true
138 | }
139 | },
140 | "indexes": {},
141 | "foreignKeys": {
142 | "sessions_user_id_users_id_fk": {
143 | "name": "sessions_user_id_users_id_fk",
144 | "tableFrom": "sessions",
145 | "tableTo": "users",
146 | "columnsFrom": [
147 | "user_id"
148 | ],
149 | "columnsTo": [
150 | "id"
151 | ],
152 | "onDelete": "no action",
153 | "onUpdate": "no action"
154 | }
155 | },
156 | "compositePrimaryKeys": {},
157 | "uniqueConstraints": {},
158 | "policies": {},
159 | "checkConstraints": {},
160 | "isRLSEnabled": false
161 | },
162 | "public.users": {
163 | "name": "users",
164 | "schema": "",
165 | "columns": {
166 | "id": {
167 | "name": "id",
168 | "type": "serial",
169 | "primaryKey": true,
170 | "notNull": true
171 | },
172 | "google_id": {
173 | "name": "google_id",
174 | "type": "varchar",
175 | "primaryKey": false,
176 | "notNull": true
177 | },
178 | "email": {
179 | "name": "email",
180 | "type": "varchar",
181 | "primaryKey": false,
182 | "notNull": true
183 | },
184 | "name": {
185 | "name": "name",
186 | "type": "varchar",
187 | "primaryKey": false,
188 | "notNull": false
189 | },
190 | "picture": {
191 | "name": "picture",
192 | "type": "text",
193 | "primaryKey": false,
194 | "notNull": false
195 | }
196 | },
197 | "indexes": {},
198 | "foreignKeys": {},
199 | "compositePrimaryKeys": {},
200 | "uniqueConstraints": {
201 | "users_googleId_unique": {
202 | "name": "users_googleId_unique",
203 | "nullsNotDistinct": false,
204 | "columns": [
205 | "google_id"
206 | ]
207 | },
208 | "users_email_unique": {
209 | "name": "users_email_unique",
210 | "nullsNotDistinct": false,
211 | "columns": [
212 | "email"
213 | ]
214 | }
215 | },
216 | "policies": {},
217 | "checkConstraints": {},
218 | "isRLSEnabled": false
219 | }
220 | },
221 | "enums": {},
222 | "schemas": {},
223 | "sequences": {},
224 | "roles": {},
225 | "policies": {},
226 | "views": {},
227 | "_meta": {
228 | "columns": {},
229 | "schemas": {},
230 | "tables": {}
231 | }
232 | }
--------------------------------------------------------------------------------
/drizzle/meta/0005_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "01f7a8c0-a363-4c0d-8a0b-a2121790eb3e",
3 | "prevId": "956c2f32-007a-43db-a459-323edb41f43f",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.credits": {
8 | "name": "credits",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "integer",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "amount": {
18 | "name": "amount",
19 | "type": "integer",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "default": 5
23 | }
24 | },
25 | "indexes": {},
26 | "foreignKeys": {
27 | "credits_id_users_id_fk": {
28 | "name": "credits_id_users_id_fk",
29 | "tableFrom": "credits",
30 | "tableTo": "users",
31 | "columnsFrom": [
32 | "id"
33 | ],
34 | "columnsTo": [
35 | "id"
36 | ],
37 | "onDelete": "no action",
38 | "onUpdate": "no action"
39 | }
40 | },
41 | "compositePrimaryKeys": {},
42 | "uniqueConstraints": {},
43 | "policies": {},
44 | "checkConstraints": {},
45 | "isRLSEnabled": false
46 | },
47 | "public.invoices": {
48 | "name": "invoices",
49 | "schema": "",
50 | "columns": {
51 | "id": {
52 | "name": "id",
53 | "type": "varchar",
54 | "primaryKey": true,
55 | "notNull": true
56 | },
57 | "user_id": {
58 | "name": "user_id",
59 | "type": "integer",
60 | "primaryKey": false,
61 | "notNull": true
62 | },
63 | "status": {
64 | "name": "status",
65 | "type": "varchar",
66 | "primaryKey": false,
67 | "notNull": true,
68 | "default": "'UNPAID'"
69 | },
70 | "expired_time": {
71 | "name": "expired_time",
72 | "type": "timestamp with time zone",
73 | "primaryKey": false,
74 | "notNull": true
75 | },
76 | "paid_at": {
77 | "name": "paid_at",
78 | "type": "timestamp with time zone",
79 | "primaryKey": false,
80 | "notNull": false
81 | },
82 | "package": {
83 | "name": "package",
84 | "type": "integer",
85 | "primaryKey": false,
86 | "notNull": true
87 | },
88 | "amount": {
89 | "name": "amount",
90 | "type": "integer",
91 | "primaryKey": false,
92 | "notNull": true
93 | },
94 | "created_at": {
95 | "name": "created_at",
96 | "type": "timestamp with time zone",
97 | "primaryKey": false,
98 | "notNull": false,
99 | "default": "now()"
100 | }
101 | },
102 | "indexes": {},
103 | "foreignKeys": {
104 | "invoices_user_id_users_id_fk": {
105 | "name": "invoices_user_id_users_id_fk",
106 | "tableFrom": "invoices",
107 | "tableTo": "users",
108 | "columnsFrom": [
109 | "user_id"
110 | ],
111 | "columnsTo": [
112 | "id"
113 | ],
114 | "onDelete": "no action",
115 | "onUpdate": "no action"
116 | }
117 | },
118 | "compositePrimaryKeys": {},
119 | "uniqueConstraints": {},
120 | "policies": {},
121 | "checkConstraints": {},
122 | "isRLSEnabled": false
123 | },
124 | "public.sessions": {
125 | "name": "sessions",
126 | "schema": "",
127 | "columns": {
128 | "id": {
129 | "name": "id",
130 | "type": "text",
131 | "primaryKey": true,
132 | "notNull": true
133 | },
134 | "user_id": {
135 | "name": "user_id",
136 | "type": "integer",
137 | "primaryKey": false,
138 | "notNull": true
139 | },
140 | "expires_at": {
141 | "name": "expires_at",
142 | "type": "timestamp with time zone",
143 | "primaryKey": false,
144 | "notNull": true
145 | }
146 | },
147 | "indexes": {},
148 | "foreignKeys": {
149 | "sessions_user_id_users_id_fk": {
150 | "name": "sessions_user_id_users_id_fk",
151 | "tableFrom": "sessions",
152 | "tableTo": "users",
153 | "columnsFrom": [
154 | "user_id"
155 | ],
156 | "columnsTo": [
157 | "id"
158 | ],
159 | "onDelete": "no action",
160 | "onUpdate": "no action"
161 | }
162 | },
163 | "compositePrimaryKeys": {},
164 | "uniqueConstraints": {},
165 | "policies": {},
166 | "checkConstraints": {},
167 | "isRLSEnabled": false
168 | },
169 | "public.users": {
170 | "name": "users",
171 | "schema": "",
172 | "columns": {
173 | "id": {
174 | "name": "id",
175 | "type": "serial",
176 | "primaryKey": true,
177 | "notNull": true
178 | },
179 | "google_id": {
180 | "name": "google_id",
181 | "type": "varchar",
182 | "primaryKey": false,
183 | "notNull": true
184 | },
185 | "email": {
186 | "name": "email",
187 | "type": "varchar",
188 | "primaryKey": false,
189 | "notNull": true
190 | },
191 | "name": {
192 | "name": "name",
193 | "type": "varchar",
194 | "primaryKey": false,
195 | "notNull": false
196 | },
197 | "picture": {
198 | "name": "picture",
199 | "type": "text",
200 | "primaryKey": false,
201 | "notNull": false
202 | }
203 | },
204 | "indexes": {},
205 | "foreignKeys": {},
206 | "compositePrimaryKeys": {},
207 | "uniqueConstraints": {
208 | "users_googleId_unique": {
209 | "name": "users_googleId_unique",
210 | "nullsNotDistinct": false,
211 | "columns": [
212 | "google_id"
213 | ]
214 | },
215 | "users_email_unique": {
216 | "name": "users_email_unique",
217 | "nullsNotDistinct": false,
218 | "columns": [
219 | "email"
220 | ]
221 | }
222 | },
223 | "policies": {},
224 | "checkConstraints": {},
225 | "isRLSEnabled": false
226 | }
227 | },
228 | "enums": {},
229 | "schemas": {},
230 | "sequences": {},
231 | "roles": {},
232 | "policies": {},
233 | "views": {},
234 | "_meta": {
235 | "columns": {},
236 | "schemas": {},
237 | "tables": {}
238 | }
239 | }
--------------------------------------------------------------------------------
/drizzle/meta/0006_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "66a52ed4-3e8e-4af6-b111-6504ed56f93d",
3 | "prevId": "01f7a8c0-a363-4c0d-8a0b-a2121790eb3e",
4 | "version": "7",
5 | "dialect": "postgresql",
6 | "tables": {
7 | "public.credits": {
8 | "name": "credits",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "integer",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "amount": {
18 | "name": "amount",
19 | "type": "integer",
20 | "primaryKey": false,
21 | "notNull": true,
22 | "default": 5
23 | }
24 | },
25 | "indexes": {},
26 | "foreignKeys": {
27 | "credits_id_users_id_fk": {
28 | "name": "credits_id_users_id_fk",
29 | "tableFrom": "credits",
30 | "tableTo": "users",
31 | "columnsFrom": [
32 | "id"
33 | ],
34 | "columnsTo": [
35 | "id"
36 | ],
37 | "onDelete": "no action",
38 | "onUpdate": "no action"
39 | }
40 | },
41 | "compositePrimaryKeys": {},
42 | "uniqueConstraints": {},
43 | "policies": {},
44 | "checkConstraints": {},
45 | "isRLSEnabled": false
46 | },
47 | "public.invoices": {
48 | "name": "invoices",
49 | "schema": "",
50 | "columns": {
51 | "id": {
52 | "name": "id",
53 | "type": "varchar",
54 | "primaryKey": true,
55 | "notNull": true
56 | },
57 | "user_id": {
58 | "name": "user_id",
59 | "type": "integer",
60 | "primaryKey": false,
61 | "notNull": true
62 | },
63 | "status": {
64 | "name": "status",
65 | "type": "varchar",
66 | "primaryKey": false,
67 | "notNull": true,
68 | "default": "'UNPAID'"
69 | },
70 | "expired_time": {
71 | "name": "expired_time",
72 | "type": "timestamp with time zone",
73 | "primaryKey": false,
74 | "notNull": true
75 | },
76 | "paid_at": {
77 | "name": "paid_at",
78 | "type": "timestamp with time zone",
79 | "primaryKey": false,
80 | "notNull": false
81 | },
82 | "package": {
83 | "name": "package",
84 | "type": "integer",
85 | "primaryKey": false,
86 | "notNull": true
87 | },
88 | "amount": {
89 | "name": "amount",
90 | "type": "integer",
91 | "primaryKey": false,
92 | "notNull": true
93 | },
94 | "checkout_url": {
95 | "name": "checkout_url",
96 | "type": "text",
97 | "primaryKey": false,
98 | "notNull": false
99 | },
100 | "created_at": {
101 | "name": "created_at",
102 | "type": "timestamp with time zone",
103 | "primaryKey": false,
104 | "notNull": false,
105 | "default": "now()"
106 | }
107 | },
108 | "indexes": {},
109 | "foreignKeys": {
110 | "invoices_user_id_users_id_fk": {
111 | "name": "invoices_user_id_users_id_fk",
112 | "tableFrom": "invoices",
113 | "tableTo": "users",
114 | "columnsFrom": [
115 | "user_id"
116 | ],
117 | "columnsTo": [
118 | "id"
119 | ],
120 | "onDelete": "no action",
121 | "onUpdate": "no action"
122 | }
123 | },
124 | "compositePrimaryKeys": {},
125 | "uniqueConstraints": {},
126 | "policies": {},
127 | "checkConstraints": {},
128 | "isRLSEnabled": false
129 | },
130 | "public.sessions": {
131 | "name": "sessions",
132 | "schema": "",
133 | "columns": {
134 | "id": {
135 | "name": "id",
136 | "type": "text",
137 | "primaryKey": true,
138 | "notNull": true
139 | },
140 | "user_id": {
141 | "name": "user_id",
142 | "type": "integer",
143 | "primaryKey": false,
144 | "notNull": true
145 | },
146 | "expires_at": {
147 | "name": "expires_at",
148 | "type": "timestamp with time zone",
149 | "primaryKey": false,
150 | "notNull": true
151 | }
152 | },
153 | "indexes": {},
154 | "foreignKeys": {
155 | "sessions_user_id_users_id_fk": {
156 | "name": "sessions_user_id_users_id_fk",
157 | "tableFrom": "sessions",
158 | "tableTo": "users",
159 | "columnsFrom": [
160 | "user_id"
161 | ],
162 | "columnsTo": [
163 | "id"
164 | ],
165 | "onDelete": "no action",
166 | "onUpdate": "no action"
167 | }
168 | },
169 | "compositePrimaryKeys": {},
170 | "uniqueConstraints": {},
171 | "policies": {},
172 | "checkConstraints": {},
173 | "isRLSEnabled": false
174 | },
175 | "public.users": {
176 | "name": "users",
177 | "schema": "",
178 | "columns": {
179 | "id": {
180 | "name": "id",
181 | "type": "serial",
182 | "primaryKey": true,
183 | "notNull": true
184 | },
185 | "google_id": {
186 | "name": "google_id",
187 | "type": "varchar",
188 | "primaryKey": false,
189 | "notNull": true
190 | },
191 | "email": {
192 | "name": "email",
193 | "type": "varchar",
194 | "primaryKey": false,
195 | "notNull": true
196 | },
197 | "name": {
198 | "name": "name",
199 | "type": "varchar",
200 | "primaryKey": false,
201 | "notNull": false
202 | },
203 | "picture": {
204 | "name": "picture",
205 | "type": "text",
206 | "primaryKey": false,
207 | "notNull": false
208 | }
209 | },
210 | "indexes": {},
211 | "foreignKeys": {},
212 | "compositePrimaryKeys": {},
213 | "uniqueConstraints": {
214 | "users_googleId_unique": {
215 | "name": "users_googleId_unique",
216 | "nullsNotDistinct": false,
217 | "columns": [
218 | "google_id"
219 | ]
220 | },
221 | "users_email_unique": {
222 | "name": "users_email_unique",
223 | "nullsNotDistinct": false,
224 | "columns": [
225 | "email"
226 | ]
227 | }
228 | },
229 | "policies": {},
230 | "checkConstraints": {},
231 | "isRLSEnabled": false
232 | }
233 | },
234 | "enums": {},
235 | "schemas": {},
236 | "sequences": {},
237 | "roles": {},
238 | "policies": {},
239 | "views": {},
240 | "_meta": {
241 | "columns": {},
242 | "schemas": {},
243 | "tables": {}
244 | }
245 | }
--------------------------------------------------------------------------------
/drizzle/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "7",
3 | "dialect": "postgresql",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "7",
8 | "when": 1731838338416,
9 | "tag": "0000_burly_colonel_america",
10 | "breakpoints": true
11 | },
12 | {
13 | "idx": 1,
14 | "version": "7",
15 | "when": 1731838510220,
16 | "tag": "0001_little_mercury",
17 | "breakpoints": true
18 | },
19 | {
20 | "idx": 2,
21 | "version": "7",
22 | "when": 1731923769234,
23 | "tag": "0002_smiling_sphinx",
24 | "breakpoints": true
25 | },
26 | {
27 | "idx": 3,
28 | "version": "7",
29 | "when": 1731978302985,
30 | "tag": "0003_high_quasar",
31 | "breakpoints": true
32 | },
33 | {
34 | "idx": 4,
35 | "version": "7",
36 | "when": 1731979053822,
37 | "tag": "0004_clammy_prowler",
38 | "breakpoints": true
39 | },
40 | {
41 | "idx": 5,
42 | "version": "7",
43 | "when": 1731981368729,
44 | "tag": "0005_puzzling_vampiro",
45 | "breakpoints": true
46 | },
47 | {
48 | "idx": 6,
49 | "version": "7",
50 | "when": 1731984952998,
51 | "tag": "0006_stale_purple_man",
52 | "breakpoints": true
53 | }
54 | ]
55 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import prettier from 'eslint-config-prettier';
2 | import js from '@eslint/js';
3 | import svelte from 'eslint-plugin-svelte';
4 | import globals from 'globals';
5 | import ts from 'typescript-eslint';
6 |
7 | export default ts.config(
8 | js.configs.recommended,
9 | ...ts.configs.recommended,
10 | ...svelte.configs['flat/recommended'],
11 | prettier,
12 | ...svelte.configs['flat/prettier'],
13 | {
14 | languageOptions: {
15 | globals: {
16 | ...globals.browser,
17 | ...globals.node
18 | }
19 | }
20 | },
21 | {
22 | files: ['**/*.svelte', '**/*.svx'],
23 |
24 | languageOptions: {
25 | parserOptions: {
26 | parser: ts.parser
27 | }
28 | }
29 | },
30 | {
31 | ignores: ['build/', '.svelte-kit/', 'dist/']
32 | }
33 | );
34 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "remove-biji",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "scripts": {
6 | "dev": "vite dev",
7 | "build": "vite build",
8 | "start": "drizzle-kit migrate && node build",
9 | "preview": "vite preview",
10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
12 | "format": "prettier --write .",
13 | "lint": "prettier --check . && eslint .",
14 | "db:push": "drizzle-kit push",
15 | "db:generate": "drizzle-kit generate",
16 | "db:migrate": "drizzle-kit migrate",
17 | "db:studio": "drizzle-kit studio"
18 | },
19 | "devDependencies": {
20 | "@floating-ui/dom": "^1.6.12",
21 | "@node-rs/argon2": "^2.0.0",
22 | "@oslojs/crypto": "^1.0.1",
23 | "@oslojs/encoding": "^1.1.0",
24 | "@pilcrowjs/object-parser": "^0.0.4",
25 | "@skeletonlabs/tw-plugin": "^0.4.0",
26 | "@sveltejs/adapter-node": "^5.2.9",
27 | "@sveltejs/kit": "^2.8.3",
28 | "@sveltejs/vite-plugin-svelte": "^4.0.2",
29 | "@tailwindcss/typography": "^0.5.15",
30 | "@types/eslint": "^9.6.1",
31 | "@types/node": "^22.9.3",
32 | "arctic": "^2.3.0",
33 | "autoprefixer": "^10.4.20",
34 | "drizzle-kit": "^0.28.1",
35 | "eslint": "9.14.0",
36 | "eslint-config-prettier": "^9.1.0",
37 | "eslint-plugin-svelte": "^2.46.0",
38 | "globals": "^15.12.0",
39 | "mdsvex": "^0.11.2",
40 | "postgres": "^3.4.5",
41 | "prettier": "^3.3.3",
42 | "prettier-plugin-svelte": "^3.3.2",
43 | "prettier-plugin-tailwindcss": "^0.6.9",
44 | "svelte": "^5.2.7",
45 | "svelte-check": "^4.1.0",
46 | "tailwindcss": "^3.4.15",
47 | "typescript": "^5.7.2",
48 | "typescript-eslint": "^8.15.0",
49 | "vite": "^5.4.11"
50 | },
51 | "dependencies": {
52 | "@fontsource/roboto": "^5.1.0",
53 | "@iconify/svelte": "^4.0.2",
54 | "@imgly/background-removal-node": "^1.4.5",
55 | "@tfkhdyt/with-catch": "npm:@jsr/tfkhdyt__with-catch@^0.3.0",
56 | "@skeletonlabs/skeleton": "^2.10.3",
57 | "clsx": "^2.1.1",
58 | "date-fns": "^4.1.0",
59 | "drizzle-orm": "^0.36.4",
60 | "jszip": "^3.10.1",
61 | "sharp": "^0.33.5",
62 | "svelte-french-toast": "^1.2.0",
63 | "svelte-seo": "^1.6.1",
64 | "sveltekit-rate-limiter": "^0.6.1",
65 | "sveltekit-superforms": "^2.20.1",
66 | "sveltekit-view-transition": "^0.5.3",
67 | "ts-pattern": "^5.5.0",
68 | "uuid": "^11.0.3",
69 | "zod": "^3.23.8"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | };
7 |
--------------------------------------------------------------------------------
/src/app.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/base';
2 | @import 'tailwindcss/components';
3 | @import 'tailwindcss/utilities';
4 |
5 | /* NOTE: set your target theme name (ex: skeleton, wintry, modern, etc) */
6 |
7 | :root [data-theme='skeleton'] {
8 | --theme-font-family-base: 'Roboto', sans-serif;
9 | --theme-font-family-heading: 'Roboto', sans-serif;
10 | /* ... */
11 | }
12 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://svelte.dev/docs/kit/types#app.d.ts
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | interface Locals {
6 | user: import('$lib/server/auth').SessionValidationResult['user'];
7 | session: import('$lib/server/auth').SessionValidationResult['session'];
8 | }
9 |
10 | namespace Superforms {
11 | type Message = {
12 | type: 'error' | 'success';
13 | message: string;
14 | };
15 | }
16 | }
17 | }
18 |
19 | export {};
20 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 |
11 | %sveltekit.body%
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/global.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.svx' {
2 | import type { SvelteComponent } from 'svelte';
3 |
4 | export default class Comp extends SvelteComponent {}
5 |
6 | export const metadata: Record;
7 | }
8 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | import type { Handle } from '@sveltejs/kit';
2 | import * as auth from '$lib/server/auth.js';
3 |
4 | const handleAuth: Handle = async ({ event, resolve }) => {
5 | const sessionToken = event.cookies.get(auth.sessionCookieName);
6 | if (!sessionToken) {
7 | event.locals.user = null;
8 | event.locals.session = null;
9 | return resolve(event);
10 | }
11 |
12 | const { session, user } = await auth.validateSessionToken(sessionToken);
13 | if (session) {
14 | auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
15 | } else {
16 | auth.deleteSessionTokenCookie(event);
17 | }
18 |
19 | event.locals.user = user;
20 | event.locals.session = session;
21 |
22 | return resolve(event);
23 | };
24 |
25 | export const handle: Handle = handleAuth;
26 |
--------------------------------------------------------------------------------
/src/lib/components/buttons/google-login-button.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
19 |
20 | Login dengan Google
21 |
22 |
--------------------------------------------------------------------------------
/src/lib/components/cards/image-picker-card.svelte:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 |
69 |
70 | {#if images && images.length > 0}
71 | 'grid-cols-1')
76 | .otherwise(() => 'grid-cols-4')
77 | )}
78 | >
79 | {#each images as image, index (image.name)}
80 |
85 |
86 |
94 | {#if outputs.length === 0 && !isLoading}
95 |
102 |
(images = images ? removeFileFromList(image, images) : undefined)}
105 | >
106 |
107 |
108 | {/if}
109 |
110 |
111 | {/each}
112 |
113 | {:else}
114 |
115 |
116 |
117 | {/if}
118 |
119 | {#if isImageExist}
120 |
146 | {/if}
147 |
148 |
--------------------------------------------------------------------------------
/src/lib/components/cards/result-card.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 | {#if outputs && outputs.length > 0}
35 | 'grid-cols-1')
41 | .otherwise(() => 'grid-cols-4')
42 | )}
43 | >
44 |
45 | {#each outputs as _, i (i)}
46 |
47 |
55 |
58 |
63 | {#if outputs.length > 1}
64 |
downloadBlob(outputPreviews[i], `remove-biji-${uuidv4()}.png`)}
67 | >
69 | {/if}
70 |
71 |
72 | {/each}
73 |
74 | {:else if isLoading}
75 |
76 |
77 |
78 |
Sabar bang, lagi diproses
79 |
80 |
81 | {:else}
82 |
83 |
Hasil akan muncul di sini
84 |
85 | {/if}
86 |
87 | {#if outputs && outputs.length > 0}
88 |
109 | {/if}
110 |
111 |
--------------------------------------------------------------------------------
/src/lib/components/compare-image.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 | Set the visibility of one image over the other. 0 is full visibility of the second image and
32 | 100 is full visibility of the first image. Any amount in-between is a left/right cutoff at the
33 | percentage of the slider.
34 |
35 |
44 |
45 |
46 |
47 |
176 |
--------------------------------------------------------------------------------
/src/lib/components/file-dropzone.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
20 |
23 | Pilih gambar atau drag and drop
24 | Hanya JPEG, PNG, dan WebP (Maksimal 5MB)
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/components/footer.svelte:
--------------------------------------------------------------------------------
1 |
45 |
46 |
50 |
51 |
Copyright © 2024 Taufik Hidayat
52 |
•
53 |
Privacy Policy
54 |
55 |
56 | {#each socialMedias as { icon, url }}
57 |
63 |
64 |
65 | {/each}
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/lib/components/header.svelte:
--------------------------------------------------------------------------------
1 |
60 |
61 |
120 |
121 |
122 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/src/lib/components/heading.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
{title}
16 | {#if subtitle}
17 | {subtitle}
18 | {/if}
19 |
20 |
--------------------------------------------------------------------------------
/src/lib/components/privacy-policy.svx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Privacy Policy
3 | description: Privacy Policy for Remove Biji
4 | ---
5 |
6 | # Privacy Policy for Remove Biji
7 |
8 | _Last Updated: November 21, 2024_
9 |
10 | ## Introduction
11 |
12 | Welcome to Remove Biji ("we," "our," or "us"). We are committed to protecting your privacy and ensuring you have a positive experience when using our background removal service. This privacy policy explains how we collect, use, and safeguard your information when you use our web-based application.
13 |
14 | ## Information We Collect
15 |
16 | ### Account Information
17 |
18 | When you use Remove Biji, we collect the following information through Google Sign-In:
19 |
20 | - Google ID
21 | - Email address
22 | - Name
23 | - Profile picture
24 |
25 | ### Technical Information
26 |
27 | We automatically collect certain information about your device when you access our service, including:
28 |
29 | - Browser type and version
30 | - Operating system
31 | - IP address
32 | - Access timestamps
33 | - Browser settings
34 |
35 | ## How We Use Your Information
36 |
37 | We use your personal information for the following purposes:
38 |
39 | - To create and manage your user account
40 | - To provide our background removal service
41 | - To authenticate your identity
42 | - To improve and optimize our application
43 | - To protect against fraud and unauthorized access
44 |
45 | ## Image Processing and Storage
46 |
47 | We want to be transparent about how we handle your images:
48 |
49 | - We process your images in real-time for background removal
50 | - We DO NOT store or retain any uploaded images
51 | - Images are automatically deleted after processing
52 | - We DO NOT use your images for any other purposes
53 |
54 | ## Payment Processing
55 |
56 | For payment processing:
57 |
58 | - We use secure third-party payment gateway
59 | - We DO NOT collect or store any payment information
60 | - All payment transactions are encrypted and processed through our payment gateway partner
61 | - Please refer to the payment gateway's privacy policy for information about how they handle your payment data
62 |
63 | ## Data Sharing and Disclosure
64 |
65 | We do not sell, trade, or rent your personal information to third parties. We may share your information only in the following circumstances:
66 |
67 | - When required by law
68 | - To protect our rights and property
69 | - To prevent fraud or illegal activities
70 | - With your explicit consent
71 |
72 | ## Data Security
73 |
74 | We implement appropriate technical and organizational security measures to protect your personal information, including:
75 |
76 | - Secure server infrastructure
77 | - Access controls and authentication measures
78 |
79 | ## Your Rights
80 |
81 | You have the right to:
82 |
83 | - Access your personal information
84 | - Correct inaccurate or incomplete information
85 | - Request deletion of your account and associated data
86 | - Export your data
87 | - Withdraw consent at any time
88 |
89 | ## Children's Privacy
90 |
91 | Remove Biji is intended for general audiences and does not knowingly collect personal information from children under 13. If we learn that we have collected personal information from a child under 13, we will take steps to delete such information.
92 |
93 | ## Changes to This Policy
94 |
95 | We may update this privacy policy from time to time. We will notify you of any changes by:
96 |
97 | - Posting the new privacy policy on our website
98 | - Updating the "Last Updated" date at the top of this policy
99 |
100 | ## Contact Us
101 |
102 | If you have any questions about this privacy policy or our practices, please contact us at:
103 |
104 | - Email: tfkhdyt@proton.me
105 | - Facebook: https://www.facebook.com/tfkhdyt142
106 |
107 | ## Compliance
108 |
109 | This privacy policy complies with applicable data protection laws and regulations, including:
110 |
111 | - General Data Protection Regulation (GDPR)
112 | - California Consumer Privacy Act (CCPA)
113 | - Personal Information Protection and Electronic Documents Act (PIPEDA)
114 |
--------------------------------------------------------------------------------
/src/lib/components/sidebar.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 | {#if isOpen}
34 |
35 |
36 |
37 |
42 | {/if}
43 |
44 |
45 |
51 |
52 |
53 |
54 |
106 |
107 |
--------------------------------------------------------------------------------
/src/lib/constants/price.ts:
--------------------------------------------------------------------------------
1 | type Package = {
2 | value: number;
3 | totalPrice: number;
4 | pricePerImage: number;
5 | };
6 |
7 | export const packages: Package[] = [
8 | {
9 | value: 10,
10 | totalPrice: 12_250,
11 | pricePerImage: 1_225
12 | },
13 | {
14 | value: 50,
15 | totalPrice: 53_250,
16 | pricePerImage: 1_065
17 | },
18 | {
19 | value: 100,
20 | totalPrice: 92_600,
21 | pricePerImage: 926
22 | },
23 | {
24 | value: 200,
25 | totalPrice: 161_000,
26 | pricePerImage: 805
27 | },
28 | {
29 | value: 500,
30 | totalPrice: 350_000,
31 | pricePerImage: 700
32 | }
33 | ];
34 |
--------------------------------------------------------------------------------
/src/lib/downloads.ts:
--------------------------------------------------------------------------------
1 | import { withCatch } from '@tfkhdyt/with-catch';
2 | import JSZip from 'jszip';
3 | import { v4 as uuidv4 } from 'uuid';
4 |
5 | export function downloadBlob(url: string, fileName = 'download'): void {
6 | // Create download link
7 | const link = document.createElement('a');
8 | link.href = url;
9 | link.download = fileName;
10 |
11 | // Trigger download
12 | link.click();
13 | }
14 |
15 | export async function downloadAll(outputs: Blob[]) {
16 | const zip = new JSZip();
17 |
18 | for (const file of outputs) {
19 | zip.file(`remove-biji-${uuidv4()}.png`, file); // adds the image file to the zip file
20 | }
21 |
22 | const [err, zipData] = await withCatch(
23 | zip.generateAsync({
24 | type: 'blob',
25 | streamFiles: true
26 | })
27 | );
28 | if (err) {
29 | console.error('Error generating zip file:', err);
30 | throw new Error('Error generating zip file', { cause: err });
31 | }
32 |
33 | const link = document.createElement('a');
34 | link.href = window.URL.createObjectURL(zipData);
35 | link.download = `remove-biji-batch-${uuidv4()}.zip`;
36 | link.click();
37 |
38 | window.URL.revokeObjectURL(link.href);
39 | }
40 |
--------------------------------------------------------------------------------
/src/lib/icons/bg-replace-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/biji-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/bill-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/icons/buy-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
14 |
18 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/lib/icons/circle-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/close-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/download-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/lib/icons/download-multiple-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/lib/icons/facebook-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/file-upload-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/lib/icons/github-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/google-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/src/lib/icons/hamburger-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
15 |
16 |
--------------------------------------------------------------------------------
/src/lib/icons/home-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/instagram-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/invoice-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/linkedin-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/loading-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
22 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/lib/icons/logout-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/money-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
16 |
17 |
--------------------------------------------------------------------------------
/src/lib/icons/reset-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/trash-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/twitter-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/view-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
16 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/lib/icons/youtube-icon.svelte:
--------------------------------------------------------------------------------
1 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/image.ts:
--------------------------------------------------------------------------------
1 | import { withCatch } from '@tfkhdyt/with-catch';
2 | import sharp from 'sharp';
3 |
4 | const getOrientation = async (blob: Blob): Promise => {
5 | const buffer = await blob.arrayBuffer();
6 | const [err, metadata] = await withCatch(sharp(Buffer.from(buffer)).metadata());
7 | if (err) {
8 | console.error('Error reading orientation:', err);
9 | throw new Error('Error reading orientation', { cause: err });
10 | }
11 |
12 | return metadata.orientation || 1; // Default to 1 (normal orientation)
13 | };
14 |
15 | export const rotateImageToMatch = async (inputBlob: Blob, outputBlob: Blob): Promise => {
16 | const inputOrientation = await getOrientation(inputBlob);
17 | const outputOrientation = await getOrientation(outputBlob);
18 |
19 | if (inputOrientation === outputOrientation) {
20 | return outputBlob; // Return the same Blob if no rotation is needed
21 | }
22 |
23 | const orientationRotationMap: Record = {
24 | 1: 0, // Normal
25 | 3: 180, // Upside Down
26 | 6: 90, // Rotated 90° Clockwise
27 | 8: -90 // Rotated 90° Counterclockwise
28 | };
29 | // Compute the relative rotation
30 | const outputRotation = orientationRotationMap[outputOrientation] || 0;
31 | const inputRotation = orientationRotationMap[inputOrientation] || 0;
32 |
33 | const rotationNeeded = inputRotation - outputRotation;
34 |
35 | // Convert Blob to Bu ffer
36 | const buffer = await outputBlob.arrayBuffer();
37 |
38 | // Apply rotation using sharp
39 | const [err, rotatedBuffer] = await withCatch(
40 | sharp(Buffer.from(buffer)).rotate(rotationNeeded).toBuffer()
41 | );
42 | if (err) {
43 | console.error('Error rotating image:', err);
44 | throw new Error('Error rotating image', { cause: err });
45 | }
46 |
47 | // Convert the rotated Buffer back to a Blob
48 | const rotatedBlob = new Blob([rotatedBuffer], { type: outputBlob.type });
49 |
50 | return rotatedBlob;
51 | };
52 |
--------------------------------------------------------------------------------
/src/lib/rate-limiter.ts:
--------------------------------------------------------------------------------
1 | import { RateLimiter } from 'sveltekit-rate-limiter/server';
2 |
3 | export const guestLimiter = new RateLimiter({
4 | IPUA: [3, 'd']
5 | });
6 |
--------------------------------------------------------------------------------
/src/lib/remove-bg.ts:
--------------------------------------------------------------------------------
1 | import { withCatch } from '@tfkhdyt/with-catch';
2 | import { imageSchema } from './schema/image-schema';
3 |
4 | export const removeBg = async (images: FileList | undefined) => {
5 | if (!images) throw new Error('Tidak ada gambar yang dikirim');
6 |
7 | const payload = imageSchema.array().safeParse(Array.from(images));
8 | if (!payload.success) {
9 | throw payload.error;
10 | }
11 |
12 | const form = new FormData();
13 | for (const image of payload.data) {
14 | form.append('image', image);
15 | }
16 |
17 | const [err, resp] = await withCatch(
18 | fetch('/api/remove-biji', {
19 | method: 'POST',
20 | body: form
21 | })
22 | );
23 | if (err) {
24 | throw err;
25 | }
26 |
27 | const [errData, data] = await withCatch(resp.json());
28 | if (errData) {
29 | throw errData;
30 | }
31 |
32 | if (!resp.ok) {
33 | throw new Error(data.message as string);
34 | }
35 |
36 | return data as { images: string[]; creditsAmount: number | undefined };
37 | };
38 |
--------------------------------------------------------------------------------
/src/lib/schema/image-schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | const supportedImageTypes = ['image/jpeg', 'image/png', 'image/webp'];
4 | const maximumImageSize = 5_000_000;
5 |
6 | export const imageSchema = z
7 | .instanceof(File)
8 | .refine((file) => supportedImageTypes.includes(file.type), 'Tipe gambar tidak didukung')
9 | .refine(
10 | (file) => file.size <= maximumImageSize,
11 | 'Ukuran gambar terlalu besar, tidak boleh lebih dari 5MB'
12 | );
13 |
--------------------------------------------------------------------------------
/src/lib/schema/payment-schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const createPaymentSchema = z.object({
4 | package: z.number()
5 | });
6 |
--------------------------------------------------------------------------------
/src/lib/server/auth.ts:
--------------------------------------------------------------------------------
1 | import { db } from '$lib/server/db';
2 | import * as table from '$lib/server/db/schema';
3 | import { sha256 } from '@oslojs/crypto/sha2';
4 | import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
5 | import type { RequestEvent } from '@sveltejs/kit';
6 | import { withCatch } from '@tfkhdyt/with-catch';
7 | import { eq } from 'drizzle-orm';
8 |
9 | const DAY_IN_MS = 1000 * 60 * 60 * 24;
10 |
11 | export const sessionCookieName = 'auth-session';
12 |
13 | export function generateSessionToken() {
14 | const bytes = crypto.getRandomValues(new Uint8Array(18));
15 | const token = encodeBase64url(bytes);
16 | return token;
17 | }
18 |
19 | export async function createSession(token: string, userId: number) {
20 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
21 | const session: table.Session = {
22 | id: sessionId,
23 | userId,
24 | expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
25 | };
26 |
27 | const [err] = await withCatch(db.insert(table.session).values(session));
28 | if (err) {
29 | throw new Error('Error creating session', { cause: err });
30 | }
31 |
32 | return session;
33 | }
34 |
35 | export async function validateSessionToken(token: string) {
36 | const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
37 |
38 | const [result] = await db
39 | .select({
40 | user: {
41 | id: table.user.id,
42 | name: table.user.name,
43 | email: table.user.email,
44 | picture: table.user.picture,
45 | creditsAmount: table.credits.amount
46 | },
47 | session: table.session
48 | })
49 | .from(table.session)
50 | .innerJoin(table.user, eq(table.session.userId, table.user.id))
51 | .leftJoin(table.credits, eq(table.user.id, table.credits.id))
52 | .where(eq(table.session.id, sessionId));
53 |
54 | if (!result) {
55 | return { session: null, user: null };
56 | }
57 | const { session, user } = result;
58 |
59 | const sessionExpired = Date.now() >= session.expiresAt.getTime();
60 | if (sessionExpired) {
61 | const [err] = await withCatch(db.delete(table.session).where(eq(table.session.id, session.id)));
62 | if (err) {
63 | throw new Error('Error deleting session', { cause: err });
64 | }
65 |
66 | return { session: null, user: null };
67 | }
68 |
69 | const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
70 | if (renewSession) {
71 | session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
72 |
73 | const [err] = await withCatch(
74 | db
75 | .update(table.session)
76 | .set({ expiresAt: session.expiresAt })
77 | .where(eq(table.session.id, session.id))
78 | );
79 | if (err) {
80 | throw new Error('Error updating session', { cause: err });
81 | }
82 | }
83 |
84 | return { session, user };
85 | }
86 |
87 | export type SessionValidationResult = Awaited>;
88 |
89 | export async function invalidateSession(sessionId: string) {
90 | const [err] = await withCatch(db.delete(table.session).where(eq(table.session.id, sessionId)));
91 | if (err) {
92 | throw new Error('Error invalidate session', { cause: err });
93 | }
94 | }
95 |
96 | export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) {
97 | event.cookies.set(sessionCookieName, token, {
98 | expires: expiresAt,
99 | path: '/'
100 | });
101 | }
102 |
103 | export function deleteSessionTokenCookie(event: RequestEvent) {
104 | event.cookies.delete(sessionCookieName, {
105 | path: '/'
106 | });
107 | }
108 |
--------------------------------------------------------------------------------
/src/lib/server/db/index.ts:
--------------------------------------------------------------------------------
1 | import { drizzle } from 'drizzle-orm/postgres-js';
2 | import postgres from 'postgres';
3 | import { env } from '$env/dynamic/private';
4 | import * as schema from './schema';
5 |
6 | if (!env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
7 |
8 | const client = postgres(env.DATABASE_URL);
9 | export const db = drizzle(client, { casing: 'snake_case', schema });
10 |
--------------------------------------------------------------------------------
/src/lib/server/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { relations } from 'drizzle-orm';
2 | import { integer, pgTable, serial, text, timestamp, varchar } from 'drizzle-orm/pg-core';
3 |
4 | export const user = pgTable('users', {
5 | id: serial().primaryKey(),
6 | googleId: varchar().notNull().unique(),
7 | email: varchar().notNull().unique(),
8 | name: varchar(),
9 | picture: text()
10 | // passwordHash: text('password_hash').notNull()
11 | });
12 |
13 | export const userRelations = relations(user, ({ one, many }) => ({
14 | creditsAmount: one(credits),
15 | sessions: many(session),
16 | invoices: many(invoices)
17 | }));
18 |
19 | export const session = pgTable('sessions', {
20 | id: text().primaryKey(),
21 | userId: integer()
22 | .notNull()
23 | .references(() => user.id),
24 | expiresAt: timestamp({ withTimezone: true, mode: 'date' }).notNull()
25 | });
26 |
27 | export const credits = pgTable('credits', {
28 | id: integer()
29 | .primaryKey()
30 | .references(() => user.id),
31 | amount: integer().notNull().default(5)
32 | });
33 |
34 | export const invoices = pgTable('invoices', {
35 | id: varchar().primaryKey(),
36 | userId: integer()
37 | .notNull()
38 | .references(() => user.id),
39 | status: varchar().notNull().default('UNPAID'),
40 | expiredTime: timestamp({ withTimezone: true, mode: 'date' }).notNull(),
41 | paidAt: timestamp({ withTimezone: true, mode: 'date' }),
42 | package: integer().notNull(),
43 | amount: integer().notNull(),
44 | checkoutUrl: text(),
45 | createdAt: timestamp({ withTimezone: true, mode: 'date' }).defaultNow()
46 | });
47 |
48 | export type Session = typeof session.$inferSelect;
49 |
50 | export type User = typeof user.$inferSelect & {
51 | creditsAmount: number | null;
52 | };
53 |
--------------------------------------------------------------------------------
/src/lib/server/oauth.ts:
--------------------------------------------------------------------------------
1 | import { Google } from 'arctic';
2 | import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, BASE_URL } from '$env/static/private';
3 |
4 | export const google = new Google(
5 | GOOGLE_CLIENT_ID,
6 | GOOGLE_CLIENT_SECRET,
7 | `${BASE_URL}/auth/login/google/callback`
8 | );
9 |
--------------------------------------------------------------------------------
/src/lib/server/payment.ts:
--------------------------------------------------------------------------------
1 | import { packages } from '$lib/constants/price';
2 | import { v4 as uuidv4 } from 'uuid';
3 | import crypto from 'node:crypto';
4 | import { env } from '$env/dynamic/private';
5 | import type { TripayInvoiceResponse } from '$lib/types/tripay';
6 | import { withCatch } from '@tfkhdyt/with-catch';
7 | import { db } from './db';
8 | import * as table from '$lib/server/db/schema';
9 |
10 | const tripayUrl =
11 | env.NODE_ENV === 'production'
12 | ? 'https://tripay.co.id/api/transaction/create'
13 | : 'https://tripay.co.id/api-sandbox/transaction/create';
14 | const apiKey = env.TRIPAY_API_KEY;
15 | const privateKey = env.TRIPAY_PRIVATE_KEY;
16 | const merchant_code = env.TRIPAY_MERCHANT_CODE;
17 |
18 | type UserData = {
19 | id: number;
20 | name: string | null;
21 | email: string | null;
22 | };
23 |
24 | export async function createInvoice(packageId: number, userData: UserData) {
25 | const pkg = packages.find((p) => p.value === packageId);
26 |
27 | const merchant_ref = `INV-${uuidv4()}`;
28 | const signature = crypto
29 | .createHmac('sha256', privateKey)
30 | .update(merchant_code + merchant_ref + pkg?.totalPrice)
31 | .digest('hex');
32 |
33 | const expiry = Math.floor(Date.now() / 1000) + 60 * 60; // 1 jam
34 |
35 | const payload = {
36 | method: 'QRIS2',
37 | merchant_ref,
38 | amount: pkg?.totalPrice,
39 | customer_name: userData.name,
40 | customer_email: userData.email,
41 | // customer_phone: '081234567890',
42 | order_items: [
43 | {
44 | sku: 'BIJI',
45 | name: `Saldo Remove Biji`,
46 | price: pkg?.pricePerImage,
47 | quantity: pkg?.value
48 | // product_url: 'https://tokokamu.com/product/nama-produk-1',
49 | // image_url: 'https://tokokamu.com/product/nama-produk-1.jpg'
50 | }
51 | ],
52 | return_url: `${env.BASE_URL}/invoices`,
53 | callback_url: `${env.BASE_URL}/api/payment/callback`,
54 | expired_time: expiry,
55 | signature
56 | };
57 |
58 | const [err, response] = await withCatch(
59 | fetch(tripayUrl, {
60 | method: 'POST',
61 | body: JSON.stringify(payload),
62 | headers: {
63 | 'Content-Type': 'application/json',
64 | Authorization: 'Bearer ' + apiKey
65 | }
66 | })
67 | );
68 | if (err) {
69 | throw new Error('Error creating payment', { cause: err });
70 | }
71 |
72 | const data = await response.json();
73 | if (!response.ok) {
74 | throw new Error(data.message);
75 | }
76 |
77 | const invoice: TripayInvoiceResponse = data.data;
78 |
79 | const [errInsert] = await withCatch(
80 | db.insert(table.invoices).values({
81 | id: invoice.merchant_ref,
82 | userId: userData.id,
83 | status: invoice.status,
84 | expiredTime: new Date(invoice.expired_time * 1000),
85 | package: packageId,
86 | amount: invoice.amount,
87 | checkoutUrl: invoice.checkout_url
88 | })
89 | );
90 | if (errInsert) {
91 | throw new Error('Error creating invoice', { cause: errInsert });
92 | }
93 |
94 | return invoice.checkout_url;
95 | }
96 |
--------------------------------------------------------------------------------
/src/lib/server/user.ts:
--------------------------------------------------------------------------------
1 | import { withCatch } from '@tfkhdyt/with-catch';
2 | import { eq } from 'drizzle-orm';
3 | import { db } from './db';
4 | import * as table from './db/schema';
5 |
6 | export async function createUser(googleId: string, email: string, name: string, picture: string) {
7 | const [user] = await db
8 | .insert(table.user)
9 | .values({
10 | googleId,
11 | email,
12 | name,
13 | picture
14 | })
15 | .returning();
16 | if (!user) {
17 | throw new Error('Failed to create user');
18 | }
19 |
20 | const [err] = await withCatch(db.insert(table.credits).values({ id: user.id, amount: 5 }));
21 | if (err) {
22 | throw new Error('Failed to create credits', { cause: err });
23 | }
24 |
25 | return user;
26 | }
27 |
28 | export async function getUserFromGoogleId(googleId: string) {
29 | const [user] = await db.select().from(table.user).where(eq(table.user.googleId, googleId));
30 | if (!user) {
31 | return null;
32 | }
33 |
34 | return user;
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib/stores/credit.svelte.ts:
--------------------------------------------------------------------------------
1 | let amount = $state(null);
2 |
3 | export function getCreditsStore() {
4 | function setAmount(newAmount: number) {
5 | amount = newAmount;
6 | }
7 |
8 | return {
9 | get amount() {
10 | return amount;
11 | },
12 | setAmount
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/types/tripay.ts:
--------------------------------------------------------------------------------
1 | export type TripayInvoiceResponse = {
2 | reference: string;
3 | merchant_ref: string;
4 | payment_selection_type: string;
5 | payment_method: string;
6 | payment_name: string;
7 | customer_name: string;
8 | customer_email: string;
9 | customer_phone: null;
10 | callback_url: string;
11 | return_url: string;
12 | amount: number;
13 | fee_merchant: number;
14 | fee_customer: number;
15 | total_fee: number;
16 | amount_received: number;
17 | pay_code: null;
18 | pay_url: null;
19 | checkout_url: string;
20 | status: string;
21 | expired_time: number;
22 | qr_string: string;
23 | qr_url: string;
24 | };
25 |
26 | export type TripayCallbackResponse = {
27 | reference: string;
28 | merchant_ref: string;
29 | payment_method: string;
30 | payment_method_code: string;
31 | total_amount: number;
32 | fee_merchant: number;
33 | fee_customer: number;
34 | total_fee: number;
35 | amount_received: number;
36 | is_closed_payment: number;
37 | status: string;
38 | paid_at: number;
39 | note: null;
40 | };
41 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | // place files you want to import through the `$lib` alias in this folder.
2 | export const removeFileFromList = (fileToRemove: File, images: FileList) => {
3 | // Convert FileList to Array
4 | const filesArray: File[] = Array.from(images);
5 |
6 | // Find index of file to remove
7 | const index: number = filesArray.findIndex((file) => file === fileToRemove);
8 |
9 | // Remove the file if found
10 | if (index > -1) {
11 | filesArray.splice(index, 1);
12 | }
13 |
14 | // Create a new DataTransfer object
15 | const dt: DataTransfer = new DataTransfer();
16 |
17 | // Add remaining files to DataTransfer object
18 | filesArray.forEach((file) => dt.items.add(file));
19 |
20 | // Return new FileList
21 | return dt.files;
22 | };
23 |
24 | export function base64ToBlob(base64: string) {
25 | const byteCharacters = atob(base64);
26 | const byteArrays = [];
27 |
28 | for (let i = 0; i < byteCharacters.length; i++) {
29 | byteArrays.push(byteCharacters.charCodeAt(i));
30 | }
31 |
32 | const byteArray = new Uint8Array(byteArrays);
33 | return new Blob([byteArray], { type: 'image/png' });
34 | }
35 |
36 | const formatter = new Intl.NumberFormat('id-ID', {
37 | style: 'currency',
38 | currency: 'IDR',
39 | trailingZeroDisplay: 'stripIfInteger'
40 | });
41 |
42 | export function formatRupiah(number: number) {
43 | return formatter.format(number);
44 | }
45 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutServerLoad } from './$types';
2 |
3 | export const load: LayoutServerLoad = async ({ parent }) => {
4 | const data = await parent();
5 |
6 | return data;
7 | };
8 |
--------------------------------------------------------------------------------
/src/routes/(app)/+layout.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 | {@render children()}
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/routes/(app)/app/+page.server.ts:
--------------------------------------------------------------------------------
1 | import type { PageServerLoad } from './$types';
2 |
3 | export const load: PageServerLoad = async ({ parent }) => {
4 | const data = await parent();
5 | return data;
6 | };
7 |
--------------------------------------------------------------------------------
/src/routes/(app)/app/+page.svelte:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 | {isLoading ? '[Processing...] ' : ''}Remove Biji - Hilangkan bijimu dengan mudah
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/routes/(app)/invoices/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from '@sveltejs/kit';
2 | import type { PageServerLoad } from './$types';
3 | import { db } from '$lib/server/db';
4 | import * as table from '$lib/server/db/schema';
5 | import { desc, eq } from 'drizzle-orm';
6 | import { withCatch } from '@tfkhdyt/with-catch';
7 |
8 | export const load: PageServerLoad = async ({ parent }) => {
9 | const data = await parent();
10 | if (!data.user) {
11 | return redirect(302, '/');
12 | }
13 |
14 | const [err, invoices] = await withCatch(
15 | db
16 | .select()
17 | .from(table.invoices)
18 | .where(eq(table.invoices.userId, data.user.id))
19 | .orderBy(desc(table.invoices.createdAt))
20 | );
21 | if (err) {
22 | throw new Error('Error loading invoices', { cause: err });
23 | }
24 |
25 | return { user: data.user, invoices };
26 | };
27 |
--------------------------------------------------------------------------------
/src/routes/(app)/invoices/+page.svelte:
--------------------------------------------------------------------------------
1 |
20 |
21 |
22 | {title}
23 |
24 |
25 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | Paket
53 | Total Harga
54 | Status
55 | Tanggal Dibuat
56 | Tanggal Dibayar
57 |
58 |
59 |
60 | {#each data.invoices as invoice (invoice.id)}
61 |
62 |
63 |
64 | {invoice.package}
66 | {formatRupiah(invoice.amount)}
67 |
68 | {#if invoice.status === 'PAID'}
69 | {invoice.status}
70 | {:else if invoice.status === 'UNPAID'}
71 | {invoice.status}
72 | {:else}
73 | {invoice.status}
74 | {/if}
75 |
76 | {!!invoice.createdAt &&
78 | format(invoice.createdAt, 'dd MMM yyyy, HH:mm', { locale: id })}
80 |
81 | {#if invoice.paidAt}
82 |
83 | {format(invoice.paidAt, 'dd MMM yyyy, HH:mm', { locale: id })}
84 |
85 | {:else if invoice.status === 'UNPAID'}
86 |
92 | {:else}
93 | -
94 | {/if}
95 |
96 |
97 | {/each}
98 |
99 |
100 |
101 |
--------------------------------------------------------------------------------
/src/routes/(app)/pricing/+page.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 | {title}
14 |
15 |
16 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Jumlah biji*
44 | Harga**
45 | Harga per biji
46 |
47 |
48 |
49 |
50 |
51 |
52 | 3 / hari***
53 |
54 | Gratis
55 | Gratis
56 |
57 | {#each packages as row}
58 |
59 |
60 |
61 | {row.value}
62 |
63 | {formatRupiah(row.totalPrice)}
64 | {formatRupiah(row.pricePerImage)} / biji
65 |
66 | {/each}
67 |
68 |
74 |
75 |
76 |
77 |
78 |
79 | *1
80 | = 1 gambar
81 |
82 |
83 | **Belum termasuk biaya admin
84 |
85 |
***Non-akumulatif
86 |
87 |
--------------------------------------------------------------------------------
/src/routes/(app)/privacy-policy/+page.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 | Privacy Policy - Remove Biji
11 |
12 |
13 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/routes/(app)/topup/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { createPaymentSchema } from '$lib/schema/payment-schema';
2 | import { createInvoice } from '$lib/server/payment';
3 | import { redirect } from '@sveltejs/kit';
4 | import { withCatch } from '@tfkhdyt/with-catch';
5 | import { fail, message, superValidate } from 'sveltekit-superforms';
6 | import { zod } from 'sveltekit-superforms/adapters';
7 | import type { PageServerLoad } from './$types';
8 |
9 | export const load: PageServerLoad = async ({ parent }) => {
10 | const data = await parent();
11 | if (!data.user) {
12 | return redirect(302, '/auth/login/google');
13 | }
14 |
15 | const form = await superValidate(zod(createPaymentSchema));
16 |
17 | return { data, form };
18 | };
19 |
20 | export const actions = {
21 | default: async ({ locals, request }) => {
22 | const form = await superValidate(request, zod(createPaymentSchema));
23 | if (!form.valid) {
24 | return fail(400, { form });
25 | }
26 |
27 | if (!locals.user) {
28 | return message(form, { type: 'error', message: 'Kamu harus login terlebih dahulu' });
29 | }
30 |
31 | const [err, checkoutUrl] = await withCatch(
32 | createInvoice(form.data.package, {
33 | id: locals.user.id,
34 | name: locals.user.name,
35 | email: locals.user.email
36 | })
37 | );
38 | if (err) {
39 | return message(form, { type: 'error', message: err.message });
40 | }
41 |
42 | return redirect(302, checkoutUrl);
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/src/routes/(app)/topup/+page.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 | {title}
32 |
33 |
34 |
52 |
53 |
54 |
55 |
101 |
--------------------------------------------------------------------------------
/src/routes/(landing-page)/+layout.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {@render children()}
22 |
23 |
24 |
29 |
30 |
--------------------------------------------------------------------------------
/src/routes/(landing-page)/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { redirect } from '@sveltejs/kit';
2 | import type { PageServerLoad } from './$types';
3 |
4 | export const load: PageServerLoad = async ({ parent }) => {
5 | const data = await parent();
6 | if (data.user) {
7 | return redirect(301, '/app');
8 | }
9 |
10 | return {};
11 | };
12 |
--------------------------------------------------------------------------------
/src/routes/(landing-page)/+page.svelte:
--------------------------------------------------------------------------------
1 |
68 |
69 |
70 | Remove Biji - Hilangkan bijimu dengan mudah
71 |
72 |
73 |
74 |
90 |
91 |
94 |
95 |
107 |
108 |
109 | {#each features as feature}
110 |
113 |
114 |
{feature.title}
115 |
{feature.description}
116 |
117 | {/each}
118 |
119 |
120 |
121 |
122 |
123 |
Cara Kerja
124 |
125 | {#each caraKerja as cara, i}
126 |
129 |
132 |
133 |
134 |
{i + 1}. {cara.title}
135 |
{cara.description}
136 |
137 | {/each}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
Frequently Asked Questions
145 |
146 | {#each faq as f}
147 |
148 |
149 | {f.question}
150 |
151 |
152 |
153 | {f.answer}
154 |
155 |
156 |
157 | {/each}
158 |
159 |
160 |
161 |
162 |
163 | Siap Mencoba?
164 |
165 | Dapatkan 5 saldo gratis untuk member baru
166 |
167 | Coba Sekarang
168 |
169 |
170 |
--------------------------------------------------------------------------------
/src/routes/+error.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
{$page.status}
8 |
{$page.error?.message}
9 |
10 |
11 |
12 | Kembali ke halaman utama
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/routes/+layout.server.ts:
--------------------------------------------------------------------------------
1 | import type { LayoutServerLoad } from './$types';
2 |
3 | export const load: LayoutServerLoad = async ({ locals }) => {
4 | return {
5 | user: locals.user
6 | };
7 | };
8 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
31 |
32 |
33 |
38 |
39 |
40 |
58 |
59 | {@render children()}
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/routes/api/payment/callback/+server.ts:
--------------------------------------------------------------------------------
1 | import { env } from '$env/dynamic/private';
2 | import { error, json } from '@sveltejs/kit';
3 | import type { RequestHandler } from './$types';
4 | import crypto from 'node:crypto';
5 | import { db } from '$lib/server/db';
6 | import * as table from '$lib/server/db/schema';
7 | import type { TripayCallbackResponse } from '$lib/types/tripay';
8 | import { eq, sql } from 'drizzle-orm';
9 | import { withCatch } from '@tfkhdyt/with-catch';
10 |
11 | export const POST: RequestHandler = async ({ request }) => {
12 | const data: TripayCallbackResponse = await request.json();
13 | const signature = crypto
14 | .createHmac('sha256', env.TRIPAY_PRIVATE_KEY)
15 | .update(JSON.stringify(data))
16 | .digest('hex');
17 | const signatureFromTripay = request.headers.get('X-Callback-Signature');
18 |
19 | if (signature !== signatureFromTripay) {
20 | return error(401, 'Signature tidak valid');
21 | }
22 |
23 | await db.transaction(async (trx) => {
24 | const [invoice] = await trx
25 | .update(table.invoices)
26 | .set({
27 | status: data.status,
28 | paidAt: data.status === 'PAID' ? new Date(data.paid_at * 1000) : undefined
29 | })
30 | .where(eq(table.invoices.id, data.merchant_ref))
31 | .returning({ package: table.invoices.package, userId: table.invoices.userId });
32 |
33 | if (data.status === 'PAID') {
34 | const [err] = await withCatch(
35 | trx
36 | .update(table.credits)
37 | .set({
38 | amount: sql`${table.credits.amount} + ${invoice.package}`
39 | })
40 | .where(eq(table.credits.id, invoice.userId))
41 | );
42 | if (err) {
43 | throw new Error('Error updating credits', { cause: err });
44 | }
45 | }
46 | });
47 |
48 | return json({ success: true });
49 | };
50 |
--------------------------------------------------------------------------------
/src/routes/api/remove-biji/+server.ts:
--------------------------------------------------------------------------------
1 | import { guestLimiter } from '$lib/rate-limiter';
2 | import { error } from '@sveltejs/kit';
3 | import type { RequestHandler } from './$types';
4 | import { removeBackground } from '@imgly/background-removal-node';
5 | import { env } from '$env/dynamic/private';
6 | import { rotateImageToMatch } from '$lib/image';
7 | import { withCatch } from '@tfkhdyt/with-catch';
8 | import { db } from '$lib/server/db';
9 | import * as table from '$lib/server/db/schema';
10 | import { eq, sql } from 'drizzle-orm';
11 |
12 | export const POST: RequestHandler = async (event) => {
13 | const { request, locals } = event;
14 |
15 | const formData = await request.formData();
16 | const files = formData.getAll('image');
17 |
18 | const isLimited = await guestLimiter.isLimited(event);
19 |
20 | if (env.NODE_ENV === 'production') {
21 | if (!locals.user && isLimited) {
22 | throw error(429, 'Kuota free tier-mu sudah habis, daftar untuk mendapatkan 5 saldo gratis');
23 | }
24 |
25 | if (locals.user?.creditsAmount === 0 && isLimited) {
26 | throw error(
27 | 429,
28 | 'Kamu hanya mendapatkan kuota free tier 3 kali per hari, silakan topup terlebih dahulu'
29 | );
30 | }
31 | }
32 |
33 | if (locals.user?.creditsAmount && files.length > locals.user.creditsAmount) {
34 | throw error(429, 'Saldomu tidak mencukupi, silakan topup terlebih dahulu');
35 | }
36 |
37 | const result = await Promise.allSettled(
38 | files.map(async (image) => {
39 | const [err, output] = await withCatch(removeBackground(image)); // Process the image
40 | if (err) {
41 | console.error('Error removing background:', err);
42 | throw new Error('Error removing background', { cause: err });
43 | }
44 | const rotatedOutput = await rotateImageToMatch(
45 | new Blob([image], { type: 'image/png' }),
46 | output!
47 | );
48 |
49 | const base64 = await blobToBase64(rotatedOutput); // Convert the blob to base64
50 | return base64;
51 | })
52 | );
53 |
54 | const output: string[] = [];
55 | for (const item of result) {
56 | if (item.status === 'fulfilled') {
57 | output.push(item.value);
58 | }
59 | }
60 |
61 | let creditsAmount = undefined;
62 | if (locals.user && locals.user.creditsAmount && locals.user.creditsAmount > 0) {
63 | const [row] = await db
64 | .update(table.credits)
65 | .set({
66 | amount: sql`${table.credits.amount} - ${files.length}`
67 | })
68 | .where(eq(table.credits.id, locals.user.id))
69 | .returning({ amount: table.credits.amount });
70 | if (!row) {
71 | throw new Error('Error updating credits');
72 | }
73 |
74 | creditsAmount = row.amount;
75 | }
76 |
77 | return new Response(JSON.stringify({ images: output, creditsAmount }), {
78 | headers: { 'Content-Type': 'application/json' }
79 | });
80 | };
81 |
82 | async function blobToBase64(blob: Blob) {
83 | const arrayBuffer = await blob.arrayBuffer();
84 | const base64 = Buffer.from(arrayBuffer).toString('base64');
85 | return base64;
86 | }
87 |
--------------------------------------------------------------------------------
/src/routes/auth/login/google/+server.ts:
--------------------------------------------------------------------------------
1 | import { google } from '$lib/server/oauth';
2 | import { generateCodeVerifier, generateState } from 'arctic';
3 |
4 | import type { RequestEvent } from './$types';
5 |
6 | export function GET(event: RequestEvent): Response {
7 | const state = generateState();
8 | const codeVerifier = generateCodeVerifier();
9 | const url = google.createAuthorizationURL(state, codeVerifier, ['openid', 'profile', 'email']);
10 |
11 | event.cookies.set('google_oauth_state', state, {
12 | httpOnly: true,
13 | maxAge: 60 * 10,
14 | secure: import.meta.env.PROD,
15 | path: '/',
16 | sameSite: 'lax'
17 | });
18 | event.cookies.set('google_code_verifier', codeVerifier, {
19 | httpOnly: true,
20 | maxAge: 60 * 10,
21 | secure: import.meta.env.PROD,
22 | path: '/',
23 | sameSite: 'lax'
24 | });
25 |
26 | return new Response(null, {
27 | status: 302,
28 | headers: {
29 | Location: url.toString()
30 | }
31 | });
32 | }
33 |
--------------------------------------------------------------------------------
/src/routes/auth/login/google/callback/+server.ts:
--------------------------------------------------------------------------------
1 | import { google } from '$lib/server/oauth';
2 | import { ObjectParser } from '@pilcrowjs/object-parser';
3 | import { createUser, getUserFromGoogleId } from '$lib/server/user';
4 | import { createSession, generateSessionToken, setSessionTokenCookie } from '$lib/server/auth';
5 | import { decodeIdToken } from 'arctic';
6 |
7 | import type { RequestEvent } from './$types';
8 | import { withCatch } from '@tfkhdyt/with-catch';
9 |
10 | export async function GET(event: RequestEvent): Promise {
11 | const storedState = event.cookies.get('google_oauth_state') ?? null;
12 | const codeVerifier = event.cookies.get('google_code_verifier') ?? null;
13 | const code = event.url.searchParams.get('code');
14 | const state = event.url.searchParams.get('state');
15 |
16 | if (storedState === null || codeVerifier === null || code === null || state === null) {
17 | return new Response('Please restart the process.', {
18 | status: 400
19 | });
20 | }
21 | if (storedState !== state) {
22 | return new Response('Please restart the process.', {
23 | status: 400
24 | });
25 | }
26 |
27 | const [err, tokens] = await withCatch(google.validateAuthorizationCode(code, codeVerifier));
28 | if (err) {
29 | return new Response('Please restart the process.', {
30 | status: 400
31 | });
32 | }
33 |
34 | const claims = decodeIdToken(tokens.idToken());
35 | const claimsParser = new ObjectParser(claims);
36 |
37 | const googleId = claimsParser.getString('sub');
38 | const name = claimsParser.getString('name');
39 | const picture = claimsParser.getString('picture');
40 | const email = claimsParser.getString('email');
41 |
42 | const existingUser = await getUserFromGoogleId(googleId);
43 | if (existingUser !== null) {
44 | const sessionToken = generateSessionToken();
45 | const session = await createSession(sessionToken, existingUser.id);
46 | setSessionTokenCookie(event, sessionToken, session.expiresAt);
47 | return new Response(null, {
48 | status: 302,
49 | headers: {
50 | Location: '/'
51 | }
52 | });
53 | }
54 |
55 | const user = await createUser(googleId, email, name, picture);
56 | const sessionToken = generateSessionToken();
57 | const session = await createSession(sessionToken, user.id);
58 | setSessionTokenCookie(event, sessionToken, session.expiresAt);
59 | return new Response(null, {
60 | status: 302,
61 | headers: {
62 | Location: '/'
63 | }
64 | });
65 | }
66 |
--------------------------------------------------------------------------------
/src/routes/auth/logout/+server.ts:
--------------------------------------------------------------------------------
1 | import { error, redirect } from '@sveltejs/kit';
2 | import type { RequestHandler } from './$types';
3 | import { deleteSessionTokenCookie, invalidateSession } from '$lib/server/auth';
4 |
5 | export const GET: RequestHandler = async (event) => {
6 | const { locals } = event;
7 |
8 | if (!locals.session) {
9 | return error(401);
10 | }
11 | await invalidateSession(locals.session.id);
12 | deleteSessionTokenCookie(event);
13 |
14 | return redirect(301, '/');
15 | };
16 |
--------------------------------------------------------------------------------
/static/after.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tfkhdyt/remove-biji/379d60f4cc2907d520aca0de8875589727a9701b/static/after.webp
--------------------------------------------------------------------------------
/static/before.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tfkhdyt/remove-biji/379d60f4cc2907d520aca0de8875589727a9701b/static/before.webp
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tfkhdyt/remove-biji/379d60f4cc2907d520aca0de8875589727a9701b/static/favicon.ico
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import { mdsvex } from 'mdsvex';
2 | import adapter from '@sveltejs/adapter-node';
3 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
4 |
5 | /** @type {import('@sveltejs/kit').Config} */
6 | const config = {
7 | // Consult https://svelte.dev/docs/kit/integrations
8 | // for more information about preprocessors
9 | preprocess: [vitePreprocess(), mdsvex()],
10 |
11 | kit: {
12 | // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
13 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
14 | // See https://svelte.dev/docs/kit/adapters for more information about adapters.
15 | adapter: adapter()
16 | },
17 |
18 | extensions: ['.svelte', '.svx']
19 | };
20 |
21 | export default config;
22 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import { skeleton } from '@skeletonlabs/tw-plugin';
2 | import { join } from 'path';
3 | import type { Config } from 'tailwindcss';
4 | import typography from '@tailwindcss/typography';
5 |
6 | export default {
7 | darkMode: 'selector',
8 | content: [
9 | './src/**/*.{html,js,svelte,ts,svx}',
10 | join(require.resolve('@skeletonlabs/skeleton'), '../**/*.{html,js,svelte,ts}')
11 | ],
12 |
13 | theme: {
14 | extend: {}
15 | },
16 |
17 | plugins: [
18 | skeleton({
19 | themes: { preset: ['skeleton'] }
20 | }),
21 | typography()
22 | ]
23 | } satisfies Config;
24 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler"
13 | }
14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
16 | //
17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
18 | // from the referenced tsconfig.json - TypeScript does not merge them in
19 | }
20 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()]
6 | });
7 |
--------------------------------------------------------------------------------