7 Commits

Author SHA1 Message Date
Zoe
5b8177bd12 improve database handling and category management, enhance admin UI with animations
Some checks failed
Build and Push Docker Image to GHCR / build-and-push (push) Failing after 2m19s
2025-09-22 18:49:53 -05:00
Zoe
8c18e81358 docker releases 2025-09-22 18:49:05 -05:00
Zoe
0558c719d6 bump(deps): fiber 2025-09-22 18:32:58 -05:00
Zoe
d31f24c101 I wish it was impossible to make this mistake 2025-04-19 01:08:38 -05:00
Zoe
d8f3c4fd69 fix styling on cards 2025-04-09 13:08:07 +00:00
b0e891a59b Update README.md 2025-04-09 04:55:12 -05:00
Zoe
bf466b26ac Better docs, bug fixes, and config structure
The project now uses a more sensible config structure to allow for
actual defaults. Passport now has NO reliance on javascript whatsoever,
and can be used identically without javascript enabled.
2025-04-09 04:48:38 -05:00
14 changed files with 580 additions and 260 deletions

View File

@@ -1,7 +1,8 @@
PASSPORT_ENABLE_WEATHER=true
OPENWEATHER_API_KEY=1234567890
OPENWEATHER_LAT=34.052235
OPENWEATHER_LON=-118.243683
OPENWEATHER_UPDATE_INTERVAL=15
PASSPORT_ADMIN_USERNAME=admin
PASSPORT_ADMIN_PASSWORD=P@ssw0rd
PASSPORT_SEARCH_PROVIDER=https://google.com/search?q=%s
PASSPORT_SEARCH_PROVIDER=https://google.com/search

39
.github/workflows/docker-publish.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Build and Push Docker Image to GHCR
on:
push:
branches: ["main"]
tags: ["v*.*.*"]
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

4
.gitignore vendored
View File

@@ -1,6 +1,8 @@
passport
.env
passport.db*
public/uploads/
public
zqdgr
# compiled via go prepare
assets/tailwind.css

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM golang:1.25 AS builder
WORKDIR /app
ENV CGO_ENABLED=1 GOOS=linux GOARCH=amd64
RUN apt-get update && apt-get install -y gcc libc6-dev sqlite3 ca-certificates
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -ldflags="-w -s" -o passport
# ---- Runtime Stage ----
FROM gcr.io/distroless/cc-debian12
WORKDIR /data
COPY --from=builder /app/passport /usr/local/bin/passport
EXPOSE 3000
CMD ["/usr/local/bin/passport"]

View File

@@ -6,7 +6,7 @@ Passport is a simple, fast, and lightweight web dashboard/new tab replacement.
## Getting Started
![Screenshot 2025-03-28 at 07-44-22 Passport](https://github.com/user-attachments/assets/d31b0694-3445-46f8-af01-158703e44b4c)
![Screenshot of Passport](/screenshot.png)
### Prerequisites
@@ -15,33 +15,70 @@ Passport is a simple, fast, and lightweight web dashboard/new tab replacement.
- [sqlite3](https://www.sqlite.org/download.html)
- [TailwdinCSS CLI](https://github.com/tailwindlabs/tailwindcss/releases/latest)
### Usage
## Usage
1. Clone the repository
2. Configure the `.env` file, an example is provided in the `.env.example` file, see below for every available environment variable
4. Deploy `passport` to your web server
5. profit
### Docker
#### Configuration
Passport is available as a Docker image via this repository. This is the recommended way to run Passport.
| Environment Variable | Description | Required | Default |
| --- | --- | --- | --- |
| `PASSPORT_DEV_MODE` | Enables dev mode | false | false |
| `PASSPORT_ENABLE_WEATHER` | Enables weather data, requires an OpenWeather API key | false | true |
| `PASSPORT_ENABLE_UPTIME` | Enables uptime data, requires an UptimeRobot API key | false | true |
| `PASSPORT_ADMIN_USERNAME` | The username for the admin dashboard | true |
| `PASSPORT_ADMIN_PASSWORD` | The password for the admin dashboard | true |
| `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar | true |
| `OPENWEATHER_API_KEY` | The OpenWeather API key | if enabled |
| `OPENWEATHER_LAT` | The latitude of your location | if enabled |
| `OPENWEATHER_LON` | The longitude of your location | if enabled |
| `OPENWEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
| `UPTIMEROBOT_API_KEY` | The UptimeRobot API key | if enabled |
| `UPTIMEROBOT_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
```bash
docker run -d --name passport -p 3000:3000 -e PASSPORT_ADMIN_USERNAME=admin -e PASSPORT_ADMIN_PASSWORD=password ghcr.io/juls0730/passport:latest
```
### Building from source
If you want to build from source, you will need to install the dependencies first.
```bash
go install github.com/juls0730/zqdgr@latest
go install github.com/tailwindlabs/tailwindcss-cli@latest
```
Then you can build the binary.
```bash
go build -o passport
```
You can then run the binary.
### Configuration
#### Passport configuration
| Environment Variable | Description | Required | Default |
| -------------------------------------- | ------------------------------------------------------------------------------- | -------- | ------- |
| `PASSPORT_DEV_MODE` | Enables dev mode | false | false |
| `PASSPORT_ENABLE_PREFORK` | Enables preforking | false | false |
| `PASSPORT_ENABLE_WEATHER` | Enables weather data, see [Weather configuration](#weather-configuration) | false | false |
| `PASSPORT_ENABLE_UPTIME` | Enables uptime data, see [Uptime configuration](#uptime-configuration) | false | false |
| `PASSPORT_ADMIN_USERNAME` | The username for the admin dashboard | true |
| `PASSPORT_ADMIN_PASSWORD` | The password for the admin dashboard | true |
| `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar, without any query parameters | true |
| `PASSPORT_SEARCH_PROVIDER_QUERY_PARAM` | The query parameter to use for the search provider, e.g. `q` for most providers | false | q |
#### Weather configuration
| Environment Variable | Description | Required | Default |
| ----------------------------- | ------------------------------------------------------------------------- | ---------- | -------------- |
| `OPENWEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | true | openweathermap |
| `OPENWEATHER_API_KEY` | The OpenWeather API key | if enabled | |
| `OPENWEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric |
| `OPENWEATHER_LAT` | The latitude of your location | if enabled | |
| `OPENWEATHER_LON` | The longitude of your location | if enabled | |
| `OPENWEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
#### Uptime configuration
| Environment Variable | Description | Required | Default |
| ----------------------------- | ------------------------------------------------- | ---------- | ------- |
| `UPTIMEROBOT_API_KEY` | The UptimeRobot API key | if enabled | |
| `UPTIMEROBOT_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
### Adding links and categories
The admin dashboard can be accessed at `/admin`, you will be redirected to the login page if you are not logged in, use the credentials you configured in the `.env` file to login. Once logged in you can add links and categories.
The admin dashboard can be accessed at `/admin`, you will be redirected to the login page if you are not logged in, use
the credentials you configured via the environment variables to login. Once logged in you can add links and categories.
## License

View File

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

32
go.mod
View File

@@ -1,40 +1,42 @@
module github.com/juls0730/passport
go 1.23.2
go 1.25.0
require github.com/caarlos0/env/v11 v11.3.1
require (
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gofiber/schema v1.2.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/gofiber/schema v1.6.0 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/tinylib/msgp v1.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
require (
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/chai2010/webp v1.1.1
github.com/gofiber/fiber/v2 v2.52.5 // indirect
github.com/gofiber/fiber/v3 v3.0.0-beta.4
github.com/gofiber/fiber/v3 v3.0.0-rc.1
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/template/handlebars/v2 v2.1.10
github.com/gofiber/utils v1.1.0 // indirect
github.com/gofiber/utils/v2 v2.0.0-beta.7 // indirect
github.com/gofiber/utils/v2 v2.0.0-rc.1 // indirect
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mattn/go-sqlite3 v1.14.24
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.58.0 // indirect
github.com/valyala/fasthttp v1.66.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)

47
go.sum
View File

@@ -1,7 +1,9 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -9,36 +11,42 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gofiber/fiber/v3 v3.0.0-beta.3 h1:7Q2I+HsIqnIEEDB+9oe7Gadpakh6ZLhXpTYz/L20vrg=
github.com/gofiber/fiber/v3 v3.0.0-beta.3/go.mod h1:kcMur0Dxqk91R7p4vxEpJfDWZ9u5IfvrtQc8Bvv/JmY=
github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0=
github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk=
github.com/gofiber/fiber/v3 v3.0.0-rc.1 h1:034MxesK6bqGkidP+QR+Ysc1ukOacBWOHCarCKC1xfg=
github.com/gofiber/fiber/v3 v3.0.0-rc.1/go.mod h1:hFdT00oT0XVuQH1/z2i5n1pl/msExHDUie1SsLOkCuM=
github.com/gofiber/schema v1.2.0 h1:j+ZRrNnUa/0ZuWrn/6kAtAufEr4jCJ+JuTURAMxNSZg=
github.com/gofiber/schema v1.2.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/handlebars/v2 v2.1.10 h1:Qc+uUMULCqW60LF4VKO1REpiyDAUy3vqW7xq66FPJGM=
github.com/gofiber/template/handlebars/v2 v2.1.10/go.mod h1:84WH9st5OJi255EGjuMAOqUVQ+Q2jUNhUKYbS5DgAcI=
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/gofiber/utils/v2 v2.0.0-beta.4 h1:1gjbVFFwVwUb9arPcqiB6iEjHBwo7cHsyS41NeIW3co=
github.com/gofiber/utils/v2 v2.0.0-beta.4/go.mod h1:sdRsPU1FXX6YiDGGxd+q2aPJRMzpsxdzCXo9dz+xtOY=
github.com/gofiber/utils/v2 v2.0.0-beta.7 h1:NnHFrRHvhrufPABdWajcKZejz9HnCWmT/asoxRsiEbQ=
github.com/gofiber/utils/v2 v2.0.0-beta.7/go.mod h1:J/M03s+HMdZdvhAeyh76xT72IfVqBzuz/OJkrMa7cwU=
github.com/gofiber/utils/v2 v2.0.0-rc.1 h1:b77K5Rk9+Pjdxz4HlwEBnS7u5nikhx7armQB8xPds4s=
github.com/gofiber/utils/v2 v2.0.0-rc.1/go.mod h1:Y1g08g7gvST49bbjHJ1AVqcsmg93912R/tbKWhn6V3E=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqACtjw=
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@@ -50,6 +58,8 @@ github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
@@ -59,35 +69,44 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/tinylib/msgp v1.4.0 h1:SYOeDRiydzOw9kSiwdYp9UcBgPFtLU2WDHaJXyHruf8=
github.com/tinylib/msgp v1.4.0/go.mod h1:cvjFkb4RiC8qSBOPMGPSzSAx47nAsfhLVTCZZNuHv5o=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.55.0 h1:Zkefzgt6a7+bVKHnu/YaYSOPfNYNisSVBo/unVCf8k8=
github.com/valyala/fasthttp v1.55.0/go.mod h1:NkY9JtkrpPKmgwV3HTaS2HWaJss9RSIsRVfcxxoHiOM=
github.com/valyala/fasthttp v1.58.0 h1:GGB2dWxSbEprU9j0iMJHgdKYJVDyjrOwF9RE59PbRuE=
github.com/valyala/fasthttp v1.58.0/go.mod h1:SYXvHHaFp7QZHGKSHmoMipInhrI5StHrhDTYVEjK/Kw=
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

449
main.go
View File

@@ -19,12 +19,15 @@ import (
"mime/multipart"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/caarlos0/env/v11"
"github.com/chai2010/webp"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/helmet"
@@ -67,15 +70,116 @@ var (
insertLinkStmt *sql.Stmt
)
type WeatherProvider string
const (
OpenWeatherMap WeatherProvider = "openweathermap"
)
type WeatherConfig struct {
Provider WeatherProvider `env:"OPENWEATHER_PROVIDER" envDefault:"openweathermap"`
OpenWeather struct {
APIKey string `env:"OPENWEATHER_API_KEY"`
Units string `env:"OPENWEATHER_TEMP_UNITS" envDefault:"metric"`
Lat float64 `env:"OPENWEATHER_LAT"`
Lon float64 `env:"OPENWEATHER_LON"`
}
UpdateInterval int `env:"OPENWEATHER_UPDATE_INTERVAL" envDefault:"15"`
}
type UptimeConfig struct {
APIKey string `env:"UPTIMEROBOT_API_KEY"`
UpdateInterval int `env:"UPTIMEROBOT_UPDATE_INTERVAL" envDefault:"300"`
}
type Config struct {
DevMode bool `env:"PASSPORT_DEV_MODE" envDefault:"false"`
Prefork bool `env:"PASSPORT_ENABLE_PREFORK" envDefault:"false"`
WeatherEnabled bool `env:"PASSPORT_ENABLE_WEATHER" envDefault:"false"`
Weather *WeatherConfig
UptimeEnabled bool `env:"PASSPORT_ENABLE_UPTIME" envDefault:"false"`
Uptime *UptimeConfig
Admin struct {
Username string `env:"PASSPORT_ADMIN_USERNAME"`
Password string `env:"PASSPORT_ADMIN_PASSWORD"`
}
SearchProvider struct {
URL string `env:"PASSPORT_SEARCH_PROVIDER"`
Query string `env:"PASSPORT_SEARCH_PROVIDER_QUERY_PARAM" envDefault:"q"`
}
}
func ParseConfig() (*Config, error) {
config := Config{}
err := env.Parse(&config)
if err != nil {
return nil, err
}
if config.WeatherEnabled {
config.Weather = &WeatherConfig{}
if err := env.Parse(config.Weather); err != nil {
return nil, err
}
}
if config.UptimeEnabled {
config.Uptime = &UptimeConfig{}
if err := env.Parse(config.Uptime); err != nil {
return nil, err
}
}
return &config, nil
}
type App struct {
*WeatherCache
*Config
*CategoryManager
*WeatherCache
*UptimeManager
db *sql.DB
}
func NewApp(dbPath string) (*App, error) {
db, err := sql.Open("sqlite3", dbPath)
func (app *App) Close() error {
return app.db.Close()
}
func NewApp(dbPath string, options map[string]any) (*App, error) {
config, err := ParseConfig()
if err != nil {
return nil, err
}
file, err := os.OpenFile(dbPath, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
if os.IsPermission(err) {
return nil, fmt.Errorf("file %s is not readable and writable: %v", dbPath, err)
}
return nil, fmt.Errorf("failed to open file %s for read/write: %v", dbPath, err)
}
defer file.Close()
var connectionOpts string
for k, v := range options {
if connectionOpts != "" {
connectionOpts += "&"
}
connectionOpts += fmt.Sprintf("%s=%v", k, v)
}
db, err := sql.Open("sqlite3", fmt.Sprintf("%s?%s", dbPath, connectionOpts))
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
@@ -96,19 +200,21 @@ func NewApp(dbPath string) (*App, error) {
}
var weatherCache *WeatherCache
if os.Getenv("PASSPORT_ENABLE_WEATHER") != "false" {
weatherCache = NewWeatherCache()
if config.WeatherEnabled {
weatherCache = NewWeatherCache(config.Weather)
}
var uptimeManager *UptimeManager
if os.Getenv("PASSPORT_ENABLE_UPTIME") != "false" {
uptimeManager = NewUptimeManager()
if config.UptimeEnabled {
uptimeManager = NewUptimeManager(config.Uptime)
}
return &App{
Config: config,
WeatherCache: weatherCache,
CategoryManager: categoryManager,
UptimeManager: uptimeManager,
db: db,
}, nil
}
@@ -127,21 +233,21 @@ type UptimeManager struct {
apiKey string
}
func NewUptimeManager() *UptimeManager {
if os.Getenv("UPTIMEROBOT_API_KEY") == "" {
func NewUptimeManager(config *UptimeConfig) *UptimeManager {
if config.APIKey == "" {
log.Fatalln("UptimeRobot API Key is required!")
return nil
}
updateInterval, err := strconv.Atoi(os.Getenv("UPTIMEROBOT_UPDATE_INTERVAL"))
if err != nil || updateInterval < 1 {
updateInterval := config.UpdateInterval
if updateInterval < 1 {
updateInterval = 300
}
uptimeManager := &UptimeManager{
updateChan: make(chan struct{}),
updateInterval: updateInterval,
apiKey: os.Getenv("UPTIMEROBOT_API_KEY"),
apiKey: config.APIKey,
sites: []UptimeRobotSite{},
}
@@ -228,22 +334,27 @@ type WeatherCache struct {
tempUnits string
updateInterval int
apiKey string
lat string
lon string
lat float64
lon float64
}
func NewWeatherCache() *WeatherCache {
if os.Getenv("OPENWEATHER_API_KEY") == "" || os.Getenv("OPENWEATHER_LAT") == "" || os.Getenv("OPENWEATHER_LON") == "" {
log.Fatalln("OpenWeather API Key, and your latitude and longitude are required!")
func NewWeatherCache(config *WeatherConfig) *WeatherCache {
if config.Provider != OpenWeatherMap {
log.Fatalln("Only OpenWeatherMap is supported!")
return nil
}
updateInterval, err := strconv.Atoi(os.Getenv("OPENWEATHER_UPDATE_INTERVAL"))
if err != nil || updateInterval < 1 {
if config.OpenWeather.APIKey == "" {
log.Fatalln("An API Key required for OpenWeather!")
return nil
}
updateInterval := config.UpdateInterval
if updateInterval < 1 {
updateInterval = 15
}
units := os.Getenv("OPENWEATHER_TEMP_UNITS")
units := config.OpenWeather.Units
if units == "" {
units = "metric"
}
@@ -253,9 +364,9 @@ func NewWeatherCache() *WeatherCache {
updateChan: make(chan struct{}),
tempUnits: units,
updateInterval: updateInterval,
apiKey: os.Getenv("OPENWEATHER_API_KEY"),
lat: os.Getenv("OPENWEATHER_LAT"),
lon: os.Getenv("OPENWEATHER_LON"),
apiKey: config.OpenWeather.APIKey,
lat: config.OpenWeather.Lat,
lon: config.OpenWeather.Lon,
}
go cache.weatherWorker()
@@ -286,7 +397,7 @@ func (c *WeatherCache) weatherWorker() {
}
func (c *WeatherCache) updateWeather() {
url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%s&lon=%s&appid=%s&units=%s",
url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s&units=%s",
c.lat, c.lon, c.apiKey, c.tempUnits)
resp, err := http.Get(url)
@@ -400,72 +511,64 @@ type Link struct {
}
type CategoryManager struct {
db *sql.DB
Categories []Category
db *sql.DB
}
func NewCategoryManager(db *sql.DB) (*CategoryManager, error) {
rows, err := db.Query(`
return &CategoryManager{
db: db,
}, nil
}
func (manager *CategoryManager) GetCategories() []Category {
rows, err := manager.db.Query(`
SELECT id, name, icon
FROM categories
ORDER BY id ASC
`)
if err != nil {
return nil, err
return nil
}
defer rows.Close()
var categories []Category
for rows.Next() {
var cat Category
if err := rows.Scan(&cat.ID, &cat.Name, &cat.Icon); err != nil {
return nil, err
return nil
}
rows, err := db.Query(`
SELECT id, category_id, name, description, icon, url
FROM links
WHERE category_id = ?
ORDER BY id ASC
`, cat.ID)
if err != nil {
return nil, err
}
defer rows.Close()
var links []Link
for rows.Next() {
var link Link
if err := rows.Scan(&link.ID, &link.CategoryID, &link.Name, &link.Description,
&link.Icon, &link.URL); err != nil {
return nil, err
}
links = append(links, link)
}
cat.Links = links
categories = append(categories, cat)
}
return &CategoryManager{
db: db,
Categories: categories,
}, nil
for i, cat := range categories {
categories[i].Links = manager.GetLinks(cat.ID)
}
return categories
}
// Get Category by ID, returns nil if not found
func (manager *CategoryManager) GetCategory(id int64) *Category {
var category *Category
rows, err := manager.db.Query(`
SELECT id, name, icon
FROM categories
WHERE id = ?
`, id)
// probably potentially bad
for _, cat := range manager.Categories {
if cat.ID == id {
category = &cat
break
}
if err != nil {
return nil
}
defer rows.Close()
var cat Category
if err := rows.Scan(&cat.ID, &cat.Name, &cat.Icon); err != nil {
return nil
}
return category
return &cat
}
func (manager *CategoryManager) CreateCategory(category Category) (*Category, error) {
@@ -488,11 +591,91 @@ func (manager *CategoryManager) CreateCategory(category Category) (*Category, er
}
category.ID = categoryID
manager.Categories = append(manager.Categories, category)
return &category, nil
}
func (manager *CategoryManager) DeleteCategory(id int64) error {
rows, err := manager.db.Query(`
SELECT icon FROM categories WHERE id = ?
UNION
SELECT icon FROM links WHERE category_id = ?
`, id, id)
if err != nil {
return err
}
defer rows.Close()
var icons []string
for rows.Next() {
var icon string
if err := rows.Scan(&icon); err != nil {
return err
}
icons = append(icons, icon)
}
tx, err := manager.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("DELETE FROM categories WHERE id = ?", id)
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM links WHERE category_id = ?", id)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
for _, icon := range icons {
if icon == "" {
continue
}
if err := os.Remove(filepath.Join("public/", icon)); err != nil {
return err
}
}
return nil
}
func (manager *CategoryManager) GetLinks(categoryID int64) []Link {
rows, err := manager.db.Query(`
SELECT id, category_id, name, description, icon, url
FROM links
WHERE category_id = ?
ORDER BY id ASC
`, categoryID)
if err != nil {
return nil
}
defer rows.Close()
var links []Link
for rows.Next() {
var link Link
if err := rows.Scan(&link.ID, &link.CategoryID, &link.Name, &link.Description,
&link.Icon, &link.URL); err != nil {
return nil
}
links = append(links, link)
}
return links
}
func (manager *CategoryManager) CreateLink(db *sql.DB, link Link) (*Link, error) {
var err error
insertLinkStmt, err = db.Prepare(`
@@ -511,20 +694,6 @@ func (manager *CategoryManager) CreateLink(db *sql.DB, link Link) (*Link, error)
link.ID = linkID
var cat *Category
for i, c := range manager.Categories {
if c.ID == link.CategoryID {
cat = &manager.Categories[i]
break
}
}
if cat == nil {
return nil, fmt.Errorf("category not found")
}
cat.Links = append(cat.Links, link)
return &link, nil
}
@@ -549,17 +718,17 @@ func (manager *CategoryManager) DeleteLink(id any) error {
}
var WeatherIcons = map[string]string{
"clear-day": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M16 12.005a4 4 0 1 1-4 4a4.005 4.005 0 0 1 4-4m0-2a6 6 0 1 0 6 6a6 6 0 0 0-6-6M5.394 6.813L6.81 5.399l3.505 3.506L8.9 10.319zM2 15.005h5v2H2zm3.394 10.193L8.9 21.692l1.414 1.414l-3.505 3.506zM15 25.005h2v5h-2zm6.687-1.9l1.414-1.414l3.506 3.506l-1.414 1.414zm3.313-8.1h5v2h-5zm-3.313-6.101l3.506-3.506l1.414 1.414l-3.506 3.506zM15 2.005h2v5h-2z"/></svg>`,
"clear-night": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M13.503 5.414a15.076 15.076 0 0 0 11.593 18.194a11.1 11.1 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1 1 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.07 13.07 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3"/></svg>`,
"partly-cloudy-day": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M27 15h4v2h-4zm-4-7.413l3-3L27.415 6l-3 3zM15 1h2v4h-2zM4.586 26l3-3l1.415 1.415l-3 3zM4.585 6L6 4.587l3 3l-1.414 1.415z"/><path fill="currentColor" d="M1 15h4v2H1zm25.794 5.342a6.96 6.96 0 0 0-1.868-3.267A9 9 0 0 0 25 16a9 9 0 1 0-14.585 7.033A4.977 4.977 0 0 0 15 30h10a4.995 4.995 0 0 0 1.794-9.658M9 16a6.996 6.996 0 0 1 13.985-.297A6.9 6.9 0 0 0 20 15a7.04 7.04 0 0 0-6.794 5.342a5 5 0 0 0-1.644 1.048A6.97 6.97 0 0 1 9 16m16 12H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.156A2.995 2.995 0 0 1 25 28"/></svg>`,
"partly-cloudy-night": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M30 19a4.97 4.97 0 0 0-3.206-4.658A6.971 6.971 0 0 0 13.758 12.9a13.14 13.14 0 0 1 .131-8.52A1.015 1.015 0 0 0 12.98 3a1 1 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.04 13.04 0 0 0 10.29-5.038A4.99 4.99 0 0 0 30 19m-15.297 7.998a11.095 11.095 0 0 1-3.2-21.584a15.2 15.2 0 0 0 .844 9.367A4.988 4.988 0 0 0 15 24h7.677a11.1 11.1 0 0 1-7.556 2.998c-.138 0-.278.004-.418 0M25 22H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.157A2.995 2.995 0 0 1 25 22"/></svg>`,
"mostly-cloudy-day": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M21.743 18.692a6 6 0 0 0 1.057-1.086a5.998 5.998 0 1 0-10.733-4.445A7.56 7.56 0 0 0 6.35 18.25A5.993 5.993 0 0 0 8 30.005h11a5.985 5.985 0 0 0 2.743-11.313M18 10.005a4.004 4.004 0 0 1 4 4a3.96 3.96 0 0 1-.8 2.4a4 4 0 0 1-.94.891a7.54 7.54 0 0 0-6.134-4.24A4 4 0 0 1 18 10.006m1 18H8a3.993 3.993 0 0 1-.673-7.93l.663-.112l.146-.656a5.496 5.496 0 0 1 10.729 0l.146.656l.662.112a3.993 3.993 0 0 1-.673 7.93m7-15.001h4v2h-4zM22.95 7.64l2.828-2.827l1.415 1.414l-2.829 2.828zM17 2.005h2v4h-2zM8.808 6.227l1.414-1.414l2.829 2.828l-1.415 1.414z"/></svg>`,
"mostly-cloudy-night": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M29.844 15.035a1.52 1.52 0 0 0-1.23-.866a5.36 5.36 0 0 1-3.41-1.716a6.47 6.47 0 0 1-1.286-6.392a1.6 1.6 0 0 0-.299-1.546a1.45 1.45 0 0 0-1.36-.493l-.019.003a7.93 7.93 0 0 0-6.22 7.431A7.4 7.4 0 0 0 13.5 11a7.55 7.55 0 0 0-7.15 5.244A5.993 5.993 0 0 0 8 28h11a5.977 5.977 0 0 0 5.615-8.088a7.5 7.5 0 0 0 5.132-3.357a1.54 1.54 0 0 0 .097-1.52M19 26H8a3.993 3.993 0 0 1-.673-7.93l.663-.112l.145-.656a5.496 5.496 0 0 1 10.73 0l.145.656l.663.113A3.993 3.993 0 0 1 19 26m4.465-8.001h-.021a5.96 5.96 0 0 0-2.795-1.755a7.5 7.5 0 0 0-2.6-3.677c-.01-.101-.036-.197-.041-.3a6.08 6.08 0 0 1 3.79-6.05a8.46 8.46 0 0 0 1.94 7.596a7.4 7.4 0 0 0 3.902 2.228a5.43 5.43 0 0 1-4.175 1.958"/></svg>`,
"light-rain": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M11 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 11 30"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0A6.497 6.497 0 0 0 8.5 22h10.881l-1.276 2.553a1 1 0 0 0 1.789.894L21.618 22H23.5a6.497 6.497 0 0 0 1.3-12.864M23.5 20h-15a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20"/></svg>`,
"rain": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-2 26a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 14 30m6 0a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 20 30M8 30a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 8 30"/></svg>`,
"thunder": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M21 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 21 30M9 32a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 9 32m6.901-1.504l-1.736-.992L17.31 24h-6l4.855-8.496l1.736.992L14.756 22h6.001z"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0a6.493 6.493 0 0 0 .23 12.768l-1.324 2.649a1 1 0 1 0 1.789.894l2-4a1 1 0 0 0-.447-1.341A1 1 0 0 0 9 20.01V20h-.5a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20H23v2h.5a6.497 6.497 0 0 0 1.3-12.864"/></svg>`,
"snow": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-4 21.05L10.95 24L9.5 25.45L8.05 24L7 25.05l1.45 1.45L7 27.95L8.05 29l1.45-1.45L10.95 29L12 27.95l-1.45-1.45zm14 0L24.95 24l-1.45 1.45L22.05 24L21 25.05l1.45 1.45L21 27.95L22.05 29l1.45-1.45L24.95 29L26 27.95l-1.45-1.45zm-7 2L17.95 26l-1.45 1.45L15.05 26L14 27.05l1.45 1.45L14 29.95L15.05 31l1.45-1.45L17.95 31L19 29.95l-1.45-1.45z"/></svg>`,
"mist": `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M24.8 11.138a8.994 8.994 0 0 0-17.6 0A6.53 6.53 0 0 0 2 17.5V19a1 1 0 0 0 1 1h12a1 1 0 0 0 0-2H4v-.497a4.52 4.52 0 0 1 4.144-4.482l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.813l.815.063A4.496 4.496 0 0 1 23.5 22H7a1 1 0 0 0 0 2h16.5a6.496 6.496 0 0 0 1.3-12.862"/><rect width="18" height="2" x="2" y="26" fill="currentColor" rx="1"/></svg>`,
"clear-day": `<svg aria-label="Clear day" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M16 12.005a4 4 0 1 1-4 4a4.005 4.005 0 0 1 4-4m0-2a6 6 0 1 0 6 6a6 6 0 0 0-6-6M5.394 6.813L6.81 5.399l3.505 3.506L8.9 10.319zM2 15.005h5v2H2zm3.394 10.193L8.9 21.692l1.414 1.414l-3.505 3.506zM15 25.005h2v5h-2zm6.687-1.9l1.414-1.414l3.506 3.506l-1.414 1.414zm3.313-8.1h5v2h-5zm-3.313-6.101l3.506-3.506l1.414 1.414l-3.506 3.506zM15 2.005h2v5h-2z"/></svg>`,
"clear-night": `<svg aria-label="Clear night" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M13.503 5.414a15.076 15.076 0 0 0 11.593 18.194a11.1 11.1 0 0 1-7.975 3.39c-.138 0-.278.005-.418 0a11.094 11.094 0 0 1-3.2-21.584M14.98 3a1 1 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.07 13.07 0 0 0 10.703-5.555a1.01 1.01 0 0 0-.783-1.565A13.08 13.08 0 0 1 15.89 4.38A1.015 1.015 0 0 0 14.98 3"/></svg>`,
"partly-cloudy-day": `<svg aria-label="Partly cloudy day" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M27 15h4v2h-4zm-4-7.413l3-3L27.415 6l-3 3zM15 1h2v4h-2zM4.586 26l3-3l1.415 1.415l-3 3zM4.585 6L6 4.587l3 3l-1.414 1.415z"/><path fill="currentColor" d="M1 15h4v2H1zm25.794 5.342a6.96 6.96 0 0 0-1.868-3.267A9 9 0 0 0 25 16a9 9 0 1 0-14.585 7.033A4.977 4.977 0 0 0 15 30h10a4.995 4.995 0 0 0 1.794-9.658M9 16a6.996 6.996 0 0 1 13.985-.297A6.9 6.9 0 0 0 20 15a7.04 7.04 0 0 0-6.794 5.342a5 5 0 0 0-1.644 1.048A6.97 6.97 0 0 1 9 16m16 12H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.156A2.995 2.995 0 0 1 25 28"/></svg>`,
"partly-cloudy-night": `<svg aria-label="Partly cloudy night" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M30 19a4.97 4.97 0 0 0-3.206-4.658A6.971 6.971 0 0 0 13.758 12.9a13.14 13.14 0 0 1 .131-8.52A1.015 1.015 0 0 0 12.98 3a1 1 0 0 0-.175.016a13.096 13.096 0 0 0 1.825 25.981c.164.006.328 0 .49 0a13.04 13.04 0 0 0 10.29-5.038A4.99 4.99 0 0 0 30 19m-15.297 7.998a11.095 11.095 0 0 1-3.2-21.584a15.2 15.2 0 0 0 .844 9.367A4.988 4.988 0 0 0 15 24h7.677a11.1 11.1 0 0 1-7.556 2.998c-.138 0-.278.004-.418 0M25 22H15a2.995 2.995 0 0 1-.696-5.908l.658-.157l.099-.67a4.992 4.992 0 0 1 9.878 0l.099.67l.658.157A2.995 2.995 0 0 1 25 22"/></svg>`,
"mostly-cloudy-day": `<svg aria-label="Mostly cloudy day" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M21.743 18.692a6 6 0 0 0 1.057-1.086a5.998 5.998 0 1 0-10.733-4.445A7.56 7.56 0 0 0 6.35 18.25A5.993 5.993 0 0 0 8 30.005h11a5.985 5.985 0 0 0 2.743-11.313M18 10.005a4.004 4.004 0 0 1 4 4a3.96 3.96 0 0 1-.8 2.4a4 4 0 0 1-.94.891a7.54 7.54 0 0 0-6.134-4.24A4 4 0 0 1 18 10.006m1 18H8a3.993 3.993 0 0 1-.673-7.93l.663-.112l.146-.656a5.496 5.496 0 0 1 10.729 0l.146.656l.662.112a3.993 3.993 0 0 1-.673 7.93m7-15.001h4v2h-4zM22.95 7.64l2.828-2.827l1.415 1.414l-2.829 2.828zM17 2.005h2v4h-2zM8.808 6.227l1.414-1.414l2.829 2.828l-1.415 1.414z"/></svg>`,
"mostly-cloudy-night": `<svg aria-label="Mostly cloudy night" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M29.844 15.035a1.52 1.52 0 0 0-1.23-.866a5.36 5.36 0 0 1-3.41-1.716a6.47 6.47 0 0 1-1.286-6.392a1.6 1.6 0 0 0-.299-1.546a1.45 1.45 0 0 0-1.36-.493l-.019.003a7.93 7.93 0 0 0-6.22 7.431A7.4 7.4 0 0 0 13.5 11a7.55 7.55 0 0 0-7.15 5.244A5.993 5.993 0 0 0 8 28h11a5.977 5.977 0 0 0 5.615-8.088a7.5 7.5 0 0 0 5.132-3.357a1.54 1.54 0 0 0 .097-1.52M19 26H8a3.993 3.993 0 0 1-.673-7.93l.663-.112l.145-.656a5.496 5.496 0 0 1 10.73 0l.145.656l.663.113A3.993 3.993 0 0 1 19 26m4.465-8.001h-.021a5.96 5.96 0 0 0-2.795-1.755a7.5 7.5 0 0 0-2.6-3.677c-.01-.101-.036-.197-.041-.3a6.08 6.08 0 0 1 3.79-6.05a8.46 8.46 0 0 0 1.94 7.596a7.4 7.4 0 0 0 3.902 2.228a5.43 5.43 0 0 1-4.175 1.958"/></svg>`,
"light-rain": `<svg aria-label="Light rain" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M11 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 11 30"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0A6.497 6.497 0 0 0 8.5 22h10.881l-1.276 2.553a1 1 0 0 0 1.789.894L21.618 22H23.5a6.497 6.497 0 0 0 1.3-12.864M23.5 20h-15a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20"/></svg>`,
"rain": `<svg aria-label="Rain" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-2 26a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 14 30m6 0a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 20 30M8 30a.93.93 0 0 1-.45-.11a1 1 0 0 1-.44-1.34l2-4a1 1 0 1 1 1.78.9l-2 4A1 1 0 0 1 8 30"/></svg>`,
"thunder": `<svg aria-label="Thunder" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M21 30a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 21 30M9 32a1 1 0 0 1-.894-1.447l2-4a1 1 0 1 1 1.788.894l-2 4A1 1 0 0 1 9 32m6.901-1.504l-1.736-.992L17.31 24h-6l4.855-8.496l1.736.992L14.756 22h6.001z"/><path fill="currentColor" d="M24.8 9.136a8.994 8.994 0 0 0-17.6 0a6.493 6.493 0 0 0 .23 12.768l-1.324 2.649a1 1 0 1 0 1.789.894l2-4a1 1 0 0 0-.447-1.341A1 1 0 0 0 9 20.01V20h-.5a4.498 4.498 0 0 1-.356-8.981l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.812l.815.064A4.498 4.498 0 0 1 23.5 20H23v2h.5a6.497 6.497 0 0 0 1.3-12.864"/></svg>`,
"snow": `<svg aria-label="Snow" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M23.5 22h-15A6.5 6.5 0 0 1 7.2 9.14a9 9 0 0 1 17.6 0A6.5 6.5 0 0 1 23.5 22M16 4a7 7 0 0 0-6.94 6.14L9 11h-.86a4.5 4.5 0 0 0 .36 9h15a4.5 4.5 0 0 0 .36-9H23l-.1-.82A7 7 0 0 0 16 4m-4 21.05L10.95 24L9.5 25.45L8.05 24L7 25.05l1.45 1.45L7 27.95L8.05 29l1.45-1.45L10.95 29L12 27.95l-1.45-1.45zm14 0L24.95 24l-1.45 1.45L22.05 24L21 25.05l1.45 1.45L21 27.95L22.05 29l1.45-1.45L24.95 29L26 27.95l-1.45-1.45zm-7 2L17.95 26l-1.45 1.45L15.05 26L14 27.05l1.45 1.45L14 29.95L15.05 31l1.45-1.45L17.95 31L19 29.95l-1.45-1.45z"/></svg>`,
"mist": `<svg aria-label="Mist" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path fill="currentColor" d="M24.8 11.138a8.994 8.994 0 0 0-17.6 0A6.53 6.53 0 0 0 2 17.5V19a1 1 0 0 0 1 1h12a1 1 0 0 0 0-2H4v-.497a4.52 4.52 0 0 1 4.144-4.482l.816-.064l.099-.812a6.994 6.994 0 0 1 13.883 0l.099.813l.815.063A4.496 4.496 0 0 1 23.5 22H7a1 1 0 0 0 0 2h16.5a6.496 6.496 0 0 0 1.3-12.862"/><rect width="18" height="2" x="2" y="26" fill="currentColor" rx="1"/></svg>`,
}
func getWeatherIcon(iconId string) string {
@@ -592,9 +761,7 @@ func getWeatherIcon(iconId string) string {
}
func init() {
if err := godotenv.Load(); err != nil {
fmt.Println("No .env file found")
}
godotenv.Load()
}
func main() {
@@ -602,11 +769,29 @@ func main() {
log.Fatal(err)
}
app, err := NewApp("passport.db?cache=shared&mode=rwc&_journal_mode=WAL")
dbPath, err := filepath.Abs("passport.db")
if err != nil {
log.Fatal(err)
}
app, err := NewApp(dbPath, map[string]any{
"cache": "shared",
"mode": "rwc",
"_journal_mode": "WAL",
})
if err != nil {
log.Fatal(err)
}
defer app.Close()
go func() {
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
<-c
app.Close()
os.Exit(0)
}()
templatesDir, err := fs.Sub(embeddedAssets, "templates")
if err != nil {
log.Fatal(err)
@@ -630,7 +815,7 @@ func main() {
})
engine.AddFunc("devContent", func() string {
if os.Getenv("PASSPORT_DEV_MODE") == "true" {
if app.Config.DevMode {
return devContent
}
return ""
@@ -646,6 +831,11 @@ func main() {
router.Use(helmet.New(helmet.ConfigDefault))
// redirect /favicon.ico to /assets/favicon.ico
router.Get("/favicon.ico", func(c fiber.Ctx) error {
return c.Redirect().To("/assets/favicon.ico")
})
router.Use("/", static.New("./public", static.Config{
Browse: false,
MaxAge: 31536000,
@@ -658,11 +848,12 @@ func main() {
router.Get("/", func(c fiber.Ctx) error {
renderData := fiber.Map{
"SearchProvider": os.Getenv("PASSPORT_SEARCH_PROVIDER"),
"Categories": app.CategoryManager.Categories,
"SearchProviderURL": app.Config.SearchProvider.URL,
"SearchParam": app.Config.SearchProvider.Query,
"Categories": app.CategoryManager.GetCategories(),
}
if os.Getenv("PASSPORT_ENABLE_WEATHER") != "false" {
if app.Config.WeatherEnabled {
weather := app.WeatherCache.GetWeather()
renderData["WeatherData"] = fiber.Map{
@@ -672,7 +863,7 @@ func main() {
}
}
if os.Getenv("PASSPORT_ENABLE_UPTIME") != "false" {
if app.Config.UptimeEnabled {
renderData["UptimeData"] = app.UptimeManager.getUptime()
}
@@ -702,7 +893,8 @@ func main() {
return err
}
if loginData.Username != os.Getenv("PASSPORT_ADMIN_USERNAME") || loginData.Password != os.Getenv("PASSPORT_ADMIN_PASSWORD") {
// possible vulnerable to timing attacks
if loginData.Username != app.Config.Admin.Username || loginData.Password != app.Config.Admin.Password {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"message": "Invalid username or password"})
}
@@ -733,12 +925,13 @@ func main() {
}
return c.Render("views/admin/index", fiber.Map{
"Categories": app.CategoryManager.Categories,
"Categories": app.CategoryManager.GetCategories(),
}, "layouts/main")
})
api := router.Group("/api")
{
// all API routes require admin auth. No user needs to make api requests since the site is SSR
api.Use(func(c fiber.Ctx) error {
if c.Locals("IsAdmin") == nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"message": "Unauthorized"})
@@ -879,69 +1072,31 @@ func main() {
api.Delete("/links/:id", func(c fiber.Ctx) error {
id := c.Params("id")
app.CategoryManager.DeleteLink(id)
err = app.CategoryManager.DeleteLink(id)
if err != nil {
return err
}
return c.SendStatus(fiber.StatusOK)
})
api.Delete("/categories/:id", func(c fiber.Ctx) error {
id := c.Params("id")
rows, err := app.db.Query(`
SELECT icon FROM categories WHERE id = ?
UNION
SELECT icon FROM links WHERE category_id = ?
`, id, id)
// id = parseInt(c.Params("id"))
id, err := strconv.ParseInt(c.Params("id"), 10, 64)
if err != nil {
return err
}
defer rows.Close()
var icons []string
for rows.Next() {
var icon string
if err := rows.Scan(&icon); err != nil {
return err
}
icons = append(icons, icon)
}
tx, err := app.db.Begin()
err = app.CategoryManager.DeleteCategory(id)
if err != nil {
return err
}
defer tx.Rollback()
_, err = tx.Exec("DELETE FROM categories WHERE id = ?", id)
if err != nil {
return err
}
_, err = tx.Exec("DELETE FROM links WHERE category_id = ?", id)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
for _, icon := range icons {
if icon == "" {
continue
}
if err := os.Remove(filepath.Join("public/", icon)); err != nil {
return err
}
}
return c.SendStatus(fiber.StatusOK)
})
}
router.Listen(":3000", fiber.ListenConfig{
EnablePrefork: os.Getenv("PASSPORT_ENABLE_PREFORK") == "true",
EnablePrefork: app.Config.Prefork,
})
}

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

View File

@@ -7,7 +7,7 @@
}
:root {
--default-font-family: "Instrument Sans", ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
--default-font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
html {
@@ -25,7 +25,7 @@ h6 {
}
h1 {
font-size: clamp(48px, 10vw, 64px);
font-size: clamp(42px, 10vw, 64px);
}
h2 {

View File

@@ -1,4 +1,4 @@
<section class="flex justify-center w-full">
<section class="flex justify-center w-full transition-[filter] duration-300 ease-[cubic-bezier(0.45,_0,_0.55,_1)]">
<div class="w-full sm:w-4/5 p-2.5">
{{#each Categories}}
<div class="flex items-center">
@@ -15,7 +15,7 @@
<div class="p-2.5 grid grid-cols-[repeat(auto-fill,_minmax(min(330px,_100%),_1fr))] gap-2">
{{#each this.Links}}
<div
class="rounded-2xl bg-[#211F23] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] relative">
class="rounded-2xl bg-[#211F23] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform,translate] ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-1 relative">
<img class="object-contain mr-2 select-none rounded-md" width="64" height="64" draggable="false"
src="{{this.Icon}}" alt="{{this.Name}}" />
<div>
@@ -31,7 +31,7 @@
</svg></button>
</div>
{{/each}}
<div onclick="addLink({{this.ID}})"
<div onclick="openLinkModal({{this.ID}})"
class="rounded-2xl border border-dashed border-[#656565] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] pointer-cursor select-none cursor-pointer">
<svg class="mr-2" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24">
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"
@@ -48,14 +48,15 @@
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 5v14m-7-7h14" />
</svg>
<h2 onclick="addCategory()" class="text-[#656565] underline decoration-dashed cursor-pointer">
<h2 onclick="openCategoryModal()" class="text-[#656565] underline decoration-dashed cursor-pointer">
Add a new category
</h2>
</div>
</div>
</section>
<div id="linkModal" class="hidden absolute top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4">
<div id="linkModal"
class="flex modal-bg fixed top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4 modal">
<h3>Add A link</h3>
<form id="link-form" action="/api/links" method="post" class="flex flex-col gap-y-3 my-2">
<div>
@@ -78,7 +79,8 @@
</div>
<div>
<label for="linkIcon">Icon</label>
<input class="w-full text-white" type="file" name="icon" id="linkIcon" accept="image/*" />
<input class="w-full text-white py-2 px-4 rounded bg-[#1C1C21] border border-[#56565b]/30" type="file"
name="icon" id="linkIcon" accept="image/*" />
</div>
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Add</button>
</form>
@@ -86,10 +88,11 @@
</div>
</div>
<div id="categoryModal"
class="hidden absolute top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4">
class="flex modal-bg fixed top-0 left-0 bottom-0 right-0 bg-[#00000070] justify-center items-center">
<div class="bg-[#151316] rounded-xl overflow-hidden w-fit p-4 modal">
<h3>Create A category</h3>
<form id="category-form" action="/api/categories" method="post" class="flex flex-col gap-y-3 my-2">
<form id="category-form" action="/api/categories" method="post"
class="flex flex-col gap-y-3 my-2 [&>div]:flex [&>div]:flex-col [&>div]:gap-1">
<div>
<label for="categoryName">Name</label>
<input
@@ -98,7 +101,8 @@
</div>
<div>
<label for="linkIcon">Icon</label>
<input class="w-full text-white" type="file" name="icon" id="linkIcon" accept=".svg" />
<input class="w-full text-white py-2 px-4 rounded bg-[#1C1C21] border border-[#56565b]/30" type="file"
name="icon" id="linkIcon" accept=".svg" />
</div>
<button class="px-4 py-2 rounded-md w-full bg-[#8A42FF] text-white border-0" type="submit">Create</button>
</form>
@@ -108,19 +112,41 @@
<script>
// idfk what this variable capitalization is, it's a mess
let linkModal = document.getElementById("linkModal");
let categoryModal = document.getElementById("categoryModal");
let linkModalBg = document.getElementById("linkModal");
let linkModal = linkModalBg.querySelector("div");
let categoryModalBg = document.getElementById("categoryModal");
let categoryModal = categoryModalBg.querySelector("div");
let pageElement = document.querySelector("section");
let targetCategoryID = null;
function addCategory() {
categoryModal.classList.remove("hidden");
categoryModal.classList.add("flex");
function openCategoryModal() {
pageElement.style.filter = "blur(20px)";
categoryModalBg.classList.add("is-visible");
categoryModal.classList.add("is-visible");
}
function addLink(categoryID) {
function closeCategoryModal() {
pageElement.style.filter = "";
categoryModalBg.classList.remove("is-visible");
categoryModal.classList.remove("is-visible");
}
function openLinkModal(categoryID) {
targetCategoryID = categoryID;
linkModal.classList.remove("hidden");
linkModal.classList.add("flex");
pageElement.style.filter = "blur(20px)";
linkModalBg.classList.add("is-visible");
linkModal.classList.add("is-visible");
}
function closeLinkModal() {
pageElement.style.filter = "";
linkModalBg.classList.remove("is-visible");
linkModal.classList.remove("is-visible");
}
async function deleteLink(linkID) {
@@ -155,8 +181,7 @@
});
if (res.status === 201) {
linkModal.classList.add("hidden");
linkModal.classList.remove("flex");
closeLinkModal();
document.getElementById("link-form").reset();
location.reload();
} else {
@@ -175,8 +200,7 @@
});
if (res.status === 201) {
categoryModal.classList.add("hidden");
categoryModal.classList.remove("flex");
closeCategoryModal()
document.getElementById("category-form").reset();
location.reload();
} else {
@@ -185,19 +209,47 @@
}
});
linkModal.addEventListener("click", (event) => {
if (event.target === linkModal) {
linkModalBg.addEventListener("click", (event) => {
if (event.target === linkModalBg) {
targetCategoryID = null;
linkModal.classList.add("hidden");
linkModal.classList.remove("flex");
closeLinkModal();
}
});
categoryModal.addEventListener("click", (event) => {
if (event.target === categoryModal) {
categoryModalBg.addEventListener("click", (event) => {
if (event.target === categoryModalBg) {
targetCategoryID = null;
categoryModal.classList.add("hidden");
categoryModal.classList.remove("flex");
closeCategoryModal();
}
});
</script>
</script>
<style>
.modal-bg {
visibility: hidden;
opacity: 0;
transition: opacity 0.3s ease, visibility 0s 0.3s;
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
.modal-bg.is-visible {
visibility: visible;
opacity: 1;
transition-delay: 0s;
}
.modal {
opacity: 0;
transform: translateY(20px) scale(0.95);
transition: opacity 0.3s ease, transform 0.3s ease;
transition-timing-function: cubic-bezier(0.45, 0, 0.55, 1);
}
.modal.is-visible {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
transition-delay: 0s;
}
</style>

View File

@@ -1,7 +1,7 @@
<main class="grid grid-rows-3 grid-cols-[1fr] justify-center items-center h-screen bg-[#0E0A0E]">
<div class="flex h-full p-2.5 justify-between">
<div class="flex h-full p-2.5 justify-between">
<div>
{{#if WeatherData}}
{{#if WeatherData}}
<div class="text-[#BABABA] flex items-center">
<span class="mr-2 flex items-center">
{{{WeatherData.Icon}}}
@@ -11,10 +11,10 @@
<p>{{WeatherData.Desc}}</p>
</div>
</div>
{{/if}}
{{/if}}
</div>
<div>
{{#if UptimeData}}
{{#if UptimeData}}
<div class="text-[#BABABA] flex items-end flex-col">
{{#each UptimeData}}
<div class="flex items-center">
@@ -33,12 +33,12 @@
</div>
{{/each}}
</div>
{{/if}}
{{/if}}
</div>
</div>
<div class="row-start-2 flex flex-col items-center w-full px-6">
<div class="flex items-center pb-2.5">
<svg class="mr-3 aspect-square w-[clamp(48px,10vw,60px)]" viewBox="0 0 100 100" fill="none"
<svg class="mr-3 aspect-square w-[clamp(42px,10vw,60px)]" viewBox="0 0 100 100" fill="none"
xmlns="http://www.w3.org/2000/svg">
<rect x="12.1483" y="24.7693" width="70" height="47" rx="12" transform="rotate(14.63 12.1483 24.7693)"
fill="url(#paint0_linear_20_10)" />
@@ -60,23 +60,25 @@
</svg>
<h1>Passport</h1>
</div>
<input id="search-input"
class="w-full max-w-3xl bg-[#1C1C21] border border-[#56565b]/30 rounded-full px-3 py-1 text-white h-7 focus-visible:outline-none placeholder:italic placeholder:text-[#434343]"
placeholder="Search..." />
<form class="w-full max-w-3xl" action="{{ SearchProviderURL }}" method="GET">
<input name="{{ SearchParam }}" aria-label="Search bar"
class="w-full bg-[#1C1C21] border border-[#56565b]/30 rounded-full px-3 py-1 text-white h-7 focus-visible:outline-none placeholder:italic placeholder:text-[#434343]"
placeholder="Search..." />
</form>
</div>
</main>
<section class="flex justify-center w-full">
<div class="w-full sm:w-4/5 p-2.5">
{{#each Categories}}
<div class="flex items-center w-fit">
<img class="object-contain mr-2 select-none" width="32" height="32" draggable="false" alt="{{this.Name}}"
<img class="object-contain mr-2 select-none text-white" width="32" height="32" draggable="false" alt="{{this.Name}}"
src="{{this.Icon}}" />
<h2 class="capitalize w-fit">{{this.Name}}</h2>
</div>
<div class="p-2.5 grid grid-cols-[repeat(auto-fill,_minmax(min(330px,_100%),_1fr))] gap-2">
{{#each this.Links}}
<a href="{{this.URL}}"
class="underline-none text-unset rounded-2xl bg-[#211F23] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform] ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-1"
class="underline-none text-unset rounded-2xl bg-[#211F23] p-2.5 flex flex-row items-center shadow-md hover:shadow-xl transition-[shadow,transform,translate] ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-1"
draggable="false" target="_blank" rel="noopener noreferrer">
<img class="object-contain mr-2 select-none rounded-md" width="64" height="64" draggable="false"
src="{{this.Icon}}" alt="{{this.Name}}" />
@@ -89,15 +91,4 @@
</div>
{{/each}}
</div>
</section>
<script>
let search_input = document.getElementById("search-input");
let search_provider = "{{ SearchProvider }}";
// on enter key press
search_input.addEventListener("keyup", function (event) {
if (event.key === "Enter") {
window.location.href = search_provider.replace("%s", search_input.value);
}
});
</script>
</section>

View File

@@ -1,6 +1,6 @@
{
"name": "passport",
"version": "0.0.1",
"version": "0.2.0",
"description": "Passport is a simple, lightweight, and fast dashboard/new tab page for your browser.",
"author": "juls0730",
"license": "BSL-1.0",
@@ -11,7 +11,8 @@
},
"scripts": {
"dev": "go generate; PASSPORT_DEV_MODE=true go run main.go",
"build": "go generate && go build -tags netgo,prod -o passport"
"build": "go generate && go build -tags netgo,prod -ldflags=\"-w -s\" -o passport"
},
"pattern": "**/*.go,templates/views/**/*.hbs,styles/**/*.css,assets/**/*.{svg,png,jpg,jpeg,webp,woff2,ttf,otf,eot,ico,gif,webp}"
"pattern": "**/*.go,templates/views/**/*.hbs,styles/**/*.css,assets/**/*.{svg,png,jpg,jpeg,webp,woff2,ttf,otf,eot,ico,gif,webp}",
"shutdown_signal": "SIGINT"
}