feat(api): create template
Add template table to the database Create controller function to check if user has template, and create it in the database Made universal jwt.Claims of user data retrieval function
This commit is contained in:
70
backend/controllers/template/template.go
Normal file
70
backend/controllers/template/template.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"backend/models/template"
|
||||
"backend/utils/jwt"
|
||||
res "backend/utils/responses"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
type TemplateForm struct {
|
||||
Name string `json:"name" validate:"required,min=2,max=50"`
|
||||
Template string `json:"template" validate:"required"`
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
func Create(c *gin.Context) {
|
||||
// Receive data from frontend, check if data is okay, hash password, call model
|
||||
var data TemplateForm
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
res.Error(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate data
|
||||
if err := validate.Struct(data); err != nil {
|
||||
res.Error(c, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get user id
|
||||
user, err := jwt.GetUser(c)
|
||||
if err != nil {
|
||||
res.Error(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if template already exists
|
||||
templates, err := template.FindByName(data.Name, user.Id)
|
||||
if err != nil {
|
||||
res.Error(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if template already exists with that name
|
||||
if len(templates) > 0 {
|
||||
res.Error(c, "Template already exists", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Create in database
|
||||
if err := template.Create(data.Name, data.Template, user.Id); err != nil {
|
||||
res.Error(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res.Success(c, gin.H{"message": "Successfully created template"})
|
||||
}
|
||||
|
||||
func Get(c *gin.Context) {
|
||||
}
|
||||
|
||||
func Update(c *gin.Context) {
|
||||
}
|
||||
|
||||
func Delete(c *gin.Context) {
|
||||
}
|
||||
@@ -10,8 +10,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
JWT "github.com/golang-jwt/jwt"
|
||||
|
||||
res "backend/utils/responses"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -138,10 +136,11 @@ func Login(c *gin.Context) {
|
||||
|
||||
// Returns info from token middleware
|
||||
func TokenInfo(c *gin.Context) {
|
||||
user := c.MustGet("user").(JWT.MapClaims)
|
||||
res.Success(c, gin.H{
|
||||
"id": user["id"],
|
||||
"name": user["name"],
|
||||
"email": user["email"],
|
||||
})
|
||||
user, err := jwt.GetUser(c)
|
||||
if err != nil {
|
||||
res.Error(c, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res.Success(c, user)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "templates" (
|
||||
"id" SERIAL PRIMARY KEY,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"name" VARCHAR(50) NOT NULL UNIQUE,
|
||||
"template" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT fk_user
|
||||
FOREIGN KEY (user_id)
|
||||
REFERENCES users (id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
70
backend/models/template/template.go
Normal file
70
backend/models/template/template.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"backend/db"
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
ID int
|
||||
UserID int
|
||||
Name string
|
||||
Template string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
func Create(name string, template string, userId float64) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// build query
|
||||
query := `INSERT INTO templates (name, template, user_id) VALUES ($1, $2, $3)`
|
||||
|
||||
// execute query
|
||||
_, err := db.Pool.Exec(ctx, query, name, template, userId)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// * will follow the pointer to its value
|
||||
// If user id is 0, then we will search only by name
|
||||
func FindByName(name string, userId float64) ([]Template, error) {
|
||||
// Create timeout context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Build query and execute
|
||||
query := `SELECT * FROM templates WHERE "name" = $1`
|
||||
args := []any{name}
|
||||
|
||||
// Do we need to search by user id?
|
||||
if userId > 0 {
|
||||
query += " AND user_id = $2"
|
||||
args = append(args, userId)
|
||||
}
|
||||
|
||||
rows, err := db.Pool.Query(ctx, query, args...)
|
||||
// Query executes query instantly, and returns error instantly
|
||||
// Not like QueryRow, which executes query only on row.Scan
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// need to tell database to close the rows connection
|
||||
// and free up resources
|
||||
defer rows.Close()
|
||||
|
||||
// Prepeare results now
|
||||
var results []Template
|
||||
for rows.Next() {
|
||||
var t Template
|
||||
if err := rows.Scan(&t.ID, &t.UserID, &t.Name, &t.Template, &t.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
results = append(results, t)
|
||||
}
|
||||
|
||||
// Give pointer back
|
||||
return results, nil
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"backend/controllers/template"
|
||||
"backend/controllers/user"
|
||||
"backend/middleware"
|
||||
|
||||
@@ -18,7 +19,15 @@ func SetupRoutes() *gin.Engine {
|
||||
auth := r.Group("/")
|
||||
auth.Use(middleware.IsAuthenticated())
|
||||
|
||||
auth.GET("/info", user.TokenInfo) // Route to check if user is authenticated
|
||||
// Route to check if user is authenticated
|
||||
auth.GET("/info", user.TokenInfo)
|
||||
|
||||
// Template routes (REST FUCKING GOOOOO)
|
||||
templates := auth.Group("/templates")
|
||||
// GET (Gets all templates)
|
||||
templates.POST("", template.Create)
|
||||
// PUT (Edit)
|
||||
// DELETE (Delete)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -6,9 +6,16 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt"
|
||||
)
|
||||
|
||||
type UserClaims struct {
|
||||
Id float64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func GenerateJWT(u *user.User) (string, error) {
|
||||
// Generate JWT token
|
||||
mySigningKey := []byte(config.Env["JWT_SECRET"])
|
||||
@@ -53,3 +60,23 @@ func ParseJWT(tokenString string) (jwt.MapClaims, error) {
|
||||
// Return on invalid token
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
func GetUser(c *gin.Context) (UserClaims, error) {
|
||||
// Get user from context
|
||||
user, ok := c.Get("user")
|
||||
if !ok {
|
||||
return UserClaims{}, fmt.Errorf("no user in middleware context")
|
||||
}
|
||||
|
||||
// Get claims from user
|
||||
mapClaims, ok := user.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return UserClaims{}, fmt.Errorf("invalid token claims")
|
||||
}
|
||||
|
||||
return UserClaims{
|
||||
Id: mapClaims["id"].(float64),
|
||||
Name: mapClaims["name"].(string),
|
||||
Email: mapClaims["email"].(string),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func Success(c *gin.Context, data gin.H) {
|
||||
func Success(c *gin.Context, data any) {
|
||||
// Return success to api
|
||||
c.JSON(200, gin.H{
|
||||
"success": true,
|
||||
|
||||
@@ -11,7 +11,7 @@ const Template = withForm({
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
<form.AppField
|
||||
name="name"
|
||||
children={(f) => <f.TextField label="Name" placeholder="Template name" />}
|
||||
children={(f) => <f.TextField maxLength={50} label="Name" placeholder="Template name" />}
|
||||
/>
|
||||
|
||||
<form.AppField name="template" children={(f) => <f.RichTextEdit />} />
|
||||
|
||||
@@ -7,6 +7,7 @@ interface TextFieldProps {
|
||||
placeholder?: string;
|
||||
label?: string;
|
||||
type?: React.ComponentProps<"input">["type"];
|
||||
maxLength?: React.ComponentProps<"input">["maxLength"];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ export default function TextField({
|
||||
type = "text",
|
||||
className = "",
|
||||
label = "",
|
||||
maxLength = 255,
|
||||
}: TextFieldProps) {
|
||||
// Get field with predefined text type
|
||||
const field = useFieldContext<string>();
|
||||
@@ -30,6 +32,7 @@ export default function TextField({
|
||||
|
||||
<Input
|
||||
id={field.name}
|
||||
maxLength={maxLength}
|
||||
name={field.name}
|
||||
value={field.state.value}
|
||||
onBlur={field.handleBlur}
|
||||
|
||||
@@ -11,7 +11,11 @@ export const Route = createFileRoute("/templates/create")({
|
||||
});
|
||||
|
||||
const TemplateSchema = z.object({
|
||||
name: z.string().nonempty("Name is required"),
|
||||
name: z
|
||||
.string()
|
||||
.nonempty("Name is required")
|
||||
.min(2, "Name is too short")
|
||||
.max(50, "Name is too long (max 50)"),
|
||||
template: z.string().nonempty("Template is required"),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user