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:
Leons Aleksandrovs
2025-07-09 23:19:31 +03:00
parent 3376043428
commit 938c9a66e5
10 changed files with 209 additions and 14 deletions

View 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) {
}

View File

@@ -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)
}

View File

@@ -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
);

View 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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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 />} />

View File

@@ -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}

View File

@@ -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"),
});