feat: file uploads
This commit is contained in:
@ -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",
|
||||
|
19
server/internal/handlers/client.go
Normal file
19
server/internal/handlers/client.go
Normal 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)
|
||||
}
|
66
server/internal/handlers/file.go
Normal file
66
server/internal/handlers/file.go
Normal 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,
|
||||
)
|
||||
}
|
@ -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"))
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
|
Reference in New Issue
Block a user