Compare commits

...

233 Commits
4.3.3 ... 5.6.3

Author SHA1 Message Date
SinTan1729
828019998e build: Bumped version to 5.6.3 2025-04-07 22:24:49 -05:00
SinTan1729
49d910fb3c build: Updated deps to mitigate a tokio security issue 2025-04-07 22:24:10 -05:00
SinTan1729
c521ad1120 docs: Added some more options and info in the compose file 2025-04-02 17:17:59 -05:00
Sayantan Santra
d42a738861 docs: Updated README.md 2025-03-17 18:23:27 -05:00
SinTan1729
e3eaf5aba8 docs: Updated screenshots 2025-03-17 18:22:25 -05:00
SinTan1729
3b48ce7b5e chg: Simplify how wrong password text is shown 2025-03-05 15:56:12 -06:00
SinTan1729
5363a1b056 docs: Added info about dark mode 2025-03-05 15:22:58 -06:00
SinTan1729
0d58e626a4 fix: Hide the disabled wrong password text area in dialog 2025-03-04 00:11:34 -06:00
SinTan1729
e8faf660f4 build: Bumped version to 5.6.2 2025-03-03 18:45:08 -06:00
SinTan1729
67695da86b fix: Use changed methods for rand 2025-03-03 18:44:42 -06:00
SinTan1729
d50c183c9c build: Updated deps 2025-03-03 18:37:49 -06:00
SinTan1729
90b04b1f21 fix: Link colors for dark mode 2025-03-03 18:34:57 -06:00
SinTan1729
babf3d8911 new: Automatic dark mode support 2025-03-03 18:10:53 -06:00
SinTan1729
1ae00eb3a8 chg: Some cosmetic changes to login dialog 2025-03-03 14:16:38 -06:00
SinTan1729
6f419c7b3d new: Enforce ordering of data
Closes #46
Data is returned in order of id, which should match the order it was
inserted in. In the WebUI, the entries are shown in reverse, so the
latest link is at the top.
2025-03-03 12:27:59 -06:00
SinTan1729
c557b8b262 docs: Change to github link for extension 2025-01-30 01:03:06 -06:00
SinTan1729
a63222a71a docs: Add a few words 2025-01-30 00:59:05 -06:00
SinTan1729
86cea6278f docs: Added mention of extension 2025-01-28 01:39:44 -06:00
SinTan1729
f283991740 build: Bumped version to 5.6.1 2025-01-17 23:30:43 -06:00
Sayantan Santra
1775f71347 Merge pull request #42 from SolninjaA/main
Correctly output created link
2025-01-17 23:28:30 -06:00
SinTan1729
0b1224f8e5 docs: Improve clarification of the port variables 2025-01-17 23:27:33 -06:00
Solninja A
1047763285 chg: Bind server to port env variable 2025-01-18 12:52:19 +10:00
Solninja A
fc785c3eef Re-comment the API key in compose.yaml 2025-01-14 18:16:37 +10:00
Solninja A
17d0df943b Correctly output created link 2025-01-14 17:20:41 +10:00
SolomonTechnology
7b52bd60da Rewording 2025-01-14 00:33:01 +10:00
SolomonTechnology
db8417d919 Improve documentation for the "port" and "site_url" env variables 2025-01-14 00:25:49 +10:00
SinTan1729
af1685bb70 build: Bumped version to 5.6.0 2025-01-09 00:34:22 +05:30
Sayantan Santra
a5621acfe4 Merge pull request #40 from SinTan1729/get-longlink
Get longlink
2025-01-09 00:30:58 +05:30
SinTan1729
1be89db43b docs: Add info about expand route, and put API as preferred method 2025-01-09 00:27:05 +05:30
SinTan1729
a60853fd21 fix: Only pull hits when needed 2025-01-09 00:21:05 +05:30
SinTan1729
2b9fafe440 new: Got the expand API path working
It replies with info for a single shortlink. May be useful for
applications using json interface.
2025-01-08 20:09:24 +05:30
SinTan1729
f952cb88a0 build: Bumped version to 5.5.0 2025-01-06 11:59:41 +05:30
SinTan1729
9eec252fe2 build: Updated deps 2025-01-06 11:54:49 +05:30
Sayantan Santra
f8f4dae457 Merge pull request #39 from SolninjaA/main
Improvements of the API system
2025-01-06 11:52:48 +05:30
SinTan1729
16bc211f9f fix: Confirm when secure API key is provided 2025-01-06 11:48:18 +05:30
SinTan1729
cca5bcfa1a docs: Add example command to generate API key 2025-01-06 11:47:01 +05:30
SinTan1729
cba667ded8 chg: Small cosmetic change 2025-01-06 11:40:20 +05:30
SinTan1729
1d9a8c202d build: Add API_KEY variable in Makefile 2025-01-06 11:17:10 +05:30
SinTan1729
eb4f05a87b fix: Disregard empty Site URL 2025-01-06 11:11:09 +05:30
SinTan1729
5183279cab docs: Small changes to the README 2025-01-05 16:25:08 +05:30
SinTan1729
f1c1642976 chg: Small semantic changes 2025-01-05 16:20:38 +05:30
Solninja A
eed3c2292a Cleaned up code 2025-01-03 00:28:51 +10:00
Solninja A
4fb8d0cb5c Edited the API Key header to comply with OpenAPI v3 specs 2025-01-03 00:25:55 +10:00
Solninja A
9a0cdec646 Improved API error codes 2025-01-01 19:08:35 +10:00
Solninja A
818dadb84f Made code more Rust-like 2025-01-01 17:34:09 +10:00
Solninja A
247cfb0476 Fixed compose.yaml 2024-12-31 20:32:46 +10:00
Solninja A
6347a89725 Minor code clean up 2024-12-31 20:30:55 +10:00
Solninja A
9ddf043c17 Fix typos, etc 2024-12-31 20:17:13 +10:00
Solninja A
a1f8700664 Change README.md 2024-12-31 20:15:06 +10:00
Solninja A
aab7a9b3d1 Change README.md and remove unneeded dependencies 2024-12-31 20:13:37 +10:00
Solninja A
1ef5d539d5 Improve API error handling 2024-12-31 19:54:22 +10:00
Solninja A
5c2886f651 Changes the API to use JSON data and results 2024-12-31 19:11:47 +10:00
Solninja A
2c56c68637 Improves API functionality 2024-12-31 16:19:20 +10:00
SinTan1729
756d675f06 fix: Capitalization, fixes #37 2024-12-30 18:41:48 +05:30
SinTan1729
e6eed2dd70 build: Bumped version to 5.4.6 2024-11-07 19:35:42 -06:00
SinTan1729
37a5300015 fix: Disable copying to clipboard on WebKit, fixes #36
This disables clipboard copying and lets the user
manually copy the links.
2024-11-07 19:33:34 -06:00
SinTan1729
66d94634d9 build: Bumped version to 5.4.5 2024-11-06 22:11:36 -06:00
SinTan1729
03f5529c30 build: Updated deps 2024-11-06 22:11:05 -06:00
SinTan1729
f772475d96 fix: Do not autocapitalize shorturl on mobile devices 2024-11-06 21:57:56 -06:00
SinTan1729
8b8ceca313 chg: Remove lowercasing of shorturl from the CSS, fixes #35
This makes the behavior more uniform across different banned characters.
2024-11-03 01:17:53 -05:00
SinTan1729
201d0b319f chg: Move the font to assets 2024-10-25 14:47:15 -05:00
SinTan1729
733ef6ea67 docs: Added note about Dark Reader 2024-10-06 20:38:05 -05:00
SinTan1729
cf5909c888 fix: Use a simpler password to make the shell happy 2024-10-05 00:26:38 -05:00
SinTan1729
dcb3144b22 chg: Added a better compose file 2024-10-05 00:24:16 -05:00
SinTan1729
e0c61bdb93 build: Bumped version to 5.4.4 2024-10-03 00:02:48 -05:00
SinTan1729
06f7a33d5d fix: Do not consider empty password 2024-10-02 23:52:23 -05:00
SinTan1729
514e905299 chg: Updated instructions in the compose file 2024-10-02 23:46:56 -05:00
SinTan1729
3688692c7a chg: Default db location 2024-10-02 23:46:35 -05:00
SinTan1729
a7cf0cdf30 build: Bumped version to 5.4.3 2024-09-18 11:52:24 -05:00
SinTan1729
35880f4d1e build: Updated dependencies 2024-09-18 11:51:50 -05:00
Sayantan Santra
0d0da1141b Merge pull request #27 from yilmaz08/main
Fix simple dockerfile errors
2024-08-28 11:49:51 -05:00
SinTan1729
4a8b62446c fix: Case mismatch 2024-08-28 11:37:45 -05:00
Abdürrahim YILMAZ
855145d4d7 fix: argument was not being passed
Argument was not being passed, although it is declared. So changed to a
static path in "FROM scratch" part

Error message before the fix:

Step 17/19 : COPY --from=builder
/chhoto-url/target/$target/release/chhoto-url /chhoto-url
COPY failed: stat chhoto-url/target//release/chhoto-url: file does not
exist
2024-08-28 03:12:33 +03:00
Abdürrahim YILMAZ
59f679a1c2 fix: copy destination path
Error message before the fix:

Step 4/19 : COPY ./actix/Cargo.toml ./actix/Cargo.lock .

When using COPY with more than one source file, the destination must be
a directory and end with a /
2024-08-28 03:04:37 +03:00
SinTan1729
5213a9df2e docs: Added a docker image size badge 2024-06-21 10:01:19 -05:00
SinTan1729
3e6f482533 docs: Updated the screenshots 2024-06-21 09:46:47 -05:00
SinTan1729
56ab16aa4e build: Bumped version to 5.4.2 2024-06-20 20:59:06 -05:00
SinTan1729
066fa9c80a build: Updated deps 2024-06-20 20:48:06 -05:00
SinTan1729
9fc8634704 fix: Typo during reading slug_length, fixes #24 2024-06-20 20:46:26 -05:00
SinTan1729
5bbaad3001 build: Bumped version to 5.4.1 2024-06-07 15:34:28 -05:00
SinTan1729
892959d49d build: Updated deps 2024-06-07 15:33:58 -05:00
SinTan1729
9948ce713c chg: Some small changes to the html pages
Should be nicer on mobile devices
2024-06-01 00:48:40 -05:00
Sayantan Santra
70d9d828c5 Update bug_report.md 2024-06-01 00:36:14 -05:00
SinTan1729
114a97a273 build: Bumped version to 5.4.0 2024-05-31 20:31:29 -05:00
SinTan1729
96495b037d new: Made the Cache-Control headers fully configurable 2024-05-31 20:30:09 -05:00
SinTan1729
69fc25a264 build: Add port settings to the docker-test recipe 2024-05-31 11:29:41 -05:00
SinTan1729
f19f3249cc docs: Changed a word in the README 2024-05-31 01:51:11 -05:00
SinTan1729
2cf0e5d2de new: Ability to disable Cache-Control headers in testing recipe 2024-05-31 01:45:19 -05:00
SinTan1729
de9bc130d5 docs: Add info about configuring Cache-Control header 2024-05-31 01:42:51 -05:00
SinTan1729
8ff4c3f24f chg: Make the Cache-Control header configurable 2024-05-31 01:41:55 -05:00
SinTan1729
eab1c9bc73 new: Added cache-control header, fixes #22 2024-05-31 01:24:33 -05:00
SinTan1729
0b50a7c261 chg: Move all the services into their own file 2024-05-30 14:45:18 -05:00
SinTan1729
e55c6f82b4 fix: Make the link clickable when securecontext is missing 2024-05-29 19:14:31 -05:00
SinTan1729
6992d27390 build: Bumped version to 5.3.1 2024-05-29 16:12:31 -05:00
SinTan1729
39e4d2df74 chg: Show the whole link when clipboard is not accessible 2024-05-29 16:03:03 -05:00
SinTan1729
41b7e37819 build: Bumped version to 5.3.0 2024-05-29 10:45:42 -05:00
SinTan1729
e67e0a88cd build: Updated deps 2024-05-29 10:45:15 -05:00
SinTan1729
00ade1af40 fix: Do not allow empty values for db_url or site_url 2024-05-29 10:40:51 -05:00
SinTan1729
b1632c4c87 fix: Fixed some UI visibility related stuff 2024-05-29 10:40:51 -05:00
SinTan1729
3445d5366a fix: Properly handle visibility of elements 2024-05-29 10:40:51 -05:00
Sayantan Santra
5fb8587628 Merge pull request #21 from SinTan1729/public-mode
Enable support for public mode, closes #20
2024-05-29 00:02:38 -05:00
SinTan1729
6cdacda510 build: Make testing more configurable using env variables 2024-05-28 18:20:20 -05:00
SinTan1729
ed2be0e883 docs: Some documentation about public-mode 2024-05-28 18:20:09 -05:00
SinTan1729
0fce881654 new: Got public mode working, testing needed 2024-05-28 18:16:58 -05:00
SinTan1729
168cff94a2 build: Bumped version to 5.2.6 2024-05-06 16:49:47 -05:00
SinTan1729
b1c4142296 chg: Reply when shortlink cannot be added 2024-05-06 16:49:01 -05:00
SinTan1729
75912e8f9d build: Updated deps 2024-05-06 16:41:31 -05:00
SinTan1729
5ac822d5f9 fix: Middleware order 2024-04-11 13:01:33 -05:00
SinTan1729
233247e154 build: Bumped version to 5.2.5 2024-04-10 14:47:31 -05:00
SinTan1729
108abc2b30 build: Updated deps 2024-04-10 14:43:34 -05:00
SinTan1729
4a158bbea6 docs: Made some changes to the badges 2024-04-10 13:46:29 -05:00
SinTan1729
51c6817487 chg: Change the button in login dialog 2024-04-10 13:33:30 -05:00
Sayantan Santra
0192c26fd0 Merge pull request #17 from SinTan1729/dependabot/cargo/actix/h2-0.3.26
build(deps): bump h2 from 0.3.25 to 0.3.26 in /actix
2024-04-05 11:46:10 -05:00
dependabot[bot]
8b815e1bbb build(deps): bump h2 from 0.3.25 to 0.3.26 in /actix
Bumps [h2](https://github.com/hyperium/h2) from 0.3.25 to 0.3.26.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.25...v0.3.26)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-05 16:23:49 +00:00
SinTan1729
3ad05f1e63 docs: Added mobile friendly UI in README 2024-04-03 20:50:27 -05:00
SinTan1729
931b4a95e4 chg: Added SPDX headers 2024-04-03 20:40:26 -05:00
SinTan1729
cdc3508a0c chg: Added use std::io::Result 2024-04-03 17:40:59 -05:00
SinTan1729
e742c0ab5e fix: Use proper HTTP response codes 2024-04-03 16:19:39 -05:00
SinTan1729
231fd3c8ca build: Bumped version to 5.2.4 2024-04-02 21:28:02 -05:00
SinTan1729
86ec787d96 fix: Errors in script.js 2024-04-02 21:26:18 -05:00
SinTan1729
30c0b8b50a fix: Typo in script.js 2024-04-02 21:02:11 -05:00
SinTan1729
62ae71f4ca build: Bumped version to 5.2.3 2024-04-02 18:12:43 -05:00
SinTan1729
e9bb9d0b65 new: Use samesite cookies 2024-04-02 18:07:29 -05:00
SinTan1729
ca14c02e70 docs: Added some comments and changed the token name 2024-04-02 17:57:49 -05:00
SinTan1729
0469f9b933 chg: Get rid of naked unwraps and improve code flow 2024-04-02 17:43:36 -05:00
SinTan1729
f27984a63f docs: Edit README 2024-04-01 13:43:03 -05:00
SinTan1729
7ad874a1ff chg: Cleaned up the js code 2024-04-01 02:07:43 -05:00
SinTan1729
604e95aa9c chg: More readable response when not logged in 2024-04-01 00:58:18 -05:00
SinTan1729
917be6ade4 docs: Fix typo, and slight change in grammar 2024-04-01 00:47:25 -05:00
SinTan1729
2594051a54 build: Bumped version to 5.2.2 2024-03-31 21:24:38 -05:00
SinTan1729
38b817fdf8 new: Proper reply when invalid data is sent 2024-03-31 21:23:22 -05:00
SinTan1729
a9168e3459 docs: Added CLI usage instructions 2024-03-31 21:07:31 -05:00
SinTan1729
d48b664c0a chg: Updated the screenshot 2024-03-31 16:39:47 -05:00
SinTan1729
a0f0eb5280 build: Added docker-stop make entry 2024-03-31 16:11:45 -05:00
SinTan1729
34518affaf chg: Changed some struct names 2024-03-31 16:07:33 -05:00
SinTan1729
4f80b1b522 build: Bumped version to 5.2.1 2024-03-31 15:40:30 -05:00
Sayantan Santra
f38abdf1fb Merge pull request #15 from SinTan1729/json
chg: Use json while sending form for new url
2024-03-31 15:39:31 -05:00
SinTan1729
f2b5e1ab6d chg: Use json for sending url list 2024-03-31 15:38:59 -05:00
SinTan1729
99b5298cd8 chg: Use json while sending form for new url 2024-03-31 15:17:54 -05:00
SinTan1729
6659452c51 docs: Do not use quotes around variable examples 2024-03-31 01:17:13 -05:00
Sayantan Santra
3441d3ae90 Merge pull request #12 from SinTan1729/nanoid
Support uid slugs
2024-03-31 01:01:33 -05:00
SinTan1729
f6060eb649 build: Bumped version to 5.2.0 2024-03-31 00:59:58 -05:00
SinTan1729
6c7ca8d0ac fix: slug_style options are consistent now 2024-03-31 00:59:05 -05:00
SinTan1729
599b013fc9 chg: Change uid to UID 2024-03-31 00:51:32 -05:00
SinTan1729
841f877ee8 docs: Include info about UID in docs 2024-03-31 00:51:10 -05:00
SinTan1729
088cd594a5 chg: Change the env_var name 2024-03-31 00:45:29 -05:00
SinTan1729
f9e642275a new: Support uid slug 2024-03-31 00:38:42 -05:00
SinTan1729
9a520c122e chg: Disable column separators and right align hits 2024-03-31 00:12:48 -05:00
SinTan1729
3a712f812a build: Bumped version to 5.1.0 2024-03-29 15:41:13 -05:00
Sayantan Santra
753b73c4a3 Merge pull request #10 from SinTan1729/multi-arch
Multi-arch builds
2024-03-29 15:38:48 -05:00
SinTan1729
fbcb088260 chg: Updated deps 2024-03-28 13:28:28 -05:00
SinTan1729
f526e7ec5b chg: 404 response doesn't change the url 2024-03-28 13:27:33 -05:00
SinTan1729
5d8dd6fb63 chg: Use Option instead of returning empty String 2024-03-26 23:52:24 -05:00
SinTan1729
5e4db14ea2 docs: Some changes to the README 2024-03-26 17:46:26 -05:00
SinTan1729
c76b39dc16 fix: Typo in Makefile, and break lines 2024-03-25 17:28:24 -05:00
SinTan1729
731cb41646 chg: Added stopping the docker image to make clean 2024-03-25 16:44:47 -05:00
SinTan1729
0cfa674029 fix: Typo in Makefile 2024-03-25 16:08:04 -05:00
SinTan1729
1f18766f79 build: Switch workflow to only use Makefile 2024-03-25 16:02:52 -05:00
SinTan1729
dfefff2703 docs: Added a note about building 2024-03-25 01:40:10 -05:00
SinTan1729
6d3d220cff build: Fix the docker_push_script 2024-03-25 01:26:13 -05:00
SinTan1729
629e66a57c docs: Updated build instructions 2024-03-25 01:22:54 -05:00
SinTan1729
ce76f04f35 build: Support specifying targets with build-arg 2024-03-25 01:10:09 -05:00
SinTan1729
db5d1f72bd chg: Use username as a variable in Makefile 2024-03-25 01:05:06 -05:00
SinTan1729
e54aa3b33b build: Use different dockerfiles for single and multi-arch builds 2024-03-25 01:00:25 -05:00
SinTan1729
82559d38fd chg: Nicer multi-arch builds and also add armv7 2024-03-25 00:56:39 -05:00
SinTan1729
2baa481040 chg: Change the push script to work with the new setup 2024-03-24 23:33:41 -05:00
SinTan1729
ffb4846239 fix: Multi-arch upload 2024-03-24 23:27:31 -05:00
SinTan1729
5bd174d287 build: Switched to a cross-rt based multi-arch build process 2024-03-24 23:03:18 -05:00
SinTan1729
9221c3e371 chg: Add support for specifying the architecture 2024-03-24 19:38:33 -05:00
SinTan1729
c5cfba85f9 chg: Allow specifying target in the Dockerfile 2024-03-24 19:12:38 -05:00
SinTan1729
d6dcd2f18d fix: Remove surrounding quotes for siteurl 2024-03-24 16:43:27 -05:00
SinTan1729
d278021e1b fix: Support working inside subdirectories 2024-03-23 18:08:25 -05:00
SinTan1729
8dbb6e9bd6 new: Added a dev tag option 2024-03-23 16:07:55 -05:00
SinTan1729
cde3fb4c89 chg: Switch to woff2 fonts 2024-03-22 11:10:04 -05:00
SinTan1729
f3d5e2cf50 chg: Do not force the use of big fonts 2024-03-19 15:44:59 -05:00
SinTan1729
fd74a941d9 build: Bumped version to 5.0.8 2024-03-18 19:28:09 -05:00
SinTan1729
2cab341e8b fix: Broken table 2024-03-18 19:27:58 -05:00
SinTan1729
2a2ed7e41a fix: Screen overflow for table in mobile UI 2024-03-18 18:59:34 -05:00
SinTan1729
5a5a1bc775 build: Bumped version to 5.0.7 2024-03-18 16:04:06 -05:00
SinTan1729
b66086be38 fix: Centering on mobile UI 2024-03-18 16:02:45 -05:00
SinTan1729
3be2862e9f fix: Width issues on mobile 2024-03-18 15:23:12 -05:00
SinTan1729
b0603f62b4 chg: Make the table a little wide 2024-03-18 02:07:46 -05:00
SinTan1729
9d5bc2d0fd build: Bumped version to 5.0.6 2024-03-18 02:00:07 -05:00
SinTan1729
94af81b802 fix: Mobile UI 2024-03-18 01:59:31 -05:00
SinTan1729
03154fd010 build: Bumped version to 5.0.5 2024-03-18 01:23:45 -05:00
SinTan1729
4c394c8004 chg: Change the font to Montserrat 2024-03-18 01:22:17 -05:00
SinTan1729
9d46546e44 chg: Some small changes to the UI 2024-03-17 01:47:01 -05:00
SinTan1729
c07bb5c25f build: Bumped version to 5.0.4 2024-03-15 23:21:15 -05:00
SinTan1729
85f150b543 build: Updated dependencies 2024-03-15 23:14:28 -05:00
SinTan1729
d235f1aea7 new: Added some validation check in client side 2024-03-15 23:05:56 -05:00
SinTan1729
13d613093b build: Change order of pushing the builds, looks nicer lol 2024-03-14 00:47:27 -05:00
SinTan1729
71ac19028e docs: Remove the cargo build instructions 2024-03-13 19:26:10 -05:00
SinTan1729
d8ee2ce658 build: Bumped version to 5.0.3 2024-03-13 18:59:46 -05:00
SinTan1729
5ecd29926d chg: Added some checks during deletion of a shortlink 2024-03-13 18:59:10 -05:00
SinTan1729
aa097ad982 fix: bash if issue 2024-03-10 18:44:11 -05:00
SinTan1729
bf0944a5fd build: Bumped version to 5.0.2 2024-03-10 18:39:05 -05:00
SinTan1729
6693941985 docs: Updated the issue templates 2024-03-10 18:22:22 -05:00
SinTan1729
0bfaa49e7b fix: Wrong regex in validate_link, fixes #6 2024-03-10 18:18:07 -05:00
SinTan1729
a32f00c36e fix: Logo in README 2024-03-08 13:33:44 -06:00
SinTan1729
8b1af0a169 chg: Moved the resources folder out of actix since it isn't Rust 2024-03-08 10:41:52 -06:00
SinTan1729
9fdb634d71 new: Added a convenience script for publishing 2024-03-07 14:30:41 -06:00
SinTan1729
a5574683a6 docs: Change NPM recommendation to caddy 2024-03-06 22:42:06 -06:00
SinTan1729
d1ac769fc1 build: Use cargo-chef to potentially speed up builds 2024-03-06 19:24:36 -06:00
SinTan1729
8ba8472940 docs: Edited the features section 2024-03-04 19:37:36 -06:00
Sayantan Santra
f331370767 Update issue templates 2024-03-04 19:17:00 -06:00
SinTan1729
cb8f6f3e1d fix: Link to the older base project 2024-03-04 19:10:47 -06:00
SinTan1729
4d3e52cc95 docs: Added some stats and goals 2024-03-04 19:05:11 -06:00
SinTan1729
8f0a6f0fc6 build: Bump version to 5.0.1 2024-03-04 17:05:34 -06:00
Sayantan Santra
15ff8819a7 Merge pull request #5 from SinTan1729/dependabot/cargo/actix/mio-0.8.11
Bump mio from 0.8.6 to 0.8.11 in /actix
2024-03-04 17:03:38 -06:00
dependabot[bot]
501f7f1d65 Bump mio from 0.8.6 to 0.8.11 in /actix
Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.6 to 0.8.11.
- [Release notes](https://github.com/tokio-rs/mio/releases)
- [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/mio/compare/v0.8.6...v0.8.11)

---
updated-dependencies:
- dependency-name: mio
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 21:45:41 +00:00
SinTan1729
bd47f3c74b chg: Expand the name explanation 2024-02-29 18:29:36 -06:00
SinTan1729
31212ab252 docs: Updated the screenshot 2024-02-13 00:56:30 -06:00
SinTan1729
deff47db2c build: Bumped version number to 5.0.0 2024-02-10 19:42:19 -06:00
SinTan1729
95a8263797 new: Show version number 2024-02-10 19:41:50 -06:00
SinTan1729
0227c5f783 docs: Explain the name 2024-02-10 18:44:15 -06:00
SinTan1729
4a8385955b chg: Renamed the project
When starting out, I kept the name same as the original project.
But I think I've made significant changes, so keeping the name
same would be disingenuous.
2024-02-10 18:29:50 -06:00
SinTan1729
1f9cf1d777 chg: Untrack some unnecessary files 2024-02-08 19:09:07 -06:00
SinTan1729
2a85189155 build: Bumped version to 4.4.0 2024-02-08 11:34:26 -06:00
SinTan1729
caa6c58fd2 build: Switch to a static build to mitigate incompatible packages 2024-02-08 11:33:26 -06:00
SinTan1729
583700cdbf build: Updated minimum deps 2024-02-08 10:04:12 -06:00
SinTan1729
432328b97e doc: Fix the license 2024-01-30 22:08:04 -06:00
Sayantan Santra
bcf30049a2 Merge pull request #4 from SinTan1729/dependabot/cargo/actix/h2-0.3.24
Bump h2 from 0.3.17 to 0.3.24 in /actix
2024-01-30 22:01:02 -06:00
dependabot[bot]
bfd7e111d9 Bump h2 from 0.3.17 to 0.3.24 in /actix
Bumps [h2](https://github.com/hyperium/h2) from 0.3.17 to 0.3.24.
- [Release notes](https://github.com/hyperium/h2/releases)
- [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md)
- [Commits](https://github.com/hyperium/h2/compare/v0.3.17...v0.3.24)

---
updated-dependencies:
- dependency-name: h2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-19 16:10:34 +00:00
SinTan1729
0bd8fbe96c build: Bump version to 4.3.6 2023-10-08 18:23:05 -05:00
SinTan1729
0c80a0ac21 build: Update deps 2023-10-08 18:22:45 -05:00
SinTan1729
f6255566b0 chg: Allow insecure cookies 2023-10-08 18:20:34 -05:00
SinTan1729
57d390a129 build: Bump version to 4.3.5 2023-09-22 15:10:32 -05:00
SinTan1729
acb67fdcf6 build: Bump dependencies 2023-09-22 15:08:12 -05:00
Sayantan Santra
9e8a9395a5 Merge pull request #2 from SinTan1729/dependabot/cargo/actix/aes-gcm-0.10.3
Bump aes-gcm from 0.10.1 to 0.10.3 in /actix
2023-09-22 16:28:41 +00:00
dependabot[bot]
c77763c1c2 Bump aes-gcm from 0.10.1 to 0.10.3 in /actix
Bumps [aes-gcm](https://github.com/RustCrypto/AEADs) from 0.10.1 to 0.10.3.
- [Commits](https://github.com/RustCrypto/AEADs/compare/aes-gcm-v0.10.1...aes-gcm-v0.10.3)

---
updated-dependencies:
- dependency-name: aes-gcm
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-22 16:12:48 +00:00
SinTan1729
79cca4bc26 build: Bump version to 4.3.4 2023-09-19 18:11:47 -05:00
SinTan1729
1ee7ebe847 fix: Correct order for random names 2023-09-19 18:11:29 -05:00
SinTan1729
ac82396584 change: Spelling 2023-06-24 17:39:21 -05:00
32 changed files with 2604 additions and 1219 deletions

6
.gitattributes vendored
View File

@@ -1,6 +0,0 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# These are explicitly windows files and should use crlf
*.bat text eol=crlf

44
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,44 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'bug'
assignees: 'SinTan1729'
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Which version of Chhoto-URL are you experiencing the problem on?**
e.g. v5.x.x
**Can you reproduce the issue in the latest version?**
Yes/No
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: 'feature-request'
assignees: 'SinTan1729'
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

7
.gitignore vendored
View File

@@ -1,4 +1,4 @@
# Ignore cargo build outputs
# Ignore build outputs
actix/target
# Ignore SQLite file
@@ -6,4 +6,7 @@ urls.sqlite
# Ignore irrelevant dotfiles
.vscode/
.directory
**/.directory
.env
cookie*
.idea/

View File

@@ -1,25 +1,30 @@
FROM rust:1 as build
RUN cargo install cargo-build-deps
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
# SPDX-License-Identifier: MIT
RUN cargo new --bin simply-shorten
WORKDIR /simply-shorten
COPY ./actix/Cargo.toml .
COPY ./actix/Cargo.lock .
RUN cargo build-deps --release
FROM lukemathwalker/cargo-chef:latest-rust-slim AS chef
WORKDIR /chhoto-url
FROM chef AS planner
COPY ./actix/Cargo.toml ./actix/Cargo.lock ./
COPY ./actix/src ./src
RUN cargo chef prepare --recipe-path recipe.json
RUN cargo build --release
FROM chef AS builder
ARG target=x86_64-unknown-linux-musl
RUN apt-get update && apt-get install -y musl-tools
RUN rustup target add $target
FROM frolvlad/alpine-glibc:latest
COPY --from=planner /chhoto-url/recipe.json recipe.json
# Build dependencies - this is the caching Docker layer
RUN cargo chef cook --release --target=$target --recipe-path recipe.json
RUN apk add sqlite-libs
COPY ./actix/Cargo.toml ./actix/Cargo.lock ./
COPY ./actix/src ./src
# Build application
RUN cargo build --release --target=$target --locked --bin chhoto-url
RUN cp /chhoto-url/target/$target/release/chhoto-url /chhoto-url/release
WORKDIR /opt
COPY --from=build /simply-shorten/target/release/simply-shorten /opt/simply-shorten
COPY ./actix/resources /opt/resources
CMD ["./simply-shorten"]
FROM scratch
COPY --from=builder /chhoto-url/release /chhoto-url
COPY ./resources /resources
ENTRYPOINT ["/chhoto-url"]

18
Dockerfile.multiarch Normal file
View File

@@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
# SPDX-License-Identifier: MIT
FROM scratch AS builder-amd64
COPY ./actix/target/x86_64-unknown-linux-musl/release/chhoto-url /chhoto-url
FROM scratch AS builder-arm64
COPY ./actix/target/aarch64-unknown-linux-musl/release/chhoto-url /chhoto-url
FROM scratch AS builder-arm
COPY ./actix/target/armv7-unknown-linux-musleabihf/release/chhoto-url /chhoto-url
ARG TARGETARCH
FROM builder-$TARGETARCH
COPY ./resources /resources
ENTRYPOINT ["/chhoto-url"]

52
Makefile Normal file
View File

@@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
# SPDX-License-Identifier: MIT
# .env file has the variables $DOCKER_USERNAME and $PASSWORD defined
include .env
setup:
cargo install cross
rustup target add x86_64-unknown-linux-musl
docker buildx create --use --platform=linux/arm64,linux/amd64 --name multi-platform-builder
docker buildx inspect --bootstrap
build-dev:
cargo build --release --locked --manifest-path=actix/Cargo.toml --target x86_64-unknown-linux-musl
docker-local: build-dev
docker build --tag chhoto-url --build-arg TARGETARCH=amd64 -f Dockerfile.multiarch .
docker-stop:
docker ps -q --filter "name=chhoto-url" | xargs -r docker stop
docker ps -aq --filter "name=chhoto-url" | xargs -r docker rm
docker-test: docker-local docker-stop
docker run -p ${PORT}:${PORT} --name chhoto-url -e password="${PASSWORD}" -e public_mode="${PUBLIC_MODE}" \
-e site_url="${SITE_URL}" -e db_url="${DB_URL}" -e redirect_method="${REDIRECT_METHOD}" -e port="${PORT}"\
-e slug_style="${SLUG_STYLE}" -e slug_length="${SLUG_LENGTH}" -e cache_control_header="${CACHE_CONTROL_HEADER}"\
-e api_key="${API_KEY}"\
-d chhoto-url
docker logs chhoto-url -f
docker-dev: build-dev
docker build --push --tag ${DOCKER_USERNAME}/chhoto-url:dev --build-arg TARGETARCH=amd64 -f Dockerfile.multiarch .
build-release:
cross build --release --locked --manifest-path=actix/Cargo.toml --target aarch64-unknown-linux-musl
cross build --release --locked --manifest-path=actix/Cargo.toml --target armv7-unknown-linux-musleabihf
cross build --release --locked --manifest-path=actix/Cargo.toml --target x86_64-unknown-linux-musl
V_PATCH := $(shell cat actix/Cargo.toml | sed -rn 's/^version = "(.+)"$$/\1/p')
V_MINOR := $(shell cat actix/Cargo.toml | sed -rn 's/^version = "(.+)\..+"$$/\1/p')
V_MAJOR := $(shell cat actix/Cargo.toml | sed -rn 's/^version = "(.+)\..+\..+"$$/\1/p')
docker-release: build-release
docker buildx build --push --tag ${DOCKER_USERNAME}/chhoto-url:${V_MAJOR} --tag ${DOCKER_USERNAME}/chhoto-url:${V_MINOR} \
--tag ${DOCKER_USERNAME}/chhoto-url:${V_PATCH} --tag ${DOCKER_USERNAME}/chhoto-url:latest \
--platform linux/amd64,linux/arm64,linux/arm/v7 -f Dockerfile.multiarch .
clean:
docker ps -q --filter "name=chhoto-url" | xargs -r docker stop
docker ps -aq --filter "name=chhoto-url" | xargs -r docker rm
cargo clean --manifest-path=actix/Cargo.toml
.PHONY: build-dev docker-local docker-stop build-release

199
README.md
View File

@@ -1,33 +1,53 @@
[![docker-pulls](https://img.shields.io/docker/pulls/sintan1729/simply-shorten)](https://hub.docker.com/r/sintan1729/simply-shorten)
[![maintainer](https://img.shields.io/badge/maintainer-SinTan1729-blue)](https://github.com/SinTan1729)
![commit-since-latest-release](https://img.shields.io/github/commits-since/SinTan1729/simply-shorten/latest?sort=semver&label=commits%20since%20latest%20release)
<!-- SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> -->
<!-- SPDX-License-Identifier: MIT -->
# ![Logo](actix/resources/assets/favicon-32.png) <span style="font-size:42px">Simply Shorten</span>
[![docker-pulls-badge](https://img.shields.io/docker/pulls/sintan1729/chhoto-url)](https://hub.docker.com/r/sintan1729/chhoto-url)
[![maintainer-badge](https://img.shields.io/badge/maintainer-SinTan1729-blue)](https://github.com/SinTan1729)
[![latest-release-badge](https://img.shields.io/github/v/release/SinTan1729/chhoto-url?label=latest%20release)](https://github.com/SinTan1729/chhoto-url/releases/latest)
![docker-image-size-badge](https://img.shields.io/docker/image-size/sintan1729/chhoto-url)
![commit-since-latest-release-badge](https://img.shields.io/github/commits-since/SinTan1729/chhoto-url/latest?sort=semver&label=commits%20since%20latest%20release)
[![license-badge](https://img.shields.io/github/license/SinTan1729/chhoto-url)](https://spdx.org/licenses/MIT.html)
# ![Logo](resources/assets/favicon-32.png) <span style="font-size:42px">Chhoto URL</span>
# What is it?
A simple selfhosted URL shortener with no unnecessary features.
A simple selfhosted URL shortener with no unnecessary features. Simplicity
and speed are the main foci of this project. The docker image is ~6 MB (compressed),
and it uses <5 MB of RAM under regular use.
Don't worry if you see no activity for a long time. I consider this project
to be complete, not dead. I'm unlikely to add any new features, but I will try
and fix every bug you report.
and fix every bug you report. I will also try to keep it updated in terms of
security vulnerabilities.
If you feel like a feature is missing, please let me know by creating an issue
using the "feature request" template.
## But why another URL shortener?
I've looked at a couple popular URL shorteners, however they either have
unnecessary features, or they didn't have all the features I wanted.
Most URL shorteners are either bloated with unnecessary features, or are a pain to set up.
Even fewer are written with simplicity and lightness in mind. When I saw the `simply-shorten`
project (linked below), I really liked the idea but thought that it missed some features. Also,
I didn't like the fact that a simple app like this had a ~200 MB docker image (mostly due to the
included java runtime). So, I decided to rewrite it in Rust and add some features to it that I
thought were essential (e.g. hit counting).
## What does the name mean?
Chhoto (ছোট, [IPA](https://en.wikipedia.org/wiki/Help:IPA/Bengali): /tʃʰoʈo/) is the Bangla word
for small. URL means, well... URL. So the name simply means Small URL.
# Features
- Shortens URLs of any length to a fixed length, randomly generated string.
- Shortens URLs of any length to a randomly generated link.
- (Optional) Allows you to specify the shortened URL instead of the generated
one (Missing in a surprising number of alternatives).
- Opening the fixed length URL in your browser will instantly redirect you
to the correct long URL (you'd think that's a standard feature, but
apparently it's not).
- Provides a simple API for adding new short links.
one. (It's surprisingly missing in a surprising number of alternatives.)
- Opening the shortened URL in your browser will instantly redirect you
to the correct long URL. (So no stupid redirecting pages.)
- Super lightweight and snappy. (The docker image is only ~6MB and RAM uasge
stays under 5MB under normal use.)
- Counts number of hits for each short link in a privacy respecting way
i.e. only the hit is recorded, and nothing else.
- Has a mobile friendly UI, and automatic dark mode.
- Has a public mode, where anyone can add links without authentication. Deleting
or listing available links will need admin access using the password.
- Allows setting the URL of your website, in case you want to conveniently
generate short links locally.
- Links are stored in an SQLite database.
@@ -36,7 +56,7 @@ unnecessary features, or they didn't have all the features I wanted.
written in plain HTML and vanilla JS, using [Pure CSS](https://purecss.io/)
for styling.
- Uses very basic authentication using a provided password. It's not encrypted in transport.
I recommend using something like [Nginx Proxy Manager](https://nginxproxymanager.com/) to
I recommend using a reverse proxy such as [caddy](https://caddyserver.com/) to
encrypt the connection by SSL.
# Bloat that will not be implemented
@@ -51,8 +71,11 @@ not needed here.
- Paywalls or messages begging for donations. If you want to support me (for
whatever reason), you can message me through GitHub issues.
# Screenshot
![Screenshot](screenshot.png)
# Screenshots
<p align="middle">
<img src="screenshot-desktop.webp" height="250" alt="desktop screenshot" />
<img src="screenshot-mobile.webp" height="250" alt="mobile screenshot" />
</p>
# Usage
## Using `docker compose` (Recommended method)
@@ -65,42 +88,25 @@ docker compose up -d
If you're using a custom location for the `db_url`, make sure to make that file
before running the docker image, as otherwise a directory will be created in its
place, resulting in possibly unwanted behavior.
## Building from source
Clone this repository
```
git clone https://github.com/SinTan1729/simply-shorten
```
### 2. Set environment variables
```bash
# Required for authentication
export password=<api password>
# Sets where the database exists. Can be local or remote (optional)
export db_url=<url> # Default: './urls.sqlite'
# Sets the url of website, so that it displays that even when accessed
# locally (optional, defaults to hostname you're accessing it on)
export site_url=<url>
```
### 3. Build and run it
```
cd actix
cargo run
```
You can optionally set the port the server listens on by appending `--port=[port]`.
### 4. Navigate to `http://localhost:4567` in your browser, add links as you wish.
## Running with docker
## Building and running with docker
### `docker run` method
0. (Only if you really want to) Build the image
0. (Only if you really want to) Build the image for the default `x86_64-unknown-linux-musl` target:
```
docker build . -t simply-shorten:latest
docker build . -t chhoto-url
```
For building on `arm64` or `arm/v7`, use the following:
```
docker build . -t chhoto-url --build-arg target=<desired-target>
```
Make sure that the desired target is a `musl` one, since the docker image is built from `scratch`.
For cross-compilation, take a look at the `Makefile`. It builds and pushes for `linux/amd64`, `linux/aarch64`
and `linux/arm/v7` architectures. For any other architectures, open a discussion, and I'll try to help you out.
1. Run the image
```
docker run -p 4567:4567
-e password="password"
-d simply-shorten:latest
-d chhoto-url:latest
```
1.a Make the database file available to host (optional)
```
@@ -109,7 +115,7 @@ docker run -p 4567:4567 \
-e password="password" \
-v ./urls.sqlite:/urls.sqlite \
-e db_url=/urls.sqlite \
-d simply-shorten:latest
-d chhoto-url:latest
```
1.b Further, set the URL of your website (optional)
```
@@ -119,11 +125,100 @@ docker run -p 4567:4567 \
-v ./urls.sqlite:/urls.sqlite \
-e db_url=/urls.sqlite \
-e site_url="https://www.example.com" \
-d simply-shorten:latest
-d chhoto-url:latest
```
1.c Further, set an API key to activate JSON result mode (optional)
```
docker run -p 4567:4567 \
-e password="password" \
-e api_key="SECURE_API_KEY" \
-v ./urls.sqlite:/urls.sqlite \
-e db_url=/urls.sqlite \
-e site_url="https://www.example.com" \
-d chhoto-url:latest
```
You can also set the redirect method to Permanent 308 (default) or Temporary 307 by setting
the `redirect_method` variable to `TEMPORARY` or `PERMANENT` (it's matched exactly).
You can set the redirect method to Permanent 308 (default) or Temporary 307 by setting
the `redirect_method` variable to `TEMPORARY` or `PERMANENT` (it's matched exactly). By
default, the auto-generated links are adjective-name pairs. You can use UIDs by setting
the `slug_style` variable to `UID`. You can also set the length of those slug by setting
the `slug_length` variable. It defaults to 8, and a minimum of 4 is supported.
To enable public mode, set `public_mode` to `Enable`. With this, anyone will be able to add
links. Listing existing links or deleting links will need admin access using the password.
By default, the server sends no Cache-Control headers. You can set custom `cache_control_header`
to send your desired headers. It must be a comma separated list of valid
[RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2) headers. For example,
you can set it to `no-cache, private` to disable caching. It might help during testing if
served through a proxy.
## Instructions for CLI usage
The application can be used from the terminal using something like `curl`. In all the examples
below, replace `http://localhost:4567` with where your instance of `chhoto-url` is accessible.
You can get the version of `chhoto-url` the server is running using `curl http://localhost:4567/api/version` and
get the siteurl using `curl http://localhost:4567/api/siteurl`. These routes are accessible without any authentication.
### API key validation
**This is required for programs that rely on a JSON response from Chhoto URL**
In order to use API key validation, set the `api_key` environment variable. If this is not set, the API will default to cookie
validation (see section above). If the API key is insecure, a warning will be outputted along with a generated API key which may be used.
Example Linux command for generating a secure API key: `tr -dc A-Za-z0-9 </dev/urandom | head -c 128`
To add a link:
``` bash
curl -X POST -H "X-API-Key: <YOUR_API_KEY>" -d '{"shortlink":"<shortlink>", "longlink":"<longlink>"}' http://localhost:4567/api/new
```
Send an empty `<shortlink>` if you want it to be auto-generated. The server will reply with the generated shortlink.
To get information about a single shortlink:
``` bash
curl -H "X-API-Key: <YOUR_API_KEY>" -d '<shortlink>' http://localhost:4567/api/expand
```
(This route is not accessible using cookie validation.)
To get a list of all the currently available links:
``` bash
curl -H "X-API-Key: <YOUR_API_KEY>" http://localhost:4567/api/all
```
To delete a link:
``` bash
curl -X DELETE -H "X-API-Key: <YOUR_API_KEY>" http://localhost:4567/api/del/<shortlink>
```
Where `<shortlink>` is name of the shortened link you would like to delete. For example, if the shortened link is
`http://localhost:4567/example`, `<shortlink>` would be `example`.
The server will output when the instance is accessed over API, when an incorrect API key is received, etc.
### Cookie validation
If you have set up a password, first do the following to get an authentication cookie and store it in a file.
```bash
curl -X POST -d "<your-password>" -c cookie.txt http://localhost:4567/api/login
```
You should receive "Correct password!" if the provided password was correct. For any subsequent
request, please add `-b cookie.txt` to provide authentication.
To add a link, do
```bash
curl -X POST -d '{"shortlink":"<shortlink>", "longlink":"<longlink>"}' http://localhost:4567/api/new
```
Send an empty `<shortlink>` if you want it to be auto-generated. The server will reply with the generated shortlink.
To get a list of all the currently available links as `json`, do
```bash
curl http://localhost:4567/api/all
```
To delete a link, do
```bash
curl -X DELETE http://localhost:4567/api/del/<shortlink>
```
The server will send a confirmation.
## Disable authentication
If you do not define a password environment variable when starting the docker image, authentication
@@ -135,6 +230,8 @@ pointing to illegal content. Since there are no logs, it's impossible to prove
that those links aren't created by you.
## Notes
- It started as a fork of [this project](https://gitlab.com/draganczukp/simply-shorten).
- It started as a fork of [`simply-shorten`](https://gitlab.com/draganczukp/simply-shorten).
- There's an (unofficial) extension maintained by for shortening URLs easily using Chhoto URL.
[You can take a look at it here.](https://github.com/SolninjaA/Chhoto-URL-Extension)
- The list of adjectives and names used for random short url generation is a modified
version of [this list used by docker](https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go).
version of [this list used by docker](https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go).

View File

@@ -1,3 +0,0 @@
[Dolphin]
Timestamp=2023,4,2,17,52,37.922
Version=4

1569
actix/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,40 @@
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
# SPDX-License-Identifier: MIT
[package]
name = "simply-shorten"
version = "4.3.3"
name = "chhoto-url"
version = "5.6.3"
edition = "2021"
authors = ["Sayantan Santra <sayantan[dot]santra689[at]gmail[dot]com"]
license = "MIT"
license = "mit"
description = "A simple selfhosted URL shortener with no unnecessary features."
homepage = "https://github.com/SinTan1729/simply-shorten"
documentation = "https://github.com/SinTan1729/simply-shorten"
repository = "https://github.com/SinTan1729/simply-shorten"
homepage = "https://github.com/SinTan1729/chhoto-url"
documentation = "https://github.com/SinTan1729/chhoto-url"
repository = "https://github.com/SinTan1729/chhoto-url"
readme = "README.md"
keywords = ["docker", "rust", "self-hosted", "url-shortener", "webapp", "shortener", "link-shortener", "actix-web"]
keywords = [
"docker",
"rust",
"self-hosted",
"url-shortener",
"webapp",
"shortener",
"link-shortener",
"actix-web",
]
categories = ["web-programming"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "4"
actix-files = "0.6.2"
rusqlite = "0.29.0"
regex = "1.7.3"
rand = "0.8.5"
actix-session = {version = "0.7.2", features = ["cookie-session"]}
env_logger = "0.10.0"
actix-web = "4.5.1"
actix-files = "0.6.5"
rusqlite = { version = "0.34.0", features = ["bundled"] }
regex = "1.10.3"
rand = "0.9.0"
passwords = "3.1.16"
actix-session = { version = "0.10.0", features = ["cookie-session"] }
env_logger = "0.11.1"
nanoid = "0.4.0"
serde_json = "1.0.115"
serde = { version = "1.0.197", features = [ "derive" ] }

View File

@@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Error 404</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<style>
#quote {
text-indent: 4em;
}
</style>
<body style="text-align: center;">
<h1>Error 404!</h1>
<div style="display: inline-block; text-align:left;">
<p>You step in the stream,</p>
<p>but the water has moved on.</p>
<p>The page is not here.</p>
<p id="quote"> — Cass Whittington</p>
</div>
</body>
</html>

View File

@@ -1,207 +0,0 @@
const getSiteUrl = async () => await fetch("/api/siteurl")
.then(res => res.text())
.then(text => {
if (text == "unset") {
return window.location.host;
}
else {
return text;
}
});
const refreshData = async () => {
let reply = await fetch("/api/all").then(res => res.text());
if (reply == "logged_out") {
console.log("logged_out");
document.getElementById("container").style.filter = "blur(2px)"
document.getElementById("login-dialog").showModal();
document.getElementById("password").focus();
} else {
data = reply
.split("\n")
.filter(line => line !== "")
.map(line => line.split(","))
.map(arr => ({
short: arr[0],
long: arr[1],
hits: arr[2]
}));
displayData(data);
}
};
const displayData = async (data) => {
let site = await getSiteUrl();
table_box = document.querySelector(".pure-table");
loading_text = document.getElementsByName("loading-text")[0];
if (data.length == 0) {
table_box.style.visibility = "hidden";
loading_text.style.display = "block";
loading_text.innerHTML = "No active links.";
}
else {
loading_text.style.display = "none";
const table = document.querySelector("#url-table");
if (!window.isSecureContext) {
const shortUrlHeader = document.getElementById("short-url-header");
shortUrlHeader.innerHTML = "Short URL<br>(right click and copy)";
}
table_box.style.visibility = "visible";
table.innerHTML = ''; // Clear
data.forEach(tr => table.appendChild(TR(tr, site)));
}
};
const showAlert = async (text, col) => {
document.getElementById("alert-box")?.remove();
const controls = document.querySelector(".pure-controls");
const alertBox = document.createElement("p");
alertBox.id = "alert-box";
alertBox.style.color = col;
alertBox.innerHTML = text;
controls.appendChild(alertBox);
};
const TR = (row, site) => {
const tr = document.createElement("tr");
const longTD = TD(A_LONG(row.long), "Long URL");
var shortTD = null;
if (window.isSecureContext) {
shortTD = TD(A_SHORT(row.short, site), "Short URL");
}
else {
shortTD = TD(A_SHORT_INSECURE(row.short, site), "Short URL");
}
hitsTD = TD(row.hits);
hitsTD.setAttribute("label", "Hits");
const btn = deleteButton(row.short);
tr.appendChild(shortTD);
tr.appendChild(longTD);
tr.appendChild(hitsTD);
tr.appendChild(btn);
return tr;
};
const copyShortUrl = async (link) => {
const site = await getSiteUrl();
try {
navigator.clipboard.writeText(`${site}/${link}`);
showAlert(`Short URL ${link} was copied to clipboard!`, "green");
} catch (e) {
console.log(e);
showAlert("Could not copy short URL to clipboard, please do it manually.", "red");
}
};
const addProtocol = (input) => {
var url = input.value.trim();
if (url != "" && !~url.indexOf("://") && !~url.indexOf("magnet:")) {
url = "https://" + url;
}
input.value = url;
return input
}
const A_LONG = (s) => `<a href='${s}'>${s}</a>`;
const A_SHORT = (s, t) => `<a href="javascript:copyShortUrl('${s}');">${s}</a>`;
const A_SHORT_INSECURE = (s, t) => `<a href="${t}/${s}">${s}</a>`;
const deleteButton = (shortUrl) => {
const td = document.createElement("td");
const btn = document.createElement("button");
btn.innerHTML = "&times;";
btn.onclick = e => {
e.preventDefault();
if (confirm("Do you want to delete the entry " + shortUrl + "?")) {
document.getElementById("alert-box")?.remove();
showAlert("&nbsp;", "black");
fetch(`/api/del/${shortUrl}`, {
method: "DELETE"
}).then(_ => refreshData());
}
};
td.setAttribute("name", "deleteBtn");
td.setAttribute("label", "Delete");
td.appendChild(btn);
return td;
};
const TD = (s, u) => {
const td = document.createElement("td");
const div = document.createElement("div");
div.innerHTML = s;
td.appendChild(div);
td.setAttribute("label", u);
return td;
};
const submitForm = () => {
const form = document.forms.namedItem("new-url-form");
const longUrl = form.elements["longUrl"];
const shortUrl = form.elements["shortUrl"];
const url = `/api/new`;
fetch(url, {
method: "POST",
body: `${longUrl.value};${shortUrl.value}`
})
.then(res => {
if (!res.ok) {
showAlert("Short URL is not valid or it's already in use!", "red");
return "error";
}
else {
return res.text();
}
}).then(text => {
if (text != "error") {
copyShortUrl(text);
longUrl.value = "";
shortUrl.value = "";
refreshData();
}
});
};
const submitLogin = () => {
const password = document.getElementById("password");
fetch("/api/login", {
method: "POST",
body: password.value
}).then(res => {
if (res.ok) {
document.getElementById("container").style.filter = "blur(0px)"
document.getElementById("login-dialog").remove();
refreshData();
} else {
const wrongPassBox = document.getElementById("wrong-pass");
wrongPassBox.innerHTML = "Wrong password!";
wrongPassBox.style.color = "red";
password.focus();
}
})
}
(async () => {
await refreshData();
const form = document.forms.namedItem("new-url-form");
form.onsubmit = e => {
e.preventDefault();
submitForm();
}
const login_form = document.forms.namedItem("login-form");
login_form.onsubmit = e => {
e.preventDefault();
submitLogin();
}
})();

View File

@@ -1,100 +0,0 @@
.container {
max-width: 950px;
margin: 20px auto auto;
}
table {
width: 100%;
}
table tr td div {
max-height: 75px;
line-height: 25px;
word-wrap: break-word;
max-width: 575px;
overflow: auto;
}
td[name="deleteBtn"] {
text-align: center;
}
td[name="deleteBtn"] button {
border-radius: 50%;
border-style: solid;
cursor: pointer;
background-color: transparent;
}
input {
width: 65%;
}
form input[name="shortUrl"] {
text-transform: lowercase;
}
form input[name="shortUrl"]::placeholder {
text-transform: none;
}
div[name="github-link"] {
position: absolute;
right: 0.5%;
top: 0.5%;
}
.pure-table {
visibility: hidden;
}
.pure-table caption {
font-size: 22px;
text-align: left;
font-style: normal;
}
#logo {
font-size: 32px;
}
#password {
width: 100%;
margin-bottom: 10px;
}
dialog form {
text-align: center;
}
/* Settings for mobile devices */
@media (pointer:none),
(pointer:coarse) {
table tr {
border-bottom: 1px solid #999;
}
table thead {
display: none;
}
table td {
display: flex;
}
table td::before {
content: attr(label);
font-weight: bold;
width: 120px;
min-width: 120px;
text-align: left;
}
table tr td div {
width: 63vw
}
.pure-table caption {
padding-top: 0px;
}
}

View File

@@ -1,16 +1,75 @@
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
// SPDX-License-Identifier: MIT
use actix_session::Session;
use actix_web::HttpRequest;
use std::{env, time::SystemTime};
// API key generation and scoring
use passwords::{analyzer, scorer, PasswordGenerator};
// Validate API key
pub fn validate_key(key: String) -> bool {
if let Ok(api_key) = env::var("api_key") {
if api_key != key {
eprintln!("Incorrect API key was provided when connecting to Chhoto URL.");
false
} else {
eprintln!("Server accessed with API key.");
true
}
} else {
eprintln!("API was accessed with API key validation but no API key was specified. Set the 'api_key' environment variable.");
false
}
}
// Generate an API key if the user doesn't specify a secure key
// Called in main.rs
pub fn gen_key() -> String {
let key = PasswordGenerator {
length: 128,
numbers: true,
lowercase_letters: true,
uppercase_letters: true,
symbols: false,
spaces: false,
exclude_similar_characters: false,
strict: true,
};
key.generate_one().unwrap()
}
// Check if the API key header exists
pub fn api_header(req: &HttpRequest) -> Option<&str> {
req.headers().get("X-API-Key")?.to_str().ok()
}
// Determine whether the inputted API key is sufficiently secure
pub fn is_key_secure() -> bool {
let score = scorer::score(&analyzer::analyze(env::var("api_key").unwrap()));
score >= 90.0
}
// Validate a given password
pub fn validate(session: Session) -> bool {
// If there's no password provided, just return true
if env::var("password").is_err() {
if env::var("password")
.ok()
.filter(|s| !s.trim().is_empty())
.is_none()
{
return true;
}
let token = session.get::<String>("session-token");
token.is_ok() && check(token.unwrap())
if let Ok(token) = session.get::<String>("chhoto-url-auth") {
check(token)
} else {
false
}
}
// Check a token cryptographically
fn check(token: Option<String>) -> bool {
if let Some(token_body) = token {
let token_parts: Vec<&str> = token_body.split(';').collect();
@@ -23,15 +82,16 @@ fn check(token: Option<String>) -> bool {
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Time went backwards!")
.as_secs();
token_text == "session-token" && time_now < token_time + 1209600 // There are 1209600 seconds in 14 days
token_text == "chhoto-url-auth" && time_now < token_time + 1209600 // There are 1209600 seconds in 14 days
}
} else {
false
}
}
// Generate a new cryptographic token
pub fn gen_token() -> String {
let token_text = String::from("session-token");
let token_text = String::from("chhoto-url-auth");
let time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Time went backwards!")

View File

@@ -1,46 +1,72 @@
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
// SPDX-License-Identifier: MIT
use rusqlite::Connection;
use serde::Serialize;
pub fn find_url(shortlink: &str, db: &Connection) -> String {
let mut statement = db
.prepare_cached("SELECT long_url FROM urls WHERE short_url = ?1")
.unwrap();
let links = statement
.query_map([shortlink], |row| row.get("long_url"))
.unwrap();
let mut longlink = String::new();
for link in links {
longlink = link.unwrap();
}
longlink
// Struct for encoding a DB row
#[derive(Serialize)]
pub struct DBRow {
shortlink: String,
longlink: String,
hits: i64,
}
pub fn getall(db: &Connection) -> Vec<String> {
let mut statement = db.prepare_cached("SELECT * FROM urls").unwrap();
// Find a single URL
pub fn find_url(shortlink: &str, db: &Connection, needhits: bool) -> (Option<String>, Option<i64>) {
let query = if needhits {
"SELECT long_url,hits FROM urls WHERE short_url = ?1"
} else {
"SELECT long_url FROM urls WHERE short_url = ?1"
};
let mut statement = db
.prepare_cached(query)
.expect("Error preparing SQL statement for find_url.");
let mut data = statement.query([]).unwrap();
let longlink = statement
.query_row([shortlink], |row| row.get("long_url"))
.ok();
let hits = statement.query_row([shortlink], |row| row.get("hits")).ok();
(longlink, hits)
}
let mut links: Vec<String> = Vec::new();
while let Some(row) = data.next().unwrap() {
let short_url: String = row.get("short_url").unwrap();
let long_url: String = row.get("long_url").unwrap();
let hits: i64 = row.get("hits").unwrap();
links.push(format!("{short_url},{long_url},{hits}"));
// Get all URLs in DB
pub fn getall(db: &Connection) -> Vec<DBRow> {
let mut statement = db
.prepare_cached("SELECT * FROM urls ORDER BY id ASC")
.expect("Error preparing SQL statement for getall.");
let mut data = statement
.query([])
.expect("Error executing query for getall.");
let mut links: Vec<DBRow> = Vec::new();
while let Some(row) = data.next().expect("Error reading fetched rows.") {
let row_struct = DBRow {
shortlink: row
.get("short_url")
.expect("Error reading shortlink from row."),
longlink: row
.get("long_url")
.expect("Error reading shortlink from row."),
hits: row.get("hits").expect("Error reading shortlink from row."),
};
links.push(row_struct);
}
links
}
// Add a hit when site is visited
pub fn add_hit(shortlink: &str, db: &Connection) {
db.execute(
"UPDATE urls SET hits = hits + 1 WHERE short_url = ?1",
[shortlink],
)
.unwrap();
.expect("Error updating hit count.");
}
// Insert a new link
pub fn add_link(shortlink: String, longlink: String, db: &Connection) -> bool {
db.execute(
"INSERT INTO urls (long_url, short_url, hits) VALUES (?1, ?2, ?3)",
@@ -49,11 +75,16 @@ pub fn add_link(shortlink: String, longlink: String, db: &Connection) -> bool {
.is_ok()
}
pub fn delete_link(shortlink: String, db: &Connection) {
db.execute("DELETE FROM urls WHERE short_url = ?1", [shortlink])
.unwrap();
// Delete and existing link
pub fn delete_link(shortlink: String, db: &Connection) -> bool {
if let Ok(delta) = db.execute("DELETE FROM urls WHERE short_url = ?1", [shortlink]) {
delta > 0
} else {
false
}
}
// Open the DB, and create schema if missing
pub fn open_db(path: String) -> Connection {
let db = Connection::open(path).expect("Unable to open database!");
// Create table if it doesn't exist
@@ -66,6 +97,7 @@ pub fn open_db(path: String) -> Connection {
)",
[],
)
.unwrap();
.expect("Unable to initialize empty database.");
db
}

View File

@@ -1,17 +1,16 @@
use actix_files::{Files, NamedFile};
use actix_session::{storage::CookieSessionStore, Session, SessionMiddleware};
use actix_web::{
cookie::Key,
delete, get,
http::StatusCode,
middleware, post,
web::{self, Redirect},
App, HttpResponse, HttpServer, Responder,
};
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
// SPDX-License-Identifier: MIT
use actix_files::Files;
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::{cookie::Key, middleware, web, App, HttpServer};
use rusqlite::Connection;
use std::env;
use std::{env, io::Result};
// Import modules
mod auth;
mod database;
mod services;
mod utils;
// This struct represents state
@@ -19,133 +18,101 @@ struct AppState {
db: Connection,
}
// Define the routes
// Add new links
#[post("/api/new")]
async fn add_link(req: String, data: web::Data<AppState>, session: Session) -> HttpResponse {
if auth::validate(session) {
let out = utils::add_link(req, &data.db);
if out.0 {
HttpResponse::Ok().body(out.1)
} else {
HttpResponse::BadRequest().body(out.1)
}
} else {
HttpResponse::Forbidden().body("logged_out")
}
}
// Return all active links
#[get("/api/all")]
async fn getall(data: web::Data<AppState>, session: Session) -> HttpResponse {
if auth::validate(session) {
HttpResponse::Ok().body(utils::getall(&data.db))
} else {
HttpResponse::Forbidden().body("logged_out")
}
}
// Get the site URL
#[get("/api/siteurl")]
async fn siteurl(session: Session) -> HttpResponse {
if auth::validate(session) {
let site_url = env::var("site_url").unwrap_or(String::from("unset"));
HttpResponse::Ok().body(site_url)
} else {
HttpResponse::Forbidden().body("logged_out")
}
}
// 404 error page
async fn error404() -> impl Responder {
NamedFile::open_async("./resources/static/404.html")
.await
.customize()
.with_status(StatusCode::NOT_FOUND)
}
// Handle a given shortlink
#[get("/{shortlink}")]
async fn link_handler(shortlink: web::Path<String>, data: web::Data<AppState>) -> impl Responder {
let shortlink_str = shortlink.to_string();
let longlink = utils::get_longurl(shortlink_str, &data.db);
if longlink.is_empty() {
Redirect::to("/err/404")
} else {
let redirect_method = env::var("redirect_method").unwrap_or(String::from("PERMANENT"));
database::add_hit(shortlink.as_str(), &data.db);
if redirect_method == "TEMPORARY" {
Redirect::to(longlink)
} else {
// Defaults to permanent redirection
Redirect::to(longlink).permanent()
}
}
}
// Handle login
#[post("/api/login")]
async fn login(req: String, session: Session) -> HttpResponse {
if req == env::var("password").unwrap_or(req.clone()) {
// If no password was provided, match any password
session.insert("session-token", auth::gen_token()).unwrap();
HttpResponse::Ok().body("Correct password!")
} else {
eprintln!("Failed login attempt!");
HttpResponse::Forbidden().body("Wrong password!")
}
}
// Delete a given shortlink
#[delete("/api/del/{shortlink}")]
async fn delete_link(
shortlink: web::Path<String>,
data: web::Data<AppState>,
session: Session,
) -> HttpResponse {
if auth::validate(session) {
database::delete_link(shortlink.to_string(), &data.db);
HttpResponse::Ok().body("")
} else {
HttpResponse::Forbidden().body("Wrong password!")
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
async fn main() -> Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("warn"));
// Generate session key in runtime so that restart invalidates older logins
let secret_key = Key::generate();
let db_location = env::var("db_url").unwrap_or(String::from("/urls.sqlite"));
let db_location = env::var("db_url")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or(String::from("urls.sqlite"));
// Get the port environment variable
let port = env::var("port")
.unwrap_or(String::from("4567"))
.parse::<u16>()
.expect("Supplied port is not an integer");
let cache_control_header = env::var("cache_control_header")
.ok()
.filter(|s| !s.trim().is_empty());
// If an API key is set, check the security
if let Ok(key) = env::var("api_key") {
if !auth::is_key_secure() {
eprintln!("WARN: API key is insecure! Please change it. Current key is: {}. Generated secure key which you may use: {}", key, auth::gen_key())
} else {
eprintln!("Secure API key was provided.")
}
}
// If the site_url env variable exists
if let Some(site_url) = env::var("site_url").ok().filter(|s| !s.trim().is_empty()) {
// Get first and last characters of the site_url
let mut chars = site_url.chars();
let first = chars.next();
let last = chars.next_back();
let url = chars.as_str();
// If the site_url is encapsulated by quotes (i.e. invalid)
if first == Option::from('"') || first == Option::from('\'') && first == last {
// Set the site_url without the quotes
env::set_var("site_url", url);
eprintln!("WARN: The site_url environment variable is encapsulated by quotes. Automatically adjusting to {}", url);
// Tell the user what URI the server will respond with
eprintln!("INFO: Public URI is: {url}:{port}.")
} else {
// No issues
eprintln!("INFO: Configured Site URL is: {site_url}.");
// Tell the user what URI the server will respond with
eprintln!("INFO: Public URI is: {site_url}:{port}.")
}
} else {
// Site URL is not configured
eprintln!("WARN: The site_url environment variable is not configured. Defaulting to http://localhost");
eprintln!("INFO: Public URI is: http://localhost:{port}.")
}
// Tell the user that the server has started, and where it is listening to, rather than simply outputting nothing
eprintln!("Server has started at 0.0.0.0 on port {port}.");
// Actually start the server
HttpServer::new(move || {
App::new()
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone(),
))
// Maintain a single instance of database throughout
.app_data(web::Data::new(AppState {
db: database::open_db(env::var("db_url").unwrap_or(db_location.clone())),
}))
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default())
.service(link_handler)
.service(getall)
.service(siteurl)
.service(add_link)
.service(delete_link)
.service(login)
.wrap(
SessionMiddleware::builder(CookieSessionStore::default(), secret_key.clone())
.cookie_same_site(actix_web::cookie::SameSite::Strict)
.cookie_secure(false)
.build(),
)
// Maintain a single instance of database throughout
.app_data(web::Data::new(AppState {
db: database::open_db(db_location.clone()),
}))
.wrap(if let Some(header) = &cache_control_header {
middleware::DefaultHeaders::new().add(("Cache-Control", header.to_owned()))
} else {
middleware::DefaultHeaders::new()
})
.service(services::link_handler)
.service(services::getall)
.service(services::siteurl)
.service(services::version)
.service(services::add_link)
.service(services::delete_link)
.service(services::login)
.service(services::logout)
.service(services::expand)
.service(Files::new("/", "./resources/").index_file("index.html"))
.default_service(web::get().to(error404))
.default_service(actix_web::web::get().to(services::error404))
})
// Hardcode the port the server listens to. Allows for more intuitive Docker Compose port management
.bind(("0.0.0.0", port))?
.run()
.await

322
actix/src/services.rs Normal file
View File

@@ -0,0 +1,322 @@
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
// SPDX-License-Identifier: MIT
use actix_files::NamedFile;
use actix_session::Session;
use actix_web::{
delete, get,
http::StatusCode,
post,
web::{self, Redirect},
Either, HttpRequest, HttpResponse, Responder,
};
use std::env;
// Serialize JSON data
use serde::Serialize;
use crate::auth;
use crate::database;
use crate::utils;
use crate::AppState;
// Store the version number
const VERSION: &str = env!("CARGO_PKG_VERSION");
// Define JSON struct for returning JSON data
#[derive(Serialize)]
struct Response {
success: bool,
error: bool,
reason: String,
}
// Needed to return the short URL to make it easier for programs leveraging the API
#[derive(Serialize)]
struct CreatedURL {
success: bool,
error: bool,
shorturl: String,
}
// Struct for returning information about a shortlink
#[derive(Serialize)]
struct LinkInfo {
success: bool,
error: bool,
longurl: String,
hits: i64,
}
// Define the routes
// Add new links
#[post("/api/new")]
pub async fn add_link(
req: String,
data: web::Data<AppState>,
session: Session,
http: HttpRequest,
) -> HttpResponse {
// Call is_api_ok() function, pass HttpRequest
let result = utils::is_api_ok(http);
// If success, add new link
if result.success {
let out = utils::add_link(req, &data.db);
if out.0 {
let port = env::var("port")
.unwrap_or(String::from("4567"))
.parse::<u16>()
.expect("Supplied port is not an integer");
let mut url = format!(
"{}:{}",
env::var("site_url")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or(String::from("http://localhost")),
port
);
// If the port is 80, remove the port from the returned URL (better for copying and pasting)
// Return http://
if port == 80 {
url = env::var("site_url")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or(String::from("http://localhost"));
}
// If the port is 443, remove the port from the returned URL (better for copying and pasting)
// Return https://
if port == 443 {
url = env::var("site_url")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or(String::from("https://localhost"));
}
let response = CreatedURL {
success: true,
error: false,
shorturl: format!("{}/{}", url, out.1),
};
HttpResponse::Created().json(response)
} else {
let response = Response {
success: false,
error: true,
reason: out.1,
};
HttpResponse::Conflict().json(response)
}
} else if result.error {
HttpResponse::Unauthorized().json(result)
// If password authentication or public mode is used - keeps backwards compatibility
} else if env::var("public_mode") == Ok(String::from("Enable")) || auth::validate(session) {
let out = utils::add_link(req, &data.db);
if out.0 {
HttpResponse::Created().body(out.1)
} else {
HttpResponse::Conflict().body(out.1)
}
} else {
HttpResponse::Unauthorized().body("Not logged in!")
}
}
// Return all active links
#[get("/api/all")]
pub async fn getall(
data: web::Data<AppState>,
session: Session,
http: HttpRequest,
) -> HttpResponse {
// Call is_api_ok() function, pass HttpRequest
let result = utils::is_api_ok(http);
// If success, return all links
if result.success {
HttpResponse::Ok().body(utils::getall(&data.db))
} else if result.error {
HttpResponse::Unauthorized().json(result)
// If password authentication is used - keeps backwards compatibility
} else if auth::validate(session) {
HttpResponse::Ok().body(utils::getall(&data.db))
} else {
let body = if env::var("public_mode") == Ok(String::from("Enable")) {
"Using public mode."
} else {
"Not logged in!"
};
HttpResponse::Unauthorized().body(body)
}
}
// Get information about a single shortlink
#[post("/api/expand")]
pub async fn expand(req: String, data: web::Data<AppState>, http: HttpRequest) -> HttpResponse {
let result = utils::is_api_ok(http);
if result.success {
let linkinfo = utils::get_longurl(req, &data.db, true);
if let Some(longlink) = linkinfo.0 {
let body = LinkInfo {
success: true,
error: false,
longurl: longlink,
hits: linkinfo
.1
.expect("Error getting hit count for existing shortlink."),
};
HttpResponse::Ok().json(body)
} else {
let body = Response {
success: false,
error: true,
reason: "The shortlink does not exist on the server.".to_string(),
};
HttpResponse::Unauthorized().json(body)
}
} else {
HttpResponse::Unauthorized().json(result)
}
}
// Get the site URL
#[get("/api/siteurl")]
pub async fn siteurl() -> HttpResponse {
let site_url = env::var("site_url")
.ok()
.filter(|s| !s.trim().is_empty())
.unwrap_or(String::from("unset"));
HttpResponse::Ok().body(site_url)
}
// Get the version number
#[get("/api/version")]
pub async fn version() -> HttpResponse {
HttpResponse::Ok().body(VERSION)
}
// 404 error page
pub async fn error404() -> impl Responder {
NamedFile::open_async("./resources/static/404.html")
.await
.customize()
.with_status(StatusCode::NOT_FOUND)
}
// Handle a given shortlink
#[get("/{shortlink}")]
pub async fn link_handler(
shortlink: web::Path<String>,
data: web::Data<AppState>,
) -> impl Responder {
let shortlink_str = shortlink.to_string();
if let Some(longlink) = utils::get_longurl(shortlink_str, &data.db, false).0 {
let redirect_method = env::var("redirect_method").unwrap_or(String::from("PERMANENT"));
database::add_hit(shortlink.as_str(), &data.db);
if redirect_method == "TEMPORARY" {
Either::Left(Redirect::to(longlink))
} else {
// Defaults to permanent redirection
Either::Left(Redirect::to(longlink).permanent())
}
} else {
Either::Right(
NamedFile::open_async("./resources/static/404.html")
.await
.customize()
.with_status(StatusCode::NOT_FOUND),
)
}
}
// Handle login
#[post("/api/login")]
pub async fn login(req: String, session: Session) -> HttpResponse {
// Keep this function backwards compatible
if env::var("api_key").is_ok() {
if let Ok(password) = env::var("password") {
if password != req {
eprintln!("Failed login attempt!");
let response = Response {
success: false,
error: true,
reason: "Wrong password!".to_string(),
};
return HttpResponse::Unauthorized().json(response);
}
}
// Return Ok if no password was set on the server side
session
.insert("chhoto-url-auth", auth::gen_token())
.expect("Error inserting auth token.");
let response = Response {
success: true,
error: false,
reason: "Correct password!".to_string(),
};
HttpResponse::Ok().json(response)
} else {
if let Ok(password) = env::var("password") {
if password != req {
eprintln!("Failed login attempt!");
return HttpResponse::Unauthorized().body("Wrong password!");
}
}
// Return Ok if no password was set on the server side
session
.insert("chhoto-url-auth", auth::gen_token())
.expect("Error inserting auth token.");
HttpResponse::Ok().body("Correct password!")
}
}
// Handle logout
// There's no reason to be calling this route with an API key, so it is not necessary to check if the api_key env variable is set.
#[delete("/api/logout")]
pub async fn logout(session: Session) -> HttpResponse {
if session.remove("chhoto-url-auth").is_some() {
HttpResponse::Ok().body("Logged out!")
} else {
HttpResponse::Unauthorized().body("You don't seem to be logged in.")
}
}
// Delete a given shortlink
#[delete("/api/del/{shortlink}")]
pub async fn delete_link(
shortlink: web::Path<String>,
data: web::Data<AppState>,
session: Session,
http: HttpRequest,
) -> HttpResponse {
// Call is_api_ok() function, pass HttpRequest
let result = utils::is_api_ok(http);
// If success, delete shortlink
if result.success {
if utils::delete_link(shortlink.to_string(), &data.db) {
let response = Response {
success: true,
error: false,
reason: format!("Deleted {}", shortlink),
};
HttpResponse::Ok().json(response)
} else {
let response = Response {
success: false,
error: true,
reason: "The short link was not found, and could not be deleted.".to_string(),
};
HttpResponse::NotFound().json(response)
}
} else if result.error {
HttpResponse::Unauthorized().json(result)
// If "pass" is true - keeps backwards compatibility
} else if auth::validate(session) {
if utils::delete_link(shortlink.to_string(), &data.db) {
HttpResponse::Ok().body(format!("Deleted {shortlink}"))
} else {
HttpResponse::NotFound().body("Not found!")
}
} else {
HttpResponse::Unauthorized().body("Not logged in!")
}
}

View File

@@ -1,51 +1,154 @@
use crate::database;
use rand::seq::SliceRandom;
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
// SPDX-License-Identifier: MIT
use crate::{auth, database};
use actix_web::HttpRequest;
use nanoid::nanoid;
use rand::seq::IndexedRandom;
use regex::Regex;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::env;
pub fn get_longurl(shortlink: String, db: &Connection) -> String {
if validate_link(&shortlink) {
database::find_url(shortlink.as_str(), db)
// Struct for reading link pairs sent during API call
#[derive(Deserialize)]
struct URLPair {
shortlink: String,
longlink: String,
}
// Define JSON struct for response
#[derive(Serialize)]
pub struct Response {
pub(crate) success: bool,
pub(crate) error: bool,
reason: String,
pass: bool,
}
// If the api_key environment variable exists
pub fn is_api_ok(http: HttpRequest) -> Response {
// If the api_key environment variable exists
if env::var("api_key").is_ok() {
// If the header exists
if let Some(header) = auth::api_header(&http) {
// If the header is correct
if auth::validate_key(header.to_string()) {
Response {
success: true,
error: false,
reason: "Correct API key".to_string(),
pass: false,
}
} else {
Response {
success: false,
error: true,
reason: "Incorrect API key".to_string(),
pass: false,
}
}
// The header may not exist when the user logs in through the web interface, so allow a request with no header.
// Further authentication checks will be conducted in services.rs
} else {
// Due to the implementation of this result in services.rs, this JSON object will not be outputted.
Response {
success: false,
error: false,
reason: "X-API-Key header was not found".to_string(),
pass: true,
}
}
} else {
String::new()
// If the API key isn't set, but an API Key header is provided
if auth::api_header(&http).is_some() {
Response {
success: false,
error: true,
reason: "An API key was provided, but the 'api_key' environment variable is not configured in the Chhoto URL instance".to_string(),
pass: false
}
} else {
Response {
success: false,
error: false,
reason: "".to_string(),
pass: true,
}
}
}
}
// Request the DB for searching an URL
pub fn get_longurl(shortlink: String, db: &Connection, needhits: bool) -> (Option<String>, Option<i64>) {
if validate_link(&shortlink) {
database::find_url(shortlink.as_str(), db, needhits)
} else {
(None, None)
}
}
// Only have a-z, 0-9, - and _ as valid characters in a shortlink
fn validate_link(link: &str) -> bool {
let re = Regex::new("[a-z0-9-_]+").unwrap();
let re = Regex::new("^[a-z0-9-_]+$").expect("Regex generation failed.");
re.is_match(link)
}
// Request the DB for all URLs
pub fn getall(db: &Connection) -> String {
let links = database::getall(db);
links.join("\n")
serde_json::to_string(&links).expect("Failure during creation of json from db.")
}
// Make checks and then request the DB to add a new URL entry
pub fn add_link(req: String, db: &Connection) -> (bool, String) {
let chunks: Vec<&str> = req.split(';').collect();
let longlink = String::from(chunks[0]);
let mut shortlink;
if chunks.len() > 1 {
shortlink = chunks[1].to_string().to_lowercase();
if shortlink.is_empty() {
shortlink = random_name();
}
let mut chunks: URLPair;
if let Ok(json) = serde_json::from_str(&req) {
chunks = json;
} else {
shortlink = random_name();
// shorturl should always be supplied, even if empty
return (false, String::from("Invalid request!"));
}
if validate_link(shortlink.as_str()) && get_longurl(shortlink.clone(), db).is_empty() {
let style = env::var("slug_style").unwrap_or(String::from("Pair"));
let mut len = env::var("slug_length")
.ok()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(8);
if len < 4 {
len = 4;
}
if chunks.shortlink.is_empty() {
chunks.shortlink = gen_link(style, len);
}
if validate_link(chunks.shortlink.as_str())
&& get_longurl(chunks.shortlink.clone(), db, false).0.is_none()
{
(
database::add_link(shortlink.clone(), longlink, db),
shortlink,
database::add_link(chunks.shortlink.clone(), chunks.longlink, db),
chunks.shortlink,
)
} else {
(false, String::from("shortUrl not valid or already in use"))
(
false,
String::from("Short URL not valid or already in use!"),
)
}
}
fn random_name() -> String {
// Check if link, and request DB to delete it if exists
pub fn delete_link(shortlink: String, db: &Connection) -> bool {
if validate_link(shortlink.as_str()) {
database::delete_link(shortlink, db)
} else {
false
}
}
// Generate a random link using either adjective-name pair (default) of a slug or a-z, 0-9
fn gen_link(style: String, len: usize) -> String {
#[rustfmt::skip]
static ADJECTIVES: [&str; 108] = ["admiring", "adoring", "affectionate", "agitated", "amazing", "angry", "awesome", "beautiful",
"blissful", "bold", "boring", "brave", "busy", "charming", "clever", "compassionate", "competent", "condescending", "confident", "cool",
@@ -77,9 +180,21 @@ fn random_name() -> String {
"taussig", "tesla", "tharp", "thompson", "torvalds", "tu", "turing", "varahamihira", "vaughan", "vaughn", "villani", "visvesvaraya", "volhard",
"wescoff", "weierstrass", "wilbur", "wiles", "williams", "williamson", "wilson", "wing", "wozniak", "wright", "wu", "yalow", "yonath", "zhukovsky"];
format!(
"{0}-{1}",
NAMES.choose(&mut rand::thread_rng()).unwrap(),
ADJECTIVES.choose(&mut rand::thread_rng()).unwrap()
)
#[rustfmt::skip]
static CHARS: [char; 36] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x',
'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
if style == "UID" {
nanoid!(len, &CHARS)
} else {
format!(
"{0}-{1}",
ADJECTIVES
.choose(&mut rand::rng())
.expect("Error choosing random adjective."),
NAMES
.choose(&mut rand::rng())
.expect("Error choosing random name.")
)
}
}

View File

@@ -1,35 +1,72 @@
# SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
# SPDX-License-Identifier: MIT
services:
simply-shorten:
image: sintan1729/simply-shorten:latest
chhoto-url:
image: sintan1729/chhoto-url:latest
restart: unless-stopped
container_name: simply-shorten
container_name: chhoto-url
# You may enable the next two options if you want, but it may break the program if the db is bind
# mounted from the system. It does add extra security, but I don't know enough about docker
# to help in case it breaks something.
# read_only: true
# cap_drop:
# - ALL
ports:
# If you changed the "port" environment variable, adjust accordingly
# The number AFTER the colon should match the "port" variable and the number
# before the colon is the port where you would access the container from outside.
- 4567:4567
environment:
# Change if you want to mount the database somewhere else
# Change if you want to mount the database somewhere else.
# In this case, you can get rid of the db volume below
# and instead do a mount manually by specifying the location
# - db_url=/urls.sqlite
# and instead do a mount manually by specifying the location.
# Make sure that you create an empty file with the correct name
# before starting the container if you do make any changes.
# (In fact, I'd suggest that you do that so that you can keep
# a copy of your database.)
- db_url=/db/urls.sqlite
# Change it in case you want to set the website name
# displayed in front of the shorturls, defaults to
# the hostname you're accessing it from
# Change this if your server URL is not "http://localhost"
# This must not be surrounded by quotes. For example:
# site_url="https://www.example.com" incorrect
# site_url=https://www.example.com correct
# This is important to ensure Chhoto URL outputs the shortened link with the correct URL.
# - site_url=https://www.example.com
- password=$3CuReP4S$W0rD
# Change this if you are running Chhoto URL on a port which is not 4567.
# This is important to ensure Chhoto URL outputs the shortened link with the correct port.
# - port=4567
# Pass the redirect method, if needed TEMPORARY and PERMANENT
# are accepted values, defaults to PERMANENT
- password=TopSecretPass
# This needs to be set in order to use programs that use the JSON interface of Chhoto URL.
# You will get a warning if this is insecure, and a generated value will be output
# You may use that value if you can't think of a secure key
# - api_key=SECURE_API_KEY
# Pass the redirect method, if needed. TEMPORARY and PERMANENT
# are accepted values, defaults to PERMANENT.
# - redirect_method=TEMPORARY
# By default, the auto-generated pairs are adjective-name pairs.
# If you want UIDs, please change slug_style to UID.
# Supported values for slug_style are Pair and UID.
# The length is 8 by default, and a minimum of 4 is allowed.
# - slug_style=Pair
# - slug_length=8
# In case you want to provide public access to adding links (and not
# delete, or listing), change the following option to Enable.
# - public_mode=Disable
# By default, the server sends no Cache-Control headers. You can supply a
# comma separated list of valid header as per RFC 7234 §5.2 to send those
# headers instead.
# - cache_control_header=no-cache, private
volumes:
- db:/urls.sqlite
networks:
- proxy
- db:/db
volumes:
db:
networks:
proxy:
external: true

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,24 +1,27 @@
<!-- SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> -->
<!-- SPDX-License-Identifier: MIT -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta charset="UTF-8" />
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Simply Shorten</title>
<meta name="description" content="A simple selfhosted URL shortener with no unnecessary features.">
<meta name="keywords" content="url shortener, link shortener, self hosted, open source">
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" sizes="any">
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link rel="icon" type="image/png" href="assets/favicon-32.png" sizes="32x32">
<link rel="icon" type="image/png" href="assets/favicon-196.png" sizes="196x196">
<title>Chhoto URL</title>
<meta name="description" content="A simple selfhosted URL shortener with no unnecessary features." />
<meta name="keywords" content="url shortener, link shortener, self hosted, open source" />
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" sizes="any" />
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg" />
<link rel="icon" type="image/png" href="assets/favicon-32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="assets/favicon-196.png" sizes="196x196" />
<script src="static/script.js"></script>
<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.1/build/pure-min.css"
integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" target="_blank" href="static/styles.css">
integrity="sha384-oAOxQR6DkCoMliIh8yFnu25d7Eq/PHS21PClpwjOTeU2jRSq11vu66rf90/cZr47" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" target="_blank" href="static/styles.css" />
</head>
<body>
@@ -26,7 +29,7 @@
<div class="container" id="container">
<form class="pure-form pure-form-aligned" name="new-url-form">
<fieldset>
<legend id="logo"><img src="assets/favicon-32.png" width="26px" alt="logo"> Simply Shorten</legend>
<legend id="logo"><img src="assets/favicon-32.png" width="26px" alt="logo"> Chhoto URL</legend>
<div class="pure-control-group">
<label for="longUrl">Long URL</label>
<input type="url" name="longUrl" id="longUrl" placeholder="Please enter a valid URL"
@@ -35,24 +38,24 @@
<div class=" pure-control-group">
<label for="shortUrl">Short URL (optional)</label>
<input type="text" name="shortUrl" id="shortUrl" placeholder="Only a-z, 0-9, - and _ are allowed"
pattern="[A-Za-z0-9_-]+" />
pattern="[a-z0-9\-_]+" title="Only a-z, 0-9, - and _ are allowed" autocapitalize="off"/>
</div>
<div class="pure-controls">
<div class="pure-controls" id="controls">
<button class="pure-button pure-button-primary">Shorten!</button>
<p id="alert-box">&nbsp;</p>
</div>
</fieldset>
</form>
<p name="loading-text">Loading links table...</p>
<table class="pure-table">
<p id="loading-text">Loading links table...</p>
<table class="pure-table" id="table-box" hidden>
<caption>Active links</caption>
<br>
<br />
<thead>
<tr>
<td id="short-url-header">Short URL<br>(click to copy)</td>
<td id="short-url-header">Short URL (click to copy)</td>
<td>Long URL</td>
<td>Hits</td>
<td name="hitsColumn">Hits</td>
<td name="deleteBtn">&times;</td>
</tr>
</thead>
@@ -62,19 +65,23 @@
</table>
</div>
<div name="github-link">
<a href="https://github.com/SinTan1729/simply-shorten" target="_blank" rel="noopener noreferrer">Source Code</a>
<div name="links-div">
<a id="admin-button" href="javascript:getLogin()" hidden>login</a>
&nbsp;
<a id="version-number" href="https://github.com/SinTan1729/chhoto-url" target="_blank" rel="noopener noreferrer"
hidden>Source Code</a>
<!-- The version number would be inserted here -->
</div>
<dialog id="login-dialog">
<form class="pure-form" name="login-form">
<p>Please enter password to access this website</p>
<input type="password" id="password" />
<button class="pure-button pure-button-primary" value="default">Submit</button>
<p id="wrong-pass">&nbsp;</p>
<button class="pure-button pure-button-primary" value="default">Log in</button>
<p id="wrong-pass" hidden>Wrong password!</p>
</form>
</dialog>
</body>
</html>
</html>

42
resources/static/404.html Normal file
View File

@@ -0,0 +1,42 @@
<!-- SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> -->
<!-- SPDX-License-Identifier: MIT -->
<!DOCTYPE html>
<html>
<head>
<title>Error 404</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
</head>
<style>
body {
text-align: center;
}
#quote {
text-indent: 4em;
}
/* Settings for mobile devices */
@media (pointer:none),
(pointer:coarse) {
body {
text-align: left;
}
}
</style>
<body>
<h1>Error 404!</h1>
<div style="display: inline-block; text-align:left;">
<p>You step in the stream,</p>
<p>But the water has moved on.</p>
<p>The page is not here.</p>
<p id="quote"> — Cass Whittington</p>
</div>
</body>
</html>

268
resources/static/script.js Normal file
View File

@@ -0,0 +1,268 @@
// SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com>
// SPDX-License-Identifier: MIT
const prepSubdir = (link) => {
let thisPage = new URL(window.location.href);
let subdir = thisPage.pathname;
let out = (subdir + link).replace('//', '/');
console.log(out);
return (subdir + link).replace('//', '/');
}
const getSiteUrl = async () => {
let url = await fetch(prepSubdir("/api/siteurl"))
.then(res => res.text());
if (url == "unset") {
return window.location.host.replace(/\/$/, '');
}
else {
return url.replace(/\/$/, '').replace(/^"/, '').replace(/"$/, '');
}
}
const getVersion = async () => {
let ver = await fetch(prepSubdir("/api/version"))
.then(res => res.text());
return ver;
}
const showVersion = async () => {
let version = await getVersion();
link = document.getElementById("version-number");
link.innerText = "v" + version;
link.href = "https://github.com/SinTan1729/chhoto-url/releases/tag/" + version;
link.hidden = false;
}
const getLogin = async () => {
document.getElementById("container").style.filter = "blur(2px)";
document.getElementById("login-dialog").showModal();
document.getElementById("password").focus();
}
const refreshData = async () => {
let res = await fetch(prepSubdir("/api/all"));
if (!res.ok) {
let errorMsg = await res.text();
document.getElementById("url-table").innerHTML = '';
console.log(errorMsg);
if (errorMsg == "Using public mode.") {
document.getElementById("admin-button").hidden = false;
loading_text = document.getElementById("loading-text");
loading_text.hidden = true;
showVersion();
} else {
getLogin();
}
} else {
let data = await res.json();
displayData(data.reverse());
}
}
const displayData = async (data) => {
showVersion();
let site = await getSiteUrl();
admin_button = document.getElementById("admin-button");
admin_button.innerText = "logout";
admin_button.href = "javascript:logOut()";
admin_button.hidden = false;
table_box = document.getElementById("table-box");
loading_text = document.getElementById("loading-text");
const table = document.getElementById("url-table");
if (data.length == 0) {
table_box.hidden = true;
loading_text.innerHTML = "No active links.";
loading_text.hidden = false;
}
else {
loading_text.hidden = true;
if (!window.isSecureContext) {
const shortUrlHeader = document.getElementById("short-url-header");
shortUrlHeader.innerHTML = "Short URL<br>(right click and copy)";
}
table_box.hidden = false;
table.innerHTML = '';
data.forEach(tr => table.appendChild(TR(tr, site)));
}
}
const showAlert = async (text, col) => {
document.getElementById("alert-box")?.remove();
const controls = document.getElementById("controls");
const alertBox = document.createElement("p");
alertBox.id = "alert-box";
alertBox.style.color = col;
alertBox.innerHTML = text;
controls.appendChild(alertBox);
}
const TR = (row, site) => {
const tr = document.createElement("tr");
const longTD = TD(A_LONG(row["longlink"]), "Long URL");
var shortTD = null;
var isSafari = /Safari/.test(navigator.userAgent) && /Apple Computer/.test(navigator.vendor);
// For now, we disable copying on WebKit due to a possible bug. Manual copying is enabled instead.
// Take a look at https://github.com/SinTan1729/chhoto-url/issues/36
if (window.isSecureContext && !(isSafari)) {
shortTD = TD(A_SHORT(row["shortlink"], site), "Short URL");
}
else {
shortTD = TD(A_SHORT_INSECURE(row["shortlink"], site), "Short URL");
}
let hitsTD = TD(row["hits"]);
hitsTD.setAttribute("label", "Hits");
hitsTD.setAttribute("name", "hitsColumn");
const btn = deleteButton(row["shortlink"]);
tr.appendChild(shortTD);
tr.appendChild(longTD);
tr.appendChild(hitsTD);
tr.appendChild(btn);
return tr;
}
const copyShortUrl = async (link) => {
const site = await getSiteUrl();
try {
navigator.clipboard.writeText(`${site}/${link}`);
showAlert(`Short URL ${link} was copied to clipboard!`, "light-dark(green, #72ff72)");
} catch (e) {
console.log(e);
showAlert(`Could not copy short URL to clipboard, please do it manually: <a href=${site}/${link}>${site}/${link}</a>`, "light-dark(red, #ff1a1a)");
}
}
const addProtocol = (input) => {
var url = input.value.trim();
if (url != "" && !~url.indexOf("://") && !~url.indexOf("magnet:")) {
url = "https://" + url;
}
input.value = url;
return input;
}
const A_LONG = (s) => `<a href='${s}'>${s}</a>`;
const A_SHORT = (s, t) => `<a href="javascript:copyShortUrl('${s}');">${s}</a>`;
const A_SHORT_INSECURE = (s, t) => `<a href="${t}/${s}">${s}</a>`;
const deleteButton = (shortUrl) => {
const td = document.createElement("td");
const div = document.createElement("div");
const btn = document.createElement("button");
btn.innerHTML = "&times;";
btn.onclick = e => {
e.preventDefault();
if (confirm("Do you want to delete the entry " + shortUrl + "?")) {
document.getElementById("alert-box")?.remove();
showAlert("&nbsp;", "black");
fetch(prepSubdir(`/api/del/${shortUrl}`), {
method: "DELETE"
}).then(res => {
if (res.ok) {
console.log("Deleted " + shortUrl);
} else {
console.log("Unable to delete " + shortUrl);
}
refreshData();
});
}
};
td.setAttribute("name", "deleteBtn");
td.setAttribute("label", "Delete");
div.appendChild(btn);
td.appendChild(div);
return td;
}
const TD = (s, u) => {
const td = document.createElement("td");
const div = document.createElement("div");
div.innerHTML = s;
td.appendChild(div);
td.setAttribute("label", u);
return td;
}
const submitForm = () => {
const form = document.forms.namedItem("new-url-form");
const data = {
"longlink": form.elements["longUrl"].value,
"shortlink": form.elements["shortUrl"].value,
};
const url = prepSubdir("/api/new");
let ok = false;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then(res => {
ok = res.ok;
return res.text();
})
.then(text => {
if (!ok) {
showAlert(text, "light-dark(red, #ff1a1a)");
}
else {
copyShortUrl(text);
longUrl.value = "";
shortUrl.value = "";
refreshData();
}
})
}
const submitLogin = () => {
const password = document.getElementById("password");
fetch(prepSubdir("/api/login"), {
method: "POST",
body: password.value
}).then(res => {
if (res.ok) {
document.getElementById("container").style.filter = "blur(0px)"
document.getElementById("login-dialog").close();
password.value = '';
document.getElementById("wrong-pass").hidden = true;
refreshData();
} else {
document.getElementById("wrong-pass").hidden = false;
password.focus();
}
})
}
const logOut = async () => {
let reply = await fetch(prepSubdir("/api/logout"), {method: "DELETE"}).then(res => res.text());
console.log(reply);
document.getElementById("table-box").hidden = true;
document.getElementById("loading-text").hidden = false;
refreshData();
}
(async () => {
await refreshData();
const form = document.forms.namedItem("new-url-form");
form.onsubmit = e => {
e.preventDefault();
submitForm();
}
const login_form = document.forms.namedItem("login-form");
login_form.onsubmit = e => {
e.preventDefault();
submitLogin();
}
})()

183
resources/static/styles.css Normal file
View File

@@ -0,0 +1,183 @@
/* SPDX-FileCopyrightText: 2023 Sayantan Santra <sayantan.santra689@gmail.com> */
/* SPDX-License-Identifier: MIT */
@font-face {
font-family: Montserrat;
src: url('/assets/Montserrat-VF.woff2');
}
:root {
color-scheme: light dark;
}
body {
color: light-dark(black, #e8e6e3);
background-color: light-dark(white, #181a1b);
}
.pure-button {
background-color: light-dark(#0078e7, #0060b9);
}
input {
border-color: light-dark(#cccccc, #3e4446) !important;
box-shadow: light-dark(#dddddd, #2b2f31) 0px 1px 3px inset !important;
}
::placeholder {
color: light-dark(#757575, #636061);
}
legend {
color: light-dark(#333333, #c8c3bc) !important;
border-bottom-color: light-dark(#e5e5e5 ,#373c3e) !important;
}
* {
font-family: Montserrat;
}
.container {
max-width: 1200px;
margin: 20px auto auto;
}
a {
color: light-dark(blue, #3391ff);
}
table tr td div {
max-height: 75px;
line-height: 25px;
word-wrap: break-word;
max-width: 575px;
overflow: auto;
}
.pure-table {
border-color: light-dark(black, #867d6e);
}
.pure-table caption {
color: light-dark(black, #e8e6e3);
}
.pure-table thead {
color: light-dark(black, #e8e6e3);
background-color: light-dark(#e0e0e0, #2a2d2f);
}
.pure-table td {
border-left: none;
}
td[name="hitsColumn"] {
text-align: right;
}
td[name="deleteBtn"] div {
display: flex;
align-items: center;
justify-content: center;
}
td[name="deleteBtn"] {
text-align: center;
}
td[name="deleteBtn"] div button {
border-radius: 100%;
aspect-ratio: 1;
border-style: solid;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
background-color: transparent;
}
input {
width: 65%;
}
form input[name="shortUrl"]::placeholder {
text-transform: none;
}
div[name="links-div"] {
position: absolute;
right: 0.5%;
top: 0.5%;
}
.pure-table {
width: 98%;
}
.pure-table caption {
font-size: 22px;
text-align: left;
font-style: normal;
}
#logo {
font-size: 32px;
}
#password {
width: 100%;
margin-bottom: 10px;
}
#login-dialog {
border-radius: 10px;
border-width: 2px;
}
#login-dialog form {
text-align: center;
}
#wrong-pass {
color: light-dark(red, #ff1a1a);
}
/* Settings for mobile devices */
@media (pointer:none),
(pointer:coarse) {
.container {
max-width: 100vw;
}
.pure-control-group input {
width: 98%;
}
table tr {
border-bottom: 1px solid #999;
}
table thead {
display: none;
}
table td {
display: flex;
justify-content: left !important;
width: 98vw;
padding: .5em .1em !important;
}
table td::before {
content: attr(label);
font-weight: bold;
width: 120px;
min-width: 120px;
text-align: left;
}
.pure-table caption {
padding-top: 0px;
}
}

BIN
screenshot-desktop.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

BIN
screenshot-mobile.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB