feat: file uploads

This commit is contained in:
2025-03-16 06:50:11 -04:00
parent 50c8d18df9
commit f6d75964c1
25 changed files with 1393 additions and 320 deletions

View File

@ -22,10 +22,10 @@ type AuthHandler struct {
key []byte
}
func (s *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) {
func (h *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.LoginRequest]) (*connect.Response[userv1.LoginResponse], error) {
// Validate
user := models.User{}
if err := s.db.First(&user, "username = ?", req.Msg.Username).Error; err != nil {
if err := h.db.First(&user, "username = ?", req.Msg.Username).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("invalid username or password"))
}
@ -46,7 +46,7 @@ func (s *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.Log
Time: time.Now().Add(time.Hour * 24),
},
})
ss, err := t.SignedString(s.key)
ss, err := t.SignedString(h.key)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
@ -69,9 +69,9 @@ func (s *AuthHandler) Login(ctx context.Context, req *connect.Request[userv1.Log
return res, nil
}
func (s *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.SignUpRequest]) (*connect.Response[userv1.SignUpResponse], error) {
func (h *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.SignUpRequest]) (*connect.Response[userv1.SignUpResponse], error) {
// Validate
if err := s.db.First(&models.User{}, "username = ?", req.Msg.Username).Error; err != nil {
if err := h.db.First(&models.User{}, "username = ?", req.Msg.Username).Error; err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, connect.NewError(connect.CodeInternal, err)
}
@ -93,7 +93,7 @@ func (s *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.Si
Username: req.Msg.Username,
Password: string(hash),
}
if err := s.db.Create(&user).Error; err != nil {
if err := h.db.Create(&user).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
@ -101,7 +101,7 @@ func (s *AuthHandler) SignUp(ctx context.Context, req *connect.Request[userv1.Si
return res, nil
}
func (s *AuthHandler) Logout(ctx context.Context, req *connect.Request[userv1.LogoutRequest]) (*connect.Response[userv1.LogoutResponse], error) {
func (h *AuthHandler) Logout(ctx context.Context, req *connect.Request[userv1.LogoutRequest]) (*connect.Response[userv1.LogoutResponse], error) {
// Clear cookie
cookie := http.Cookie{
Name: "token",

View File

@ -0,0 +1,19 @@
package handlers
import (
"embed"
"io/fs"
"log"
"net/http"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
)
func NewClientHandler(client embed.FS, key string) http.Handler {
clientFs, err := fs.Sub(client, "client")
if err != nil {
log.Fatalf("failed to get sub filesystem: %v", err)
}
return interceptors.WithAuthRedirect(http.FileServer(http.FS(clientFs)), key)
}

View File

@ -0,0 +1,66 @@
package handlers
import (
"errors"
"log"
"net/http"
"strings"
"github.com/spotdemo4/trevstack/server/internal/interceptors"
"github.com/spotdemo4/trevstack/server/internal/models"
"gorm.io/gorm"
)
type FileHandler struct {
db *gorm.DB
key []byte
}
func (h *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
userid, ok := interceptors.GetUserContext(r.Context())
if !ok {
http.Redirect(w, r, "/auth", http.StatusFound)
return
}
// Get the file id from the path
pathItems := strings.Split(r.URL.Path, "/")
if len(pathItems) < 3 {
http.Redirect(w, r, "/auth", http.StatusFound)
return
}
id := pathItems[2]
// Get the file from the database
file := models.File{}
if err := h.db.First(&file, "id = ? AND user_id = ?", id, userid).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
http.Error(w, "File not found", http.StatusNotFound)
return
}
log.Println(err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Serve the file
if r.Method == http.MethodGet {
w.Header().Set("Content-Type", http.DetectContentType(file.Data))
w.Write(file.Data)
return
} else {
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
return
}
}
func NewFileHandler(db *gorm.DB, key string) http.Handler {
return interceptors.WithAuthRedirect(
&FileHandler{
db: db,
key: []byte(key),
},
key,
)
}

View File

@ -21,7 +21,7 @@ type ItemHandler struct {
}
func (h *ItemHandler) GetItem(ctx context.Context, req *connect.Request[itemv1.GetItemRequest]) (*connect.Response[itemv1.GetItemResponse], error) {
userid, ok := interceptors.UserFromContext(ctx)
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
@ -39,7 +39,7 @@ func (h *ItemHandler) GetItem(ctx context.Context, req *connect.Request[itemv1.G
}
func (h *ItemHandler) GetItems(ctx context.Context, req *connect.Request[itemv1.GetItemsRequest]) (*connect.Response[itemv1.GetItemsResponse], error) {
userid, ok := interceptors.UserFromContext(ctx)
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
@ -89,7 +89,7 @@ func (h *ItemHandler) GetItems(ctx context.Context, req *connect.Request[itemv1.
}
func (h *ItemHandler) CreateItem(ctx context.Context, req *connect.Request[itemv1.CreateItemRequest]) (*connect.Response[itemv1.CreateItemResponse], error) {
userid, ok := interceptors.UserFromContext(ctx)
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
@ -114,7 +114,7 @@ func (h *ItemHandler) CreateItem(ctx context.Context, req *connect.Request[itemv
}
func (h *ItemHandler) UpdateItem(ctx context.Context, req *connect.Request[itemv1.UpdateItemRequest]) (*connect.Response[itemv1.UpdateItemResponse], error) {
userid, ok := interceptors.UserFromContext(ctx)
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
@ -144,7 +144,7 @@ func (h *ItemHandler) UpdateItem(ctx context.Context, req *connect.Request[itemv
}
func (h *ItemHandler) DeleteItem(ctx context.Context, req *connect.Request[itemv1.DeleteItemRequest]) (*connect.Response[itemv1.DeleteItemResponse], error) {
userid, ok := interceptors.UserFromContext(ctx)
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}

View File

@ -22,15 +22,33 @@ type UserHandler struct {
key []byte
}
func (s *UserHandler) ChangePassword(ctx context.Context, req *connect.Request[userv1.ChangePasswordRequest]) (*connect.Response[userv1.ChangePasswordResponse], error) {
userid, ok := interceptors.UserFromContext(ctx)
func (h *UserHandler) GetUser(ctx context.Context, req *connect.Request[userv1.GetUserRequest]) (*connect.Response[userv1.GetUserResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user := models.User{}
if err := s.db.First(&user, "id = ?", userid).Error; err != nil {
if err := h.db.Preload("ProfilePicture").First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.GetUserResponse{
User: user.ToConnectV1(),
})
return res, nil
}
func (h *UserHandler) UpdatePassword(ctx context.Context, req *connect.Request[userv1.UpdatePasswordRequest]) (*connect.Response[userv1.UpdatePasswordResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user := models.User{}
if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
@ -49,23 +67,23 @@ func (s *UserHandler) ChangePassword(ctx context.Context, req *connect.Request[u
}
// Update password
if err := s.db.Model(&user).Update("password", string(hash)).Error; err != nil {
if err := h.db.Model(&user).Update("password", string(hash)).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.ChangePasswordResponse{})
res := connect.NewResponse(&userv1.UpdatePasswordResponse{})
return res, nil
}
func (s *UserHandler) APIKey(ctx context.Context, req *connect.Request[userv1.APIKeyRequest]) (*connect.Response[userv1.APIKeyResponse], error) {
userid, ok := interceptors.UserFromContext(ctx)
func (h *UserHandler) GetAPIKey(ctx context.Context, req *connect.Request[userv1.GetAPIKeyRequest]) (*connect.Response[userv1.GetAPIKeyResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Get user
user := models.User{}
if err := s.db.First(&user, "id = ?", userid).Error; err != nil {
if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
@ -85,17 +103,71 @@ func (s *UserHandler) APIKey(ctx context.Context, req *connect.Request[userv1.AP
Time: time.Now(),
},
})
ss, err := t.SignedString(s.key)
ss, err := t.SignedString(h.key)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
res := connect.NewResponse(&userv1.APIKeyResponse{
res := connect.NewResponse(&userv1.GetAPIKeyResponse{
Key: ss,
})
return res, nil
}
func (h *UserHandler) UpdateProfilePicture(ctx context.Context, req *connect.Request[userv1.UpdateProfilePictureRequest]) (*connect.Response[userv1.UpdateProfilePictureResponse], error) {
userid, ok := interceptors.GetUserContext(ctx)
if !ok {
return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unauthenticated"))
}
// Validate file
fileType := http.DetectContentType(req.Msg.Data)
if fileType != "image/jpeg" && fileType != "image/png" {
return nil, connect.NewError(connect.CodeInvalidArgument, errors.New("invalid file type"))
}
// Save bytes into file
file := models.File{
Name: req.Msg.FileName,
Data: req.Msg.Data,
UserID: uint(userid),
}
if err := h.db.Create(&file).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Get user info
user := models.User{}
if err := h.db.First(&user, "id = ?", userid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Get old profile picture ID
var ppid *uint32
if user.ProfilePicture != nil {
ppid = &user.ProfilePicture.ID
}
// Update user profile picture
fid := uint(file.ID)
user.ProfilePictureID = &fid
if err := h.db.Save(&user).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
// Delete old profile picture if exists
if ppid != nil {
if err := h.db.Delete(models.File{}, "id = ?", *ppid).Error; err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
}
res := connect.NewResponse(&userv1.UpdateProfilePictureResponse{
User: user.ToConnectV1(),
})
return res, nil
}
func NewUserHandler(db *gorm.DB, key string) (string, http.Handler) {
interceptors := connect.WithInterceptors(interceptors.NewAuthInterceptor(key))