summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-06 13:14:34 +0530
committerBobby <[email protected]>2026-03-06 13:14:34 +0530
commit6b38ef50818bd8503145ff8ef9651609e88b74f6 (patch)
treecaa2cfbb261ce17fd60f026da3706b6fd64b079c
parentb947bd40d50ddc566ee859a20304655f116c9bf8 (diff)
downloadpagoda-6b38ef50818bd8503145ff8ef9651609e88b74f6.tar.xz
pagoda-6b38ef50818bd8503145ff8ef9651609e88b74f6.zip
feat: integrate MinIO storage functionality and update user model for CDN support
-rw-r--r--garden/src/components/Layout.tsx18
-rw-r--r--garden/src/styles/layout.css23
-rw-r--r--shrine/config/config.go5
-rw-r--r--shrine/config/env.go9
-rw-r--r--shrine/go.mod23
-rw-r--r--shrine/go.sum45
-rw-r--r--shrine/models/user.go9
-rw-r--r--shrine/utils/storage/storage.go50
-rw-r--r--shrine/utils/validators/username.go2
9 files changed, 156 insertions, 28 deletions
diff --git a/garden/src/components/Layout.tsx b/garden/src/components/Layout.tsx
index 4ff2e8e..3d9fdf0 100644
--- a/garden/src/components/Layout.tsx
+++ b/garden/src/components/Layout.tsx
@@ -125,7 +125,14 @@ export default function Layout(props: LayoutProps) {
<li class="placeholder">Be the first to join!</li>
}>
<For each={stats.data()?.newest_citizens}>
- {(citizen) => <li><A href={`/u/${citizen.username}`}>{citizen.display_name}</A></li>}
+ {(citizen) => (
+ <li class="citizen-item">
+ <A href={`/u/${citizen.username}`}>
+ <img src={citizen.avatar_url} alt="" class="citizen-avatar" />
+ {citizen.display_name}
+ </A>
+ </li>
+ )}
</For>
</Show>
</ul>
@@ -136,7 +143,14 @@ export default function Layout(props: LayoutProps) {
<li class="placeholder">No one online.</li>
}>
<For each={stats.data()?.online_citizens}>
- {(citizen) => <li><A href={`/u/${citizen.username}`}>{citizen.display_name}</A></li>}
+ {(citizen) => (
+ <li class="citizen-item">
+ <A href={`/u/${citizen.username}`}>
+ <img src={citizen.avatar_url} alt="" class="citizen-avatar" />
+ {citizen.display_name}
+ </A>
+ </li>
+ )}
</For>
</Show>
</ul>
diff --git a/garden/src/styles/layout.css b/garden/src/styles/layout.css
index 4e55e9b..f4c4772 100644
--- a/garden/src/styles/layout.css
+++ b/garden/src/styles/layout.css
@@ -312,16 +312,17 @@ a:hover {
}
.nav-section-body li {
- padding: 3px 8px 3px 16px;
+ padding: 3px 8px;
font-size: 12px;
- position: relative;
+ display: flex;
+ align-items: center;
+ gap: 4px;
}
.nav-section-body li::before {
content: "\25B8";
- position: absolute;
- left: 6px;
font-size: 10px;
+ flex-shrink: 0;
color: var(--color-text-muted);
}
@@ -409,7 +410,6 @@ a:hover {
.nav-section-body li.placeholder {
color: var(--color-text-muted);
font-style: italic;
- padding: 3px 8px;
}
.nav-section-body li.placeholder::before {
@@ -525,4 +525,17 @@ a:hover {
.sidebar-logout:hover {
color: var(--color-pink);
+}
+
+.citizen-item a {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.citizen-avatar {
+ width: 20px;
+ height: 20px;
+ border-radius: 2px;
+ object-fit: cover;
} \ No newline at end of file
diff --git a/shrine/config/config.go b/shrine/config/config.go
index e07f1c6..d59f620 100644
--- a/shrine/config/config.go
+++ b/shrine/config/config.go
@@ -11,6 +11,7 @@ var (
Server server
Database database
SMTP smtp
+ Storage storage
)
func init() {
@@ -32,6 +33,10 @@ func init() {
logger.Fatalf("Config", "Failed to parse SMTP config: %v", err)
}
+ if err := env.Parse(&Storage); err != nil {
+ logger.Fatalf("Config", "Failed to parse storage config: %v", err)
+ }
+
if Server.Debug {
logger.SetDebug(true)
}
diff --git a/shrine/config/env.go b/shrine/config/env.go
index 5d803d4..e711493 100644
--- a/shrine/config/env.go
+++ b/shrine/config/env.go
@@ -24,3 +24,12 @@ type smtp struct {
From string `env:"SMTP_FROM" default:"[email protected]"`
FrontendURL string `env:"FRONTEND_URL" default:"http://localhost:5173"`
}
+
+type storage struct {
+ Endpoint string `env:"MINIO_ENDPOINT" default:"localhost:9000"`
+ AccessKey string `env:"MINIO_ACCESS_KEY" default:""`
+ SecretKey string `env:"MINIO_SECRET_KEY" default:""`
+ Bucket string `env:"MINIO_BUCKET" default:"pagoda"`
+ UseSSL bool `env:"MINIO_USE_SSL" default:"false"`
+ CDN string `env:"CDN_URL" default:""`
+}
diff --git a/shrine/go.mod b/shrine/go.mod
index f72fca3..28dcfd4 100644
--- a/shrine/go.mod
+++ b/shrine/go.mod
@@ -6,8 +6,9 @@ require (
github.com/flosch/pongo2/v6 v6.0.0
github.com/gofiber/fiber/v2 v2.52.12
github.com/joho/godotenv v1.5.1
+ github.com/minio/minio-go/v7 v7.0.98
go.uber.org/zap v1.27.1
- golang.org/x/crypto v0.31.0
+ golang.org/x/crypto v0.46.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -15,6 +16,8 @@ require (
require (
github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/go-ini/ini v1.67.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -22,19 +25,27 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
- github.com/klauspost/compress v1.17.9 // indirect
+ github.com/klauspost/compress v1.18.2 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.11 // indirect
+ github.com/klauspost/crc32 v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
+ github.com/minio/crc64nvme v1.1.1 // indirect
+ github.com/minio/md5-simd v1.1.2 // indirect
+ github.com/philhofer/fwd v1.2.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
- github.com/stretchr/testify v1.9.0 // indirect
+ github.com/rs/xid v1.6.0 // indirect
+ github.com/tinylib/msgp v1.6.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
- golang.org/x/sync v0.10.0 // indirect
- golang.org/x/sys v0.28.0 // indirect
- golang.org/x/text v0.21.0 // indirect
+ go.yaml.in/yaml/v3 v3.0.4 // indirect
+ golang.org/x/net v0.48.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/text v0.32.0 // indirect
)
diff --git a/shrine/go.sum b/shrine/go.sum
index 0ccc7fa..7da097c 100644
--- a/shrine/go.sum
+++ b/shrine/go.sum
@@ -3,8 +3,12 @@ github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer5
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
+github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
+github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -23,8 +27,13 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
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.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
+github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
+github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
+github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
+github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -38,17 +47,29 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
+github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
+github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
+github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
+github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
+github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
+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=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
+github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
+github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
@@ -61,16 +82,20 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-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/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
+golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
+golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
-golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-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/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
+golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
+golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/shrine/models/user.go b/shrine/models/user.go
index c0e1e23..95885f0 100644
--- a/shrine/models/user.go
+++ b/shrine/models/user.go
@@ -4,6 +4,7 @@ import (
"errors"
"shrine/enums"
"shrine/types"
+ "shrine/utils/storage"
"shrine/utils/validators"
"strings"
"time"
@@ -20,7 +21,7 @@ type User struct {
DisplayName string `gorm:"size:50;not null"`
Bio string `gorm:"size:500"`
Birthday *time.Time
- AvatarURL string `gorm:"size:512"`
+ AvatarURL string `gorm:"size:512;not null;default:defaults/avatar.png"`
BlinkieURL string `gorm:"size:512"`
Website string `gorm:"size:255"`
Location string `gorm:"size:100"`
@@ -111,8 +112,8 @@ func (user *User) ToResponse() types.UserResponse {
DisplayName: user.DisplayName,
Bio: user.Bio,
Birthday: user.Birthday,
- AvatarURL: user.AvatarURL,
- BlinkieURL: user.BlinkieURL,
+ AvatarURL: storage.ResolveCDN(user.AvatarURL),
+ BlinkieURL: storage.ResolveCDN(user.BlinkieURL),
Website: user.Website,
Location: user.Location,
Pronouns: user.Pronouns,
@@ -127,7 +128,7 @@ func (user *User) ToSummary() types.CitizenSummary {
ID: user.ID,
Username: user.Username,
DisplayName: user.DisplayName,
- AvatarURL: user.AvatarURL,
+ AvatarURL: storage.ResolveCDN(user.AvatarURL),
}
}
diff --git a/shrine/utils/storage/storage.go b/shrine/utils/storage/storage.go
new file mode 100644
index 0000000..32b3db5
--- /dev/null
+++ b/shrine/utils/storage/storage.go
@@ -0,0 +1,50 @@
+package storage
+
+import (
+ "context"
+ "io"
+ "shrine/config"
+ "shrine/utils/logger"
+ "strings"
+
+ "github.com/minio/minio-go/v7"
+ "github.com/minio/minio-go/v7/pkg/credentials"
+)
+
+var Client *minio.Client
+
+func init() {
+ if config.Storage.AccessKey == "" || config.Storage.SecretKey == "" {
+ logger.Infof("Storage", "MinIO credentials not configured, storage disabled")
+ return
+ }
+
+ var err error
+ Client, err = minio.New(config.Storage.Endpoint, &minio.Options{
+ Creds: credentials.NewStaticV4(config.Storage.AccessKey, config.Storage.SecretKey, ""),
+ Secure: config.Storage.UseSSL,
+ })
+ if err != nil {
+ logger.Fatalf("Storage", "Failed to initialize MinIO client: %v", err)
+ }
+
+ logger.Successf("Storage", "MinIO client initialized for %s", config.Storage.Endpoint)
+}
+
+func Upload(path string, reader io.Reader, size int64, contentType string) error {
+ _, err := Client.PutObject(context.Background(), config.Storage.Bucket, path, reader, size, minio.PutObjectOptions{
+ ContentType: contentType,
+ })
+ return err
+}
+
+func Delete(path string) error {
+ return Client.RemoveObject(context.Background(), config.Storage.Bucket, path, minio.RemoveObjectOptions{})
+}
+
+func ResolveCDN(path string) string {
+ if path == "" {
+ return ""
+ }
+ return strings.TrimRight(config.Storage.CDN, "/") + "/" + config.Storage.Bucket + "/" + path
+} \ No newline at end of file
diff --git a/shrine/utils/validators/username.go b/shrine/utils/validators/username.go
index 729548d..f77cda9 100644
--- a/shrine/utils/validators/username.go
+++ b/shrine/utils/validators/username.go
@@ -26,7 +26,7 @@ var reservedUsernames = collections.SetOf(
"members", "online", "buttons", "webring",
"guestbook", "hitcounter", "letters",
"rules", "faq", "blog", "news", "feed",
- "static", "assets", "uploads", "images", "media",
+ "static", "assets", "uploads", "images", "media", "default", "defaults",
"test", "testing", "debug", "dev", "staging",
"everyone", "all", "here", "channel",
)