generated from tsivinsky/go-template
Compare commits
2 Commits
1baa6a4806
...
d8fc6e481f
| Author | SHA1 | Date | |
|---|---|---|---|
| d8fc6e481f | |||
| 3a8bc6df2d |
45
auth/cookies.go
Normal file
45
auth/cookies.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetUserCookie(w http.ResponseWriter, token string, expiryTime time.Time) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "token",
|
||||||
|
Value: token,
|
||||||
|
Secure: true,
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
Expires: expiryTime,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveUserCookie(w http.ResponseWriter) {
|
||||||
|
http.SetCookie(w, &http.Cookie{
|
||||||
|
Name: "token",
|
||||||
|
Value: "",
|
||||||
|
Secure: true,
|
||||||
|
HttpOnly: true,
|
||||||
|
Path: "/",
|
||||||
|
Expires: time.Now().Add(-time.Hour),
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetUserIdFromRequest(r *http.Request) (int64, error) {
|
||||||
|
c, err := r.Cookie("token")
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("no token cookie: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userId, err := ValidateUserToken(c.Value)
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("invalid token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userId, nil
|
||||||
|
}
|
||||||
48
auth/jwt.go
Normal file
48
auth/jwt.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
var secretKey = os.Getenv("JWT_SECRET_KEY")
|
||||||
|
|
||||||
|
type UserClaims struct {
|
||||||
|
jwt.RegisteredClaims
|
||||||
|
UserID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateUserToken(userId int64, expiryTime time.Time) (string, error) {
|
||||||
|
now := time.Now()
|
||||||
|
claims := UserClaims{
|
||||||
|
UserID: userId,
|
||||||
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
|
ExpiresAt: jwt.NewNumericDate(expiryTime),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
|
|
||||||
|
return token.SignedString([]byte(secretKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateUserToken(token string) (int64, error) {
|
||||||
|
claims := &UserClaims{}
|
||||||
|
|
||||||
|
parsed, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (any, error) {
|
||||||
|
return []byte(secretKey), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return -1, fmt.Errorf("failed to parse token: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !parsed.Valid {
|
||||||
|
return -1, fmt.Errorf("invalid token")
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims.UserID, nil
|
||||||
|
}
|
||||||
25
db/migrations/20260316171605_init_users.sql
Normal file
25
db/migrations/20260316171605_init_users.sql
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE users (
|
||||||
|
id integer primary key not null,
|
||||||
|
email varchar not null unique,
|
||||||
|
password varchar not null,
|
||||||
|
created_at datetime default current_timestamp,
|
||||||
|
updated_at datetime default current_timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER update_users_updated_at
|
||||||
|
AFTER UPDATE ON users
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE users
|
||||||
|
SET updated_at = current_timestamp
|
||||||
|
WHERE id = OLD.id;
|
||||||
|
END;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE users;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
27
db/migrations/20260316172048_init_images.sql
Normal file
27
db/migrations/20260316172048_init_images.sql
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE images (
|
||||||
|
id varchar primary key not null,
|
||||||
|
data blob not null,
|
||||||
|
user_id integer not null,
|
||||||
|
created_at datetime default current_timestamp,
|
||||||
|
updated_at datetime default current_timestamp,
|
||||||
|
|
||||||
|
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TRIGGER update_images_updated_at
|
||||||
|
AFTER UPDATE ON images
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
UPDATE images
|
||||||
|
SET updated_at = current_timestamp
|
||||||
|
WHERE id = OLD.id;
|
||||||
|
END;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE images;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
10
db/migrations/20260316180504_images_content_type_field.sql
Normal file
10
db/migrations/20260316180504_images_content_type_field.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE images ADD COLUMN content_type varchar default '';
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
ALTER TABLE images DROP COLUMN content_type;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
9
docs/api/.gitignore
vendored
Normal file
9
docs/api/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Secrets
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
7
docs/api/auth/folder.yml
Normal file
7
docs/api/auth/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
info:
|
||||||
|
name: auth
|
||||||
|
type: folder
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
22
docs/api/auth/login user.yml
Normal file
22
docs/api/auth/login user.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
info:
|
||||||
|
name: login user
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{base_url}}/api/auth/login"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "daniil@tsivinsky.com",
|
||||||
|
"password": "Daniil2000"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
22
docs/api/auth/register user.yml
Normal file
22
docs/api/auth/register user.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
info:
|
||||||
|
name: register user
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{base_url}}/api/auth/register"
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "daniil@tsivinsky.com",
|
||||||
|
"password": "Daniil2000"
|
||||||
|
}
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
4
docs/api/environments/dev.yml
Normal file
4
docs/api/environments/dev.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
name: dev
|
||||||
|
variables:
|
||||||
|
- name: base_url
|
||||||
|
value: http://localhost:5000
|
||||||
19
docs/api/images/delete image.yml
Normal file
19
docs/api/images/delete image.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
info:
|
||||||
|
name: delete image
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: "{{base_url}}/api/images/:id"
|
||||||
|
params:
|
||||||
|
- name: id
|
||||||
|
value: 019cf7de-0844-76d0-9d84-a4bbe7afc475
|
||||||
|
type: path
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
7
docs/api/images/folder.yml
Normal file
7
docs/api/images/folder.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
info:
|
||||||
|
name: images
|
||||||
|
type: folder
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
request:
|
||||||
|
auth: inherit
|
||||||
15
docs/api/images/get user images.yml
Normal file
15
docs/api/images/get user images.yml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
info:
|
||||||
|
name: get user images
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: "{{base_url}}/api/images"
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
21
docs/api/images/upload new image.yml
Normal file
21
docs/api/images/upload new image.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
info:
|
||||||
|
name: upload new image
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: "{{base_url}}/api/images"
|
||||||
|
body:
|
||||||
|
type: file
|
||||||
|
data:
|
||||||
|
- filePath: /home/daniil/Pictures/glizzy.png
|
||||||
|
contentType: image/png
|
||||||
|
selected: true
|
||||||
|
auth: inherit
|
||||||
|
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
10
docs/api/opencollection.yml
Normal file
10
docs/api/opencollection.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
opencollection: 1.0.0
|
||||||
|
|
||||||
|
info:
|
||||||
|
name: image-storage
|
||||||
|
bundled: false
|
||||||
|
extensions:
|
||||||
|
bruno:
|
||||||
|
ignore:
|
||||||
|
- node_modules
|
||||||
|
- .git
|
||||||
7
go.mod
7
go.mod
@@ -1,15 +1,18 @@
|
|||||||
module go-template
|
module image-storage
|
||||||
|
|
||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
|
github.com/google/uuid v1.6.0
|
||||||
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/pressly/goose/v3 v3.27.0
|
github.com/pressly/goose/v3 v3.27.0
|
||||||
|
golang.org/x/crypto v0.48.0
|
||||||
modernc.org/sqlite v1.46.1
|
modernc.org/sqlite v1.46.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mfridman/interpolate v0.0.2 // indirect
|
github.com/mfridman/interpolate v0.0.2 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -1,15 +1,29 @@
|
|||||||
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
|
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
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/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
|
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
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=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
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/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
|
||||||
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
@@ -26,6 +40,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
|
||||||
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
|
|||||||
182
main.go
182
main.go
@@ -1,24 +1,196 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image-storage/auth"
|
||||||
|
"image-storage/model"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
db, err := sqlx.Connect("sqlite", "./db/sqlite.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to connect to db: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
srv := NewServer(":5000")
|
srv := NewServer(":5000")
|
||||||
|
|
||||||
srv.Handle("GET /", func(w http.ResponseWriter, r *http.Request) error {
|
srv.Handle("POST /api/auth/register", func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
var body struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
srv.Error(w, "empty body", err, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Email == "" || body.Password == "" {
|
||||||
|
srv.Error(w, "email or password missing", nil, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(body.Password), 10)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate password hash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &model.User{
|
||||||
|
Email: body.Email,
|
||||||
|
Password: string(hash),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.Create(db); err != nil {
|
||||||
|
srv.Error(w, "failed to create user", err, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.FindByID(db); err != nil {
|
||||||
|
return fmt.Errorf("failed to populate user object after creating it: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expiryTime := time.Now().Add(time.Hour * 24 * 30)
|
||||||
|
token, err := auth.GenerateUserToken(user.ID, expiryTime)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate access token: %v", err)
|
||||||
|
}
|
||||||
|
auth.SetUserCookie(w, token, expiryTime)
|
||||||
|
|
||||||
|
return srv.JSON(w, user, 201)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv.Handle("POST /api/auth/login", func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
var body struct {
|
||||||
|
Email string `json:"email"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
srv.Error(w, "empty body", err, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.Email == "" || body.Password == "" {
|
||||||
|
srv.Error(w, "email or password missing", nil, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &model.User{Email: body.Email}
|
||||||
|
if err := user.FindByEmail(db); err != nil {
|
||||||
|
srv.Error(w, "user not found", err, 404)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.Password)); err != nil {
|
||||||
|
srv.Error(w, "invalid password", nil, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
expiryTime := time.Now().Add(time.Hour * 24 * 30)
|
||||||
|
token, err := auth.GenerateUserToken(user.ID, expiryTime)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate access token: %v", err)
|
||||||
|
}
|
||||||
|
auth.SetUserCookie(w, token, expiryTime)
|
||||||
|
|
||||||
|
return srv.JSON(w, user, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv.Handle("POST /api/images", func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
userId, err := auth.GetUserIdFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
srv.Error(w, "unauthorized", err, 401)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user := &model.User{Model: model.Model{ID: userId}}
|
||||||
|
if err := user.FindByID(db); err != nil {
|
||||||
|
srv.Error(w, "user not found", nil, 401)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read request body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
img := &model.Image{
|
||||||
|
UserID: user.ID,
|
||||||
|
Data: data,
|
||||||
|
ContentType: r.Header.Get("Content-Type"),
|
||||||
|
}
|
||||||
|
if err := img.Create(db); err != nil {
|
||||||
|
srv.Error(w, "failed to save image to database: %v", err, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.JSON(w, img, 201)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv.Handle("GET /api/images", func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
userId, err := auth.GetUserIdFromRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
srv.Error(w, "unauthorized", err, 401)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Queryx("SELECT * FROM images WHERE user_id = ?", userId)
|
||||||
|
if err != nil {
|
||||||
|
srv.Error(w, "images not found", err, 400)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
images := []model.Image{}
|
||||||
|
for rows.Next() {
|
||||||
|
img := model.Image{}
|
||||||
|
if err := rows.StructScan(&img); err != nil {
|
||||||
|
log.Printf("failed to save image to struct: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
images = append(images, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
return srv.JSON(w, images, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
srv.Handle("GET /images/{id}", func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
img := &model.Image{ID: r.PathValue("id")}
|
||||||
|
if err := img.FindByID(db); err != nil {
|
||||||
|
srv.Error(w, "image not found", nil, 404)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", img.ContentType)
|
||||||
|
w.Write(img.Data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
srv.Handle("DELETE /api/images/{id}", func(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
img := &model.Image{ID: r.PathValue("id")}
|
||||||
|
if err := img.FindByID(db); err != nil {
|
||||||
|
srv.Error(w, "image not found", nil, 404)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := img.DeleteByID(db); err != nil {
|
||||||
|
srv.Error(w, "failed to delete image: %v", err, 500)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return srv.JSON(w, struct {
|
return srv.JSON(w, struct {
|
||||||
Ok bool `json:"ok"`
|
Ok bool `json:"ok"`
|
||||||
}{true}, 200)
|
}{true}, 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
srv.Handle("GET /error", func(w http.ResponseWriter, r *http.Request) error {
|
|
||||||
return fmt.Errorf("not ok")
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := srv.ListenAndServe(); err != nil {
|
if err := srv.ListenAndServe(); err != nil {
|
||||||
log.Fatalf("failed to start http server: %v", err)
|
log.Fatalf("failed to start http server: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
63
model/image.go
Normal file
63
model/image.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Image struct {
|
||||||
|
// uuid
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
Data []byte `json:"-" db:"data"`
|
||||||
|
ContentType string `json:"contentType" db:"content_type"`
|
||||||
|
UserID int64 `json:"userId" db:"user_id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (img *Image) Create(db *sqlx.DB) error {
|
||||||
|
if img.ID == "" {
|
||||||
|
uid, err := uuid.NewV7()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate uuid: %v", err)
|
||||||
|
}
|
||||||
|
img.ID = uid.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := db.NamedExec("INSERT INTO images (id, data, user_id, content_type) VALUES (:id, :data, :user_id, :content_type)", map[string]any{
|
||||||
|
"id": img.ID,
|
||||||
|
"data": img.Data,
|
||||||
|
"user_id": img.UserID,
|
||||||
|
"content_type": img.ContentType,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (img *Image) FindByID(db *sqlx.DB) error {
|
||||||
|
row := db.QueryRowx("SELECT * FROM images WHERE id = ?", img.ID)
|
||||||
|
if row.Err() != nil {
|
||||||
|
return row.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.StructScan(img); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (img *Image) DeleteByID(db *sqlx.DB) error {
|
||||||
|
_, err := db.Exec("DELETE FROM images WHERE id = ?", img.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
9
model/model.go
Normal file
9
model/model.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Model struct {
|
||||||
|
ID int64 `json:"id" db:"id"`
|
||||||
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
||||||
|
}
|
||||||
50
model/user.go
Normal file
50
model/user.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Model
|
||||||
|
|
||||||
|
Email string `json:"email" db:"email"`
|
||||||
|
Password string `json:"-" db:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) Create(db *sqlx.DB) error {
|
||||||
|
res, err := db.NamedExec("INSERT INTO users (email, password) VALUES (:email, :password)", map[string]any{
|
||||||
|
"email": u.Email,
|
||||||
|
"password": u.Password,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
u.ID, _ = res.LastInsertId()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) FindByID(db *sqlx.DB) error {
|
||||||
|
row := db.QueryRowx("SELECT * FROM users WHERE id = ?", u.ID)
|
||||||
|
if row.Err() != nil {
|
||||||
|
return row.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.StructScan(u); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *User) FindByEmail(db *sqlx.DB) error {
|
||||||
|
row := db.QueryRowx("SELECT * FROM users WHERE email = ?", u.Email)
|
||||||
|
if row.Err() != nil {
|
||||||
|
return row.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := row.StructScan(u); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user