Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

import (
"context"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/dotcomnerd/seleneo/internal/repository"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"

"go.uber.org/zap"

"github.com/dotcomnerd/seleneo/internal/env"
)

type application struct {
config config
logger *zap.SugaredLogger
repo repository.Storage
}

type config struct {
addr string
env string
db dbConfig
}

type dbConfig struct {
connStr string
maxIdleTime string
maxConn int
maxIdleConns int
}

func (app *application) mount() http.Handler {
r := chi.NewRouter()

r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(middleware.Logger)
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{env.GetString("CORS_ALLOWED_ORIGIN", "http://localhost:3000")},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
// ExposedHeaders: []string{"Link"}, dont htink i need this ngl
AllowCredentials: false,
MaxAge: 300,
}))
r.Use(middleware.Timeout(30 * time.Second))

r.Route("/api", func(r chi.Router) {
r.Route("/v1", func(r chi.Router) {
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("we gon be ok, ok the hardest, istg"))
})

r.Post("/save", app.uploadImageHandler)

r.Route("/users", func(r chi.Router) {
r.Delete("/{userId}", app.deleteUserHandler)
r.Get("/{userId}", app.getUser)
})
})
})

return r
}

func (app *application) run(mux http.Handler) error {

server := &http.Server{
Addr: app.config.addr,
Handler: mux,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 15 * time.Second,
}

shutdown := make(chan error)

go func() {
quit := make(chan os.Signal, 1)

signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
s := <-quit

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

app.logger.Infow("signal got caught lacking", "signal", s.String())

shutdown <- server.Shutdown(ctx)
}()

app.logger.Infow("server is running", "addr", app.config.addr)

err := server.ListenAndServe()
if err != http.ErrServerClosed {
return err
}

if err := <-shutdown; err != nil {
return err
}

app.logger.Info("server stopped thankfully")

return nil
}
15 changes: 15 additions & 0 deletions cmd/api/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import "net/http"

func (app *application) internalServerError(w http.ResponseWriter, r *http.Request, err error) {
app.logger.Errorw("internal error", "method", r.Method, "path", r.URL.Path, "error", err.Error())

writeJsonError(w, http.StatusInternalServerError, "the server encountered a problem")
}

func (app *application) notFoundResponse(w http.ResponseWriter, r *http.Request, err error) {
app.logger.Warnf("not found error", "method", r.Method, "path", r.URL.Path, "error", err.Error())

writeJsonError(w, http.StatusNotFound, "not found")
}
35 changes: 35 additions & 0 deletions cmd/api/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main

import (
"encoding/json"
"net/http"
"github.com/go-playground/validator/v10"
)

var Validate *validator.Validate // good library for validating data

func init() {
Validate = validator.New(validator.WithRequiredStructEnabled())
}

func writeJSON(w http.ResponseWriter, status int, data any) error {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
return json.NewEncoder(w).Encode(data)
}

func writeJsonError(w http.ResponseWriter, status int, message string) error {
type json struct {
Error string `json:"error"`
}

return writeJSON(w, status, &json{Error: message})
}

func (app *application) jsonResponse(w http.ResponseWriter, status int, data any) error {
type json struct {
Data any `json:"data"`
}

return writeJSON(w, status, &json{Data: data})
}
53 changes: 53 additions & 0 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package main

import (
"fmt"

"github.com/dotcomnerd/seleneo/internal/db"
"github.com/dotcomnerd/seleneo/internal/env"
"github.com/dotcomnerd/seleneo/internal/repository"
"github.com/joho/godotenv"

"go.uber.org/zap"
)

func main() {
err := godotenv.Load()
if err != nil {
fmt.Println("Error loading .env file")
}

cfg := config{
addr: env.GetString("API_ADDR", ":8080"),
env: env.GetString("ENV", "development"), //might not be needed (find a way to switch to vault soon)
db: dbConfig{
connStr: env.GetString("DB_ADDR", "postgresql://devinechinemere:postgres123@localhost:5432/seleneo_db?sslmode=disable"),
maxIdleTime: env.GetString("DB_MAX_IDLE_TIME", "15m"),
maxConn: env.GetInt("DB_MAX_CONN", 25),
maxIdleConns: env.GetInt("DB_MAX_IDLE_CONN", 25),
},
}

logger := zap.Must(zap.NewProduction()).Sugar()
defer logger.Sync()

db, err := db.New(cfg.db.connStr, cfg.db.maxIdleTime, cfg.db.maxConn, cfg.db.maxIdleConns)
if err != nil {
logger.Fatalf("cannot connect to db: %v", err)
}

defer db.Close()
logger.Info("database connection established")

repo := repository.New(db)

app := &application{
config: cfg,
logger: logger,
repo: repo,
}

mux := app.mount()

logger.Fatal(app.run(mux))
}
88 changes: 88 additions & 0 deletions cmd/api/userImageController.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"io"
"net/http"
"strings"

"github.com/dotcomnerd/seleneo/internal/repository"
"github.com/dotcomnerd/seleneo/internal/repository/cloudflare"
)

func (app *application) uploadImageHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

maxBytes := 6 * 1024 * 1024
err := r.ParseMultipartForm(int64(maxBytes))
if err != nil {
writeJsonError(w, http.StatusBadRequest, "Unable to parse form data")
return
}

userID := r.FormValue("userId")
identifier := r.FormValue("identifier")
visibility := repository.Visibility(r.FormValue("visibility"))
if userID == "" || identifier == "" || (visibility != repository.VisibilityPrivate && visibility != repository.VisibilityPublic) {
writeJsonError(w, http.StatusBadRequest, "Missing required fields")
return
}

// this will be slow
// TODO: FIND A WAY TO MAKE IT FASTER
file, _, err := r.FormFile("file")
if err != nil {
writeJsonError(w, http.StatusBadRequest, "file not found")
return
}
defer file.Close()

fileBuffer, err := io.ReadAll(file)
if err != nil {
writeJsonError(w, http.StatusInternalServerError, "error reading file")
return
}

fileType := http.DetectContentType(fileBuffer)
fileExt := strings.Split(fileType, "/")[1]
if !isValidFileType(fileExt) {
writeJsonError(w, http.StatusBadRequest, "invalid file type. Only PNG, JPG, WEBP, and SVG allowed")
return
}

imageURL, err := cloudflare.UploadImageToCloudflare(fileBuffer, fileExt, userID)
if err != nil {
writeJsonError(w, http.StatusInternalServerError, "error uploading image to cloudflare")
return
}

msg, err := app.repo.UserImage.SaveOrUpdateUserImage(ctx, userID, imageURL, identifier, visibility)
if err != nil {
writeJsonError(w, http.StatusInternalServerError, "error saving image")
return
}

app.jsonResponse(w, http.StatusOK, map[string]interface{}{
"message": msg,
"type": "NEW_SAVE",
"visibility": visibility,
})
}

type FileType string

const (
FileTypePNG FileType = "PNG"
FileTypeJPG FileType = "JPG"
FileTypeWEBP FileType = "WEBP"
FileTypeSVG FileType = "SVG"
)

// i like my helpers and this will be used elsewhere (i hope)
func isValidFileType(fileType string) bool {
switch FileType(strings.ToUpper(fileType)) {
case FileTypePNG, FileTypeJPG, FileTypeWEBP, FileTypeSVG:
return true
default:
return false
}
}
48 changes: 48 additions & 0 deletions cmd/api/usersController.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package main

import (
"net/http"
"github.com/go-chi/chi/v5"

)

func (app *application) getUser(w http.ResponseWriter, r *http.Request) {
userId := chi.URLParam(r, "userId")

user, err := app.repo.User.GetUserById(r.Context(), userId)
if err != nil {
app.internalServerError(w, r, err)
return
}

if user == nil {
app.notFoundResponse(w, r, err)
return
}

app.jsonResponse(w, http.StatusOK, user)
}

func (app *application) deleteUserHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

userID := chi.URLParam(r, "userId")
if userID == "" {
writeJsonError(w, http.StatusBadRequest, "User ID is required")
return
}

authUserID := r.Header.Get("X-Authenticated-User-ID")
if authUserID == "" || authUserID != userID {
writeJsonError(w, http.StatusForbidden, "Unauthorized: Invalid userId")
return
}

err := app.repo.User.DeleteUser(ctx, userID)
if err != nil {
app.internalServerError(w, r, err)
return
}

app.jsonResponse(w, http.StatusOK, map[string]string{"message": "Account successfully deleted"})
}
26 changes: 26 additions & 0 deletions cmd/migrate/seed/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import (
"log"

"github.com/dotcomnerd/seleneo/internal/db"
"github.com/dotcomnerd/seleneo/internal/env"
"github.com/dotcomnerd/seleneo/internal/repository"
)

func main() {
addr := env.GetString("DB_ADDR", "postgresql://devinechinemere:postgres123@localhost:5432/seleneo_db?sslmode=disable")
conn, err := db.New(addr, "15m", 3, 3)
if err != nil {
log.Fatal(err)
}

defer conn.Close()

store := repository.New(conn)

_ = store

// TODO: implement dummy data
// db.Seed(store, conn)
}
Loading