Initial commit: Homelab API
This commit is contained in:
292
internal/service/auth.go
Normal file
292
internal/service/auth.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"github.com/daniil/homelab-api/internal/model"
|
||||
"github.com/daniil/homelab-api/internal/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrWeakPassword = errors.New("password must be at least 8 characters")
|
||||
ErrEmailNotVerified = errors.New("email not verified")
|
||||
)
|
||||
|
||||
type AuthService struct {
|
||||
userRepo *repository.UserRepository
|
||||
emailTokenRepo *repository.EmailTokenRepository
|
||||
emailService *EmailService
|
||||
jwtSecret string
|
||||
}
|
||||
|
||||
func NewAuthService(
|
||||
userRepo *repository.UserRepository,
|
||||
emailTokenRepo *repository.EmailTokenRepository,
|
||||
emailService *EmailService,
|
||||
jwtSecret string,
|
||||
) *AuthService {
|
||||
return &AuthService{
|
||||
userRepo: userRepo,
|
||||
emailTokenRepo: emailTokenRepo,
|
||||
emailService: emailService,
|
||||
jwtSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AuthService) Register(req *model.RegisterRequest) (*model.AuthResponse, error) {
|
||||
if len(req.Password) < 8 {
|
||||
return nil, ErrWeakPassword
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &model.User{
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
PasswordHash: string(hash),
|
||||
EmailVerified: false,
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Send verification email
|
||||
if err := s.sendVerificationEmail(user); err != nil {
|
||||
// Log error but don't fail registration
|
||||
_ = err
|
||||
}
|
||||
|
||||
return s.generateAuthResponse(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) Login(req *model.LoginRequest) (*model.AuthResponse, error) {
|
||||
user, err := s.userRepo.GetByEmail(req.Email)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrUserNotFound) {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Optional: require email verification
|
||||
// if !user.EmailVerified {
|
||||
// return nil, ErrEmailNotVerified
|
||||
// }
|
||||
|
||||
return s.generateAuthResponse(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) Refresh(refreshToken string) (*model.AuthResponse, error) {
|
||||
claims, err := s.validateToken(refreshToken, "refresh")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userID, ok := claims["user_id"].(float64)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(int64(userID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.generateAuthResponse(user)
|
||||
}
|
||||
|
||||
func (s *AuthService) GetUser(userID int64) (*model.User, error) {
|
||||
return s.userRepo.GetByID(userID)
|
||||
}
|
||||
|
||||
func (s *AuthService) UpdateProfile(userID int64, req *model.UpdateProfileRequest) (*model.User, error) {
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Username != "" {
|
||||
user.Username = req.Username
|
||||
}
|
||||
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) ChangePassword(userID int64, req *model.ChangePasswordRequest) error {
|
||||
if len(req.NewPassword) < 8 {
|
||||
return ErrWeakPassword
|
||||
}
|
||||
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.userRepo.UpdatePassword(userID, string(hash))
|
||||
}
|
||||
|
||||
// Email verification
|
||||
|
||||
func (s *AuthService) sendVerificationEmail(user *model.User) error {
|
||||
// Delete any existing verification tokens
|
||||
s.emailTokenRepo.DeleteByUserAndType(user.ID, "verification")
|
||||
|
||||
// Create new token (24 hours)
|
||||
token, err := s.emailTokenRepo.Create(user.ID, "verification", 24*time.Hour)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.emailService.SendVerificationEmail(user.Email, user.Username, token.Token)
|
||||
}
|
||||
|
||||
func (s *AuthService) VerifyEmail(req *model.VerifyEmailRequest) error {
|
||||
token, err := s.emailTokenRepo.Validate(req.Token, "verification")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.userRepo.SetEmailVerified(token.UserID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.emailTokenRepo.MarkUsed(token.ID)
|
||||
}
|
||||
|
||||
func (s *AuthService) ResendVerification(req *model.ResendVerificationRequest) error {
|
||||
user, err := s.userRepo.GetByEmail(req.Email)
|
||||
if err != nil {
|
||||
// Don't reveal if user exists
|
||||
return nil
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s.sendVerificationEmail(user)
|
||||
}
|
||||
|
||||
// Password reset
|
||||
|
||||
func (s *AuthService) ForgotPassword(req *model.ForgotPasswordRequest) error {
|
||||
user, err := s.userRepo.GetByEmail(req.Email)
|
||||
if err != nil {
|
||||
// Don't reveal if user exists
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete any existing reset tokens
|
||||
s.emailTokenRepo.DeleteByUserAndType(user.ID, "reset")
|
||||
|
||||
// Create new token (1 hour)
|
||||
token, err := s.emailTokenRepo.Create(user.ID, "reset", 1*time.Hour)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.emailService.SendPasswordResetEmail(user.Email, user.Username, token.Token)
|
||||
}
|
||||
|
||||
func (s *AuthService) ResetPassword(req *model.ResetPasswordRequest) error {
|
||||
if len(req.NewPassword) < 8 {
|
||||
return ErrWeakPassword
|
||||
}
|
||||
|
||||
token, err := s.emailTokenRepo.Validate(req.Token, "reset")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.userRepo.UpdatePassword(token.UserID, string(hash)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.emailTokenRepo.MarkUsed(token.ID)
|
||||
}
|
||||
|
||||
func (s *AuthService) generateAuthResponse(user *model.User) (*model.AuthResponse, error) {
|
||||
accessToken, err := s.generateToken(user.ID, "access", 15*time.Minute)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
refreshToken, err := s.generateToken(user.ID, "refresh", 30*24*time.Hour)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.AuthResponse{
|
||||
User: user,
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *AuthService) generateToken(userID int64, tokenType string, expiry time.Duration) (string, error) {
|
||||
claims := jwt.MapClaims{
|
||||
"user_id": userID,
|
||||
"type": tokenType,
|
||||
"exp": time.Now().Add(expiry).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(s.jwtSecret))
|
||||
}
|
||||
|
||||
func (s *AuthService) validateToken(tokenString, expectedType string) (jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return []byte(s.jwtSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
if tokenType, ok := claims["type"].(string); !ok || tokenType != expectedType {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
133
internal/service/email.go
Normal file
133
internal/service/email.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type EmailService struct {
|
||||
apiKey string
|
||||
fromEmail string
|
||||
fromName string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
type ResendRequest struct {
|
||||
From string `json:"from"`
|
||||
To []string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
HTML string `json:"html"`
|
||||
}
|
||||
|
||||
func NewEmailService(apiKey, fromEmail, fromName, baseURL string) *EmailService {
|
||||
return &EmailService{
|
||||
apiKey: apiKey,
|
||||
fromEmail: fromEmail,
|
||||
fromName: fromName,
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *EmailService) SendVerificationEmail(toEmail, username, token string) error {
|
||||
verifyURL := fmt.Sprintf("%s/verify-email?token=%s", s.baseURL, token)
|
||||
|
||||
html := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #FAF9F6; margin: 0; padding: 20px; }
|
||||
.container { max-width: 500px; margin: 0 auto; padding: 40px; background: white; border-radius: 24px; }
|
||||
h2 { color: #0f766e; margin-bottom: 8px; }
|
||||
.button { display: inline-block; padding: 14px 28px; background: linear-gradient(135deg, #14b8a6, #0f766e); color: white; text-decoration: none; border-radius: 16px; font-weight: 600; }
|
||||
.link { color: #0f766e; word-break: break-all; }
|
||||
.footer { margin-top: 30px; font-size: 13px; color: #888; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Привет, %s! 👋</h2>
|
||||
<p>Спасибо за регистрацию в Pulse! Подтверди свой email, нажав на кнопку ниже:</p>
|
||||
<p style="margin: 30px 0;"><a href="%s" class="button">Подтвердить email</a></p>
|
||||
<p>Или скопируй ссылку:<br><a href="%s" class="link">%s</a></p>
|
||||
<p class="footer">Ссылка действительна 24 часа. Если ты не регистрировался — просто проигнорируй это письмо.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`, username, verifyURL, verifyURL, verifyURL)
|
||||
|
||||
return s.send(toEmail, "Подтверди свой email — Pulse", html)
|
||||
}
|
||||
|
||||
func (s *EmailService) SendPasswordResetEmail(toEmail, username, token string) error {
|
||||
resetURL := fmt.Sprintf("%s/reset-password?token=%s", s.baseURL, token)
|
||||
|
||||
html := fmt.Sprintf(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #FAF9F6; margin: 0; padding: 20px; }
|
||||
.container { max-width: 500px; margin: 0 auto; padding: 40px; background: white; border-radius: 24px; }
|
||||
h2 { color: #0f766e; margin-bottom: 8px; }
|
||||
.button { display: inline-block; padding: 14px 28px; background: linear-gradient(135deg, #14b8a6, #0f766e); color: white; text-decoration: none; border-radius: 16px; font-weight: 600; }
|
||||
.link { color: #0f766e; word-break: break-all; }
|
||||
.footer { margin-top: 30px; font-size: 13px; color: #888; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h2>Сброс пароля</h2>
|
||||
<p>Привет, %s! Мы получили запрос на сброс пароля для твоего аккаунта в Pulse.</p>
|
||||
<p style="margin: 30px 0;"><a href="%s" class="button">Сбросить пароль</a></p>
|
||||
<p>Или скопируй ссылку:<br><a href="%s" class="link">%s</a></p>
|
||||
<p class="footer">Ссылка действительна 1 час. Если ты не запрашивал сброс пароля — просто проигнорируй это письмо.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>`, username, resetURL, resetURL, resetURL)
|
||||
|
||||
return s.send(toEmail, "Сброс пароля — Pulse", html)
|
||||
}
|
||||
|
||||
func (s *EmailService) send(to, subject, html string) error {
|
||||
if s.apiKey == "" {
|
||||
fmt.Printf("[EMAIL] Would send to %s: %s\n", to, subject)
|
||||
return nil
|
||||
}
|
||||
|
||||
payload := ResendRequest{
|
||||
From: fmt.Sprintf("%s <%s>", s.fromName, s.fromEmail),
|
||||
To: []string{to},
|
||||
Subject: subject,
|
||||
HTML: html,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://api.resend.com/emails", bytes.NewBuffer(jsonData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+s.apiKey)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("resend API error: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
191
internal/service/habit.go
Normal file
191
internal/service/habit.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/daniil/homelab-api/internal/model"
|
||||
"github.com/daniil/homelab-api/internal/repository"
|
||||
)
|
||||
|
||||
type HabitService struct {
|
||||
habitRepo *repository.HabitRepository
|
||||
}
|
||||
|
||||
func NewHabitService(habitRepo *repository.HabitRepository) *HabitService {
|
||||
return &HabitService{habitRepo: habitRepo}
|
||||
}
|
||||
|
||||
func (s *HabitService) Create(userID int64, req *model.CreateHabitRequest) (*model.Habit, error) {
|
||||
habit := &model.Habit{
|
||||
UserID: userID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
Color: defaultString(req.Color, "#6366f1"),
|
||||
Icon: defaultString(req.Icon, "check"),
|
||||
Frequency: defaultString(req.Frequency, "daily"),
|
||||
TargetDays: req.TargetDays,
|
||||
TargetCount: defaultInt(req.TargetCount, 1),
|
||||
}
|
||||
|
||||
if err := s.habitRepo.Create(habit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return habit, nil
|
||||
}
|
||||
|
||||
func (s *HabitService) Get(id, userID int64) (*model.Habit, error) {
|
||||
return s.habitRepo.GetByID(id, userID)
|
||||
}
|
||||
|
||||
func (s *HabitService) List(userID int64, includeArchived bool) ([]model.Habit, error) {
|
||||
habits, err := s.habitRepo.ListByUser(userID, includeArchived)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if habits == nil {
|
||||
habits = []model.Habit{}
|
||||
}
|
||||
return habits, nil
|
||||
}
|
||||
|
||||
func (s *HabitService) Update(id, userID int64, req *model.UpdateHabitRequest) (*model.Habit, error) {
|
||||
habit, err := s.habitRepo.GetByID(id, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Name != nil {
|
||||
habit.Name = *req.Name
|
||||
}
|
||||
if req.Description != nil {
|
||||
habit.Description = *req.Description
|
||||
}
|
||||
if req.Color != nil {
|
||||
habit.Color = *req.Color
|
||||
}
|
||||
if req.Icon != nil {
|
||||
habit.Icon = *req.Icon
|
||||
}
|
||||
if req.Frequency != nil {
|
||||
habit.Frequency = *req.Frequency
|
||||
}
|
||||
if req.TargetDays != nil {
|
||||
habit.TargetDays = req.TargetDays
|
||||
}
|
||||
if req.TargetCount != nil {
|
||||
habit.TargetCount = *req.TargetCount
|
||||
}
|
||||
if req.IsArchived != nil {
|
||||
habit.IsArchived = *req.IsArchived
|
||||
}
|
||||
|
||||
if err := s.habitRepo.Update(habit); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return habit, nil
|
||||
}
|
||||
|
||||
func (s *HabitService) Delete(id, userID int64) error {
|
||||
return s.habitRepo.Delete(id, userID)
|
||||
}
|
||||
|
||||
func (s *HabitService) Log(habitID, userID int64, req *model.LogHabitRequest) (*model.HabitLog, error) {
|
||||
// Verify habit exists and belongs to user
|
||||
if _, err := s.habitRepo.GetByID(habitID, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
date := time.Now().Truncate(24 * time.Hour)
|
||||
if req.Date != "" {
|
||||
parsed, err := time.Parse("2006-01-02", req.Date)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
date = parsed
|
||||
}
|
||||
|
||||
log := &model.HabitLog{
|
||||
HabitID: habitID,
|
||||
UserID: userID,
|
||||
Date: date,
|
||||
Count: defaultInt(req.Count, 1),
|
||||
Note: req.Note,
|
||||
}
|
||||
|
||||
if err := s.habitRepo.CreateLog(log); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return log, nil
|
||||
}
|
||||
|
||||
func (s *HabitService) GetLogs(habitID, userID int64, days int) ([]model.HabitLog, error) {
|
||||
// Verify habit exists and belongs to user
|
||||
if _, err := s.habitRepo.GetByID(habitID, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
to := time.Now()
|
||||
from := to.AddDate(0, 0, -days)
|
||||
|
||||
logs, err := s.habitRepo.GetLogs(habitID, userID, from, to)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if logs == nil {
|
||||
logs = []model.HabitLog{}
|
||||
}
|
||||
return logs, nil
|
||||
}
|
||||
|
||||
func (s *HabitService) DeleteLog(logID, userID int64) error {
|
||||
return s.habitRepo.DeleteLog(logID, userID)
|
||||
}
|
||||
|
||||
func (s *HabitService) GetHabitStats(habitID, userID int64) (*model.HabitStats, error) {
|
||||
// Verify habit exists and belongs to user
|
||||
if _, err := s.habitRepo.GetByID(habitID, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.habitRepo.GetStats(habitID, userID)
|
||||
}
|
||||
|
||||
func (s *HabitService) GetOverallStats(userID int64) (*model.OverallStats, error) {
|
||||
habits, err := s.habitRepo.ListByUser(userID, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allHabits, err := s.habitRepo.ListByUser(userID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
todayLogs, err := s.habitRepo.GetUserLogsForDate(userID, time.Now().Truncate(24*time.Hour))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &model.OverallStats{
|
||||
TotalHabits: len(allHabits),
|
||||
ActiveHabits: len(habits),
|
||||
TodayCompleted: len(todayLogs),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func defaultString(val, def string) string {
|
||||
if val == "" {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func defaultInt(val, def int) int {
|
||||
if val == 0 {
|
||||
return def
|
||||
}
|
||||
return val
|
||||
}
|
||||
126
internal/service/task.go
Normal file
126
internal/service/task.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"github.com/daniil/homelab-api/internal/model"
|
||||
"github.com/daniil/homelab-api/internal/repository"
|
||||
)
|
||||
|
||||
type TaskService struct {
|
||||
taskRepo *repository.TaskRepository
|
||||
}
|
||||
|
||||
func NewTaskService(taskRepo *repository.TaskRepository) *TaskService {
|
||||
return &TaskService{taskRepo: taskRepo}
|
||||
}
|
||||
|
||||
func (s *TaskService) Create(userID int64, req *model.CreateTaskRequest) (*model.Task, error) {
|
||||
task := &model.Task{
|
||||
UserID: userID,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Icon: defaultString(req.Icon, "📋"),
|
||||
Color: defaultString(req.Color, "#6B7280"),
|
||||
Priority: req.Priority,
|
||||
}
|
||||
|
||||
if req.DueDate != nil && *req.DueDate != "" {
|
||||
parsed, err := time.Parse("2006-01-02", *req.DueDate)
|
||||
if err == nil {
|
||||
task.DueDate = sql.NullTime{Time: parsed, Valid: true}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Create(task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.ProcessForJSON()
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (s *TaskService) Get(id, userID int64) (*model.Task, error) {
|
||||
return s.taskRepo.GetByID(id, userID)
|
||||
}
|
||||
|
||||
func (s *TaskService) List(userID int64, completed *bool) ([]model.Task, error) {
|
||||
tasks, err := s.taskRepo.ListByUser(userID, completed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tasks == nil {
|
||||
tasks = []model.Task{}
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (s *TaskService) GetTodayTasks(userID int64) ([]model.Task, error) {
|
||||
tasks, err := s.taskRepo.GetTodayTasks(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tasks == nil {
|
||||
tasks = []model.Task{}
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (s *TaskService) Update(id, userID int64, req *model.UpdateTaskRequest) (*model.Task, error) {
|
||||
task, err := s.taskRepo.GetByID(id, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
task.Title = *req.Title
|
||||
}
|
||||
if req.Description != nil {
|
||||
task.Description = *req.Description
|
||||
}
|
||||
if req.Icon != nil {
|
||||
task.Icon = *req.Icon
|
||||
}
|
||||
if req.Color != nil {
|
||||
task.Color = *req.Color
|
||||
}
|
||||
if req.Priority != nil {
|
||||
task.Priority = *req.Priority
|
||||
}
|
||||
if req.DueDate != nil {
|
||||
if *req.DueDate == "" {
|
||||
task.DueDate = sql.NullTime{Valid: false}
|
||||
} else {
|
||||
parsed, err := time.Parse("2006-01-02", *req.DueDate)
|
||||
if err == nil {
|
||||
task.DueDate = sql.NullTime{Time: parsed, Valid: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.taskRepo.Update(task); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task.ProcessForJSON()
|
||||
return task, nil
|
||||
}
|
||||
|
||||
func (s *TaskService) Delete(id, userID int64) error {
|
||||
return s.taskRepo.Delete(id, userID)
|
||||
}
|
||||
|
||||
func (s *TaskService) Complete(id, userID int64) (*model.Task, error) {
|
||||
if err := s.taskRepo.Complete(id, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.taskRepo.GetByID(id, userID)
|
||||
}
|
||||
|
||||
func (s *TaskService) Uncomplete(id, userID int64) (*model.Task, error) {
|
||||
if err := s.taskRepo.Uncomplete(id, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.taskRepo.GetByID(id, userID)
|
||||
}
|
||||
Reference in New Issue
Block a user