build multi tenant

This commit is contained in:
2025-10-12 15:47:00 +07:00
parent e466e2f801
commit 1fbb202002
24 changed files with 1947 additions and 29 deletions

View File

@@ -0,0 +1,105 @@
package postgres
import (
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/ajaxray/geek-life/model"
)
// ProjectRepository implements the project repository interface for PostgreSQL
type ProjectRepository struct {
db *sqlx.DB
}
// NewProjectRepository creates a new project repository
func NewProjectRepository(db *sqlx.DB) *ProjectRepository {
return &ProjectRepository{db: db}
}
// GetAll retrieves all projects for a user
func (r *ProjectRepository) GetAll(ctx *model.UserContext) ([]model.Project, error) {
var projects []model.Project
query := `SELECT id, tenant_id, user_id, title, uuid, created_at, updated_at
FROM projects WHERE tenant_id = $1 AND user_id = $2 ORDER BY created_at DESC`
err := r.db.Select(&projects, query, ctx.Tenant.ID, ctx.User.ID)
return projects, err
}
// GetByID retrieves a project by ID
func (r *ProjectRepository) GetByID(ctx *model.UserContext, id int64) (model.Project, error) {
var project model.Project
query := `SELECT id, tenant_id, user_id, title, uuid, created_at, updated_at
FROM projects WHERE id = $1 AND tenant_id = $2 AND user_id = $3`
err := r.db.Get(&project, query, id, ctx.Tenant.ID, ctx.User.ID)
return project, err
}
// GetByTitle retrieves a project by title
func (r *ProjectRepository) GetByTitle(ctx *model.UserContext, title string) (model.Project, error) {
var project model.Project
query := `SELECT id, tenant_id, user_id, title, uuid, created_at, updated_at
FROM projects WHERE title = $1 AND tenant_id = $2 AND user_id = $3`
err := r.db.Get(&project, query, title, ctx.Tenant.ID, ctx.User.ID)
return project, err
}
// GetByUUID retrieves a project by UUID
func (r *ProjectRepository) GetByUUID(ctx *model.UserContext, UUID string) (model.Project, error) {
var project model.Project
query := `SELECT id, tenant_id, user_id, title, uuid, created_at, updated_at
FROM projects WHERE uuid = $1 AND tenant_id = $2 AND user_id = $3`
err := r.db.Get(&project, query, UUID, ctx.Tenant.ID, ctx.User.ID)
return project, err
}
// Create creates a new project
func (r *ProjectRepository) Create(ctx *model.UserContext, title, UUID string) (model.Project, error) {
if UUID == "" {
UUID = uuid.New().String()
}
project := model.Project{
TenantID: ctx.Tenant.ID,
UserID: ctx.User.ID,
Title: title,
UUID: UUID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
query := `INSERT INTO projects (tenant_id, user_id, title, uuid, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`
err := r.db.QueryRow(query, project.TenantID, project.UserID, project.Title, project.UUID, project.CreatedAt, project.UpdatedAt).Scan(&project.ID)
if err != nil {
return model.Project{}, err
}
return project, nil
}
// Update updates an existing project
func (r *ProjectRepository) Update(ctx *model.UserContext, p *model.Project) error {
p.UpdatedAt = time.Now()
query := `UPDATE projects SET title = $1, updated_at = $2
WHERE id = $3 AND tenant_id = $4 AND user_id = $5`
_, err := r.db.Exec(query, p.Title, p.UpdatedAt, p.ID, ctx.Tenant.ID, ctx.User.ID)
return err
}
// UpdateField updates a specific field of a project
func (r *ProjectRepository) UpdateField(ctx *model.UserContext, p *model.Project, field string, value interface{}) error {
p.UpdatedAt = time.Now()
query := `UPDATE projects SET ` + field + ` = $1, updated_at = $2
WHERE id = $3 AND tenant_id = $4 AND user_id = $5`
_, err := r.db.Exec(query, value, p.UpdatedAt, p.ID, ctx.Tenant.ID, ctx.User.ID)
return err
}
// Delete deletes a project
func (r *ProjectRepository) Delete(ctx *model.UserContext, p *model.Project) error {
query := `DELETE FROM projects WHERE id = $1 AND tenant_id = $2 AND user_id = $3`
_, err := r.db.Exec(query, p.ID, ctx.Tenant.ID, ctx.User.ID)
return err
}

View File

@@ -0,0 +1,63 @@
package postgres
import (
"time"
"github.com/jmoiron/sqlx"
"github.com/ajaxray/geek-life/model"
)
// SessionRepository implements the session repository interface for PostgreSQL
type SessionRepository struct {
db *sqlx.DB
}
// NewSessionRepository creates a new session repository
func NewSessionRepository(db *sqlx.DB) *SessionRepository {
return &SessionRepository{db: db}
}
// GetByToken retrieves a session by token
func (r *SessionRepository) GetByToken(token string) (*model.UserSession, error) {
var session model.UserSession
query := `SELECT id, user_id, token, expires_at, created_at
FROM user_sessions WHERE token = $1 AND expires_at > NOW()`
err := r.db.Get(&session, query, token)
if err != nil {
return nil, err
}
return &session, nil
}
// Create creates a new session
func (r *SessionRepository) Create(userID int64, token string, expiresAt int64) (*model.UserSession, error) {
session := &model.UserSession{
UserID: userID,
Token: token,
ExpiresAt: time.Unix(expiresAt, 0),
CreatedAt: time.Now(),
}
query := `INSERT INTO user_sessions (user_id, token, expires_at, created_at)
VALUES ($1, $2, $3, $4) RETURNING id`
err := r.db.QueryRow(query, session.UserID, session.Token, session.ExpiresAt, session.CreatedAt).Scan(&session.ID)
if err != nil {
return nil, err
}
return session, nil
}
// Delete deletes a session
func (r *SessionRepository) Delete(session *model.UserSession) error {
query := `DELETE FROM user_sessions WHERE id = $1`
_, err := r.db.Exec(query, session.ID)
return err
}
// DeleteExpired deletes all expired sessions
func (r *SessionRepository) DeleteExpired() error {
query := `DELETE FROM user_sessions WHERE expires_at <= NOW()`
_, err := r.db.Exec(query)
return err
}

148
repository/postgres/task.go Normal file
View File

@@ -0,0 +1,148 @@
package postgres
import (
"strconv"
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/ajaxray/geek-life/model"
)
// TaskRepository implements the task repository interface for PostgreSQL
type TaskRepository struct {
db *sqlx.DB
}
// NewTaskRepository creates a new task repository
func NewTaskRepository(db *sqlx.DB) *TaskRepository {
return &TaskRepository{db: db}
}
// GetAll retrieves all tasks for a user
func (r *TaskRepository) GetAll(ctx *model.UserContext) ([]model.Task, error) {
var tasks []model.Task
query := `SELECT id, tenant_id, user_id, project_id, uuid, title, details, completed, due_date, created_at, updated_at
FROM tasks WHERE tenant_id = $1 AND user_id = $2 ORDER BY created_at DESC`
err := r.db.Select(&tasks, query, ctx.Tenant.ID, ctx.User.ID)
return tasks, err
}
// GetAllByProject retrieves all tasks for a specific project
func (r *TaskRepository) GetAllByProject(ctx *model.UserContext, project model.Project) ([]model.Task, error) {
var tasks []model.Task
query := `SELECT id, tenant_id, user_id, project_id, uuid, title, details, completed, due_date, created_at, updated_at
FROM tasks WHERE tenant_id = $1 AND user_id = $2 AND project_id = $3 ORDER BY created_at DESC`
err := r.db.Select(&tasks, query, ctx.Tenant.ID, ctx.User.ID, project.ID)
return tasks, err
}
// GetAllByDate retrieves all tasks for a specific date
func (r *TaskRepository) GetAllByDate(ctx *model.UserContext, date time.Time) ([]model.Task, error) {
var tasks []model.Task
startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
endOfDay := startOfDay.Add(24 * time.Hour)
query := `SELECT id, tenant_id, user_id, project_id, uuid, title, details, completed, due_date, created_at, updated_at
FROM tasks WHERE tenant_id = $1 AND user_id = $2 AND due_date >= $3 AND due_date < $4 ORDER BY due_date ASC`
err := r.db.Select(&tasks, query, ctx.Tenant.ID, ctx.User.ID, startOfDay, endOfDay)
return tasks, err
}
// GetAllByDateRange retrieves all tasks within a date range
func (r *TaskRepository) GetAllByDateRange(ctx *model.UserContext, from, to time.Time) ([]model.Task, error) {
var tasks []model.Task
query := `SELECT id, tenant_id, user_id, project_id, uuid, title, details, completed, due_date, created_at, updated_at
FROM tasks WHERE tenant_id = $1 AND user_id = $2 AND due_date >= $3 AND due_date <= $4 ORDER BY due_date ASC`
err := r.db.Select(&tasks, query, ctx.Tenant.ID, ctx.User.ID, from, to)
return tasks, err
}
// GetByID retrieves a task by ID (string format for compatibility)
func (r *TaskRepository) GetByID(ctx *model.UserContext, ID string) (model.Task, error) {
var task model.Task
id, err := strconv.ParseInt(ID, 10, 64)
if err != nil {
return task, err
}
query := `SELECT id, tenant_id, user_id, project_id, uuid, title, details, completed, due_date, created_at, updated_at
FROM tasks WHERE id = $1 AND tenant_id = $2 AND user_id = $3`
err = r.db.Get(&task, query, id, ctx.Tenant.ID, ctx.User.ID)
return task, err
}
// GetByUUID retrieves a task by UUID
func (r *TaskRepository) GetByUUID(ctx *model.UserContext, UUID string) (model.Task, error) {
var task model.Task
query := `SELECT id, tenant_id, user_id, project_id, uuid, title, details, completed, due_date, created_at, updated_at
FROM tasks WHERE uuid = $1 AND tenant_id = $2 AND user_id = $3`
err := r.db.Get(&task, query, UUID, ctx.Tenant.ID, ctx.User.ID)
return task, err
}
// Create creates a new task
func (r *TaskRepository) Create(ctx *model.UserContext, project model.Project, title, details, UUID string, dueDate *int64) (model.Task, error) {
if UUID == "" {
UUID = uuid.New().String()
}
task := model.Task{
TenantID: ctx.Tenant.ID,
UserID: ctx.User.ID,
ProjectID: project.ID,
UUID: UUID,
Title: title,
Details: details,
Completed: false,
DueDate: dueDate,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
query := `INSERT INTO tasks (tenant_id, user_id, project_id, uuid, title, details, completed, due_date, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`
var dueDateValue interface{}
if dueDate != nil {
dueDateValue = time.Unix(*dueDate, 0)
}
err := r.db.QueryRow(query, task.TenantID, task.UserID, task.ProjectID, task.UUID, task.Title, task.Details, task.Completed, dueDateValue, task.CreatedAt, task.UpdatedAt).Scan(&task.ID)
if err != nil {
return model.Task{}, err
}
return task, nil
}
// Update updates an existing task
func (r *TaskRepository) Update(ctx *model.UserContext, t *model.Task) error {
t.UpdatedAt = time.Now()
var dueDateValue interface{}
if t.DueDate != nil {
dueDateValue = time.Unix(*t.DueDate, 0)
}
query := `UPDATE tasks SET title = $1, details = $2, completed = $3, due_date = $4, updated_at = $5
WHERE id = $6 AND tenant_id = $7 AND user_id = $8`
_, err := r.db.Exec(query, t.Title, t.Details, t.Completed, dueDateValue, t.UpdatedAt, t.ID, ctx.Tenant.ID, ctx.User.ID)
return err
}
// UpdateField updates a specific field of a task
func (r *TaskRepository) UpdateField(ctx *model.UserContext, t *model.Task, field string, value interface{}) error {
t.UpdatedAt = time.Now()
query := `UPDATE tasks SET ` + field + ` = $1, updated_at = $2
WHERE id = $3 AND tenant_id = $4 AND user_id = $5`
_, err := r.db.Exec(query, value, t.UpdatedAt, t.ID, ctx.Tenant.ID, ctx.User.ID)
return err
}
// Delete deletes a task
func (r *TaskRepository) Delete(ctx *model.UserContext, t *model.Task) error {
query := `DELETE FROM tasks WHERE id = $1 AND tenant_id = $2 AND user_id = $3`
_, err := r.db.Exec(query, t.ID, ctx.Tenant.ID, ctx.User.ID)
return err
}

View File

@@ -0,0 +1,72 @@
package postgres
import (
"time"
"github.com/jmoiron/sqlx"
"github.com/ajaxray/geek-life/model"
)
// TenantRepository implements the tenant repository interface for PostgreSQL
type TenantRepository struct {
db *sqlx.DB
}
// NewTenantRepository creates a new tenant repository
func NewTenantRepository(db *sqlx.DB) *TenantRepository {
return &TenantRepository{db: db}
}
// GetByID retrieves a tenant by ID
func (r *TenantRepository) GetByID(id int64) (*model.Tenant, error) {
var tenant model.Tenant
query := `SELECT id, name, created_at, updated_at FROM tenants WHERE id = $1`
err := r.db.Get(&tenant, query, id)
if err != nil {
return nil, err
}
return &tenant, nil
}
// GetByName retrieves a tenant by name
func (r *TenantRepository) GetByName(name string) (*model.Tenant, error) {
var tenant model.Tenant
query := `SELECT id, name, created_at, updated_at FROM tenants WHERE name = $1`
err := r.db.Get(&tenant, query, name)
if err != nil {
return nil, err
}
return &tenant, nil
}
// Create creates a new tenant
func (r *TenantRepository) Create(name string) (*model.Tenant, error) {
tenant := &model.Tenant{
Name: name,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
query := `INSERT INTO tenants (name, created_at, updated_at) VALUES ($1, $2, $3) RETURNING id`
err := r.db.QueryRow(query, tenant.Name, tenant.CreatedAt, tenant.UpdatedAt).Scan(&tenant.ID)
if err != nil {
return nil, err
}
return tenant, nil
}
// Update updates an existing tenant
func (r *TenantRepository) Update(tenant *model.Tenant) error {
tenant.UpdatedAt = time.Now()
query := `UPDATE tenants SET name = $1, updated_at = $2 WHERE id = $3`
_, err := r.db.Exec(query, tenant.Name, tenant.UpdatedAt, tenant.ID)
return err
}
// Delete deletes a tenant
func (r *TenantRepository) Delete(tenant *model.Tenant) error {
query := `DELETE FROM tenants WHERE id = $1`
_, err := r.db.Exec(query, tenant.ID)
return err
}

View File

@@ -0,0 +1,91 @@
package postgres
import (
"time"
"github.com/jmoiron/sqlx"
"github.com/ajaxray/geek-life/model"
)
// UserRepository implements the user repository interface for PostgreSQL
type UserRepository struct {
db *sqlx.DB
}
// NewUserRepository creates a new user repository
func NewUserRepository(db *sqlx.DB) *UserRepository {
return &UserRepository{db: db}
}
// GetByID retrieves a user by ID
func (r *UserRepository) GetByID(id int64) (*model.User, error) {
var user model.User
query := `SELECT id, tenant_id, username, email, password_hash, created_at, updated_at
FROM users WHERE id = $1`
err := r.db.Get(&user, query, id)
if err != nil {
return nil, err
}
return &user, nil
}
// GetByUsername retrieves a user by username within a tenant
func (r *UserRepository) GetByUsername(tenantID int64, username string) (*model.User, error) {
var user model.User
query := `SELECT id, tenant_id, username, email, password_hash, created_at, updated_at
FROM users WHERE tenant_id = $1 AND username = $2`
err := r.db.Get(&user, query, tenantID, username)
if err != nil {
return nil, err
}
return &user, nil
}
// GetByEmail retrieves a user by email within a tenant
func (r *UserRepository) GetByEmail(tenantID int64, email string) (*model.User, error) {
var user model.User
query := `SELECT id, tenant_id, username, email, password_hash, created_at, updated_at
FROM users WHERE tenant_id = $1 AND email = $2`
err := r.db.Get(&user, query, tenantID, email)
if err != nil {
return nil, err
}
return &user, nil
}
// Create creates a new user
func (r *UserRepository) Create(tenantID int64, username, email, passwordHash string) (*model.User, error) {
user := &model.User{
TenantID: tenantID,
Username: username,
Email: email,
PasswordHash: passwordHash,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
query := `INSERT INTO users (tenant_id, username, email, password_hash, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`
err := r.db.QueryRow(query, user.TenantID, user.Username, user.Email, user.PasswordHash, user.CreatedAt, user.UpdatedAt).Scan(&user.ID)
if err != nil {
return nil, err
}
return user, nil
}
// Update updates an existing user
func (r *UserRepository) Update(user *model.User) error {
user.UpdatedAt = time.Now()
query := `UPDATE users SET username = $1, email = $2, password_hash = $3, updated_at = $4
WHERE id = $5`
_, err := r.db.Exec(query, user.Username, user.Email, user.PasswordHash, user.UpdatedAt, user.ID)
return err
}
// Delete deletes a user
func (r *UserRepository) Delete(user *model.User) error {
query := `DELETE FROM users WHERE id = $1`
_, err := r.db.Exec(query, user.ID)
return err
}

View File

@@ -4,12 +4,12 @@ import "github.com/ajaxray/geek-life/model"
// ProjectRepository interface defines methods of project data accessor
type ProjectRepository interface {
GetAll() ([]model.Project, error)
GetByID(id int64) (model.Project, error)
GetByTitle(title string) (model.Project, error)
GetByUUID(UUID string) (model.Project, error)
Create(title, UUID string) (model.Project, error)
Update(p *model.Project) error
UpdateField(p *model.Project, field string, value interface{}) error
Delete(p *model.Project) error
GetAll(ctx *model.UserContext) ([]model.Project, error)
GetByID(ctx *model.UserContext, id int64) (model.Project, error)
GetByTitle(ctx *model.UserContext, title string) (model.Project, error)
GetByUUID(ctx *model.UserContext, UUID string) (model.Project, error)
Create(ctx *model.UserContext, title, UUID string) (model.Project, error)
Update(ctx *model.UserContext, p *model.Project) error
UpdateField(ctx *model.UserContext, p *model.Project, field string, value interface{}) error
Delete(ctx *model.UserContext, p *model.Project) error
}

View File

@@ -8,14 +8,14 @@ import (
// TaskRepository interface defines methods of task data accessor
type TaskRepository interface {
GetAll() ([]model.Task, error)
GetAllByProject(project model.Project) ([]model.Task, error)
GetAllByDate(date time.Time) ([]model.Task, error)
GetAllByDateRange(from, to time.Time) ([]model.Task, error)
GetByID(ID string) (model.Task, error)
GetByUUID(UUID string) (model.Task, error)
Create(project model.Project, title, details, UUID string, dueDate int64) (model.Task, error)
Update(t *model.Task) error
UpdateField(t *model.Task, field string, value interface{}) error
Delete(t *model.Task) error
GetAll(ctx *model.UserContext) ([]model.Task, error)
GetAllByProject(ctx *model.UserContext, project model.Project) ([]model.Task, error)
GetAllByDate(ctx *model.UserContext, date time.Time) ([]model.Task, error)
GetAllByDateRange(ctx *model.UserContext, from, to time.Time) ([]model.Task, error)
GetByID(ctx *model.UserContext, ID string) (model.Task, error)
GetByUUID(ctx *model.UserContext, UUID string) (model.Task, error)
Create(ctx *model.UserContext, project model.Project, title, details, UUID string, dueDate *int64) (model.Task, error)
Update(ctx *model.UserContext, t *model.Task) error
UpdateField(ctx *model.UserContext, t *model.Task, field string, value interface{}) error
Delete(ctx *model.UserContext, t *model.Task) error
}

30
repository/user.go Normal file
View File

@@ -0,0 +1,30 @@
package repository
import "github.com/ajaxray/geek-life/model"
// UserRepository interface defines methods for user data access
type UserRepository interface {
GetByID(id int64) (*model.User, error)
GetByUsername(tenantID int64, username string) (*model.User, error)
GetByEmail(tenantID int64, email string) (*model.User, error)
Create(tenantID int64, username, email, passwordHash string) (*model.User, error)
Update(user *model.User) error
Delete(user *model.User) error
}
// TenantRepository interface defines methods for tenant data access
type TenantRepository interface {
GetByID(id int64) (*model.Tenant, error)
GetByName(name string) (*model.Tenant, error)
Create(name string) (*model.Tenant, error)
Update(tenant *model.Tenant) error
Delete(tenant *model.Tenant) error
}
// SessionRepository interface defines methods for session management
type SessionRepository interface {
GetByToken(token string) (*model.UserSession, error)
Create(userID int64, token string, expiresAt int64) (*model.UserSession, error)
Delete(session *model.UserSession) error
DeleteExpired() error
}