initial commit

This commit is contained in:
hanif salafi 2025-09-19 11:08:42 +07:00
commit a88bd957e3
324 changed files with 60919 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/vendor
debug.log
/.ideadebug.log

33
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,33 @@
stages:
- build-image
- deploy
#build-1:
# stage: build-app
# image: golang:alpine
# script:
# - go build -o main .
# artifacts:
# paths:
# - main
build-2:
stage: build-image
image: docker/compose:latest
services:
- name: docker:dind
command: [ "--insecure-registry=103.82.242.92:8900" ]
script:
- docker login -u $DEPLOY_USERNAME -p $DEPLOY_TOKEN 103.82.242.92:8900
- docker-compose build
- docker tag registry.gitlab.com/hanifsalafi/narasi-ahli-be:dev 103.82.242.92:8900/medols/narasi-ahli-be:dev
- docker push 103.82.242.92:8900/medols/narasi-ahli-be:dev
deploy:
stage: deploy
when: on_success
image: curlimages/curl:latest
services:
- docker:dind
script:
- curl --user $JENKINS_USER:$JENKINS_PWD http://38.47.180.165:8080/job/autodeploy-medols-be/build?token=autodeploymedols

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/narasi-ahli-be.iml" filepath="$PROJECT_DIR$/.idea/narasi-ahli-be.iml" />
</modules>
</component>
</project>

9
.idea/narasi-ahli-be.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM golang:alpine
WORKDIR /app
COPY . .
RUN go mod download
RUN go get -v ./...
RUN go mod vendor
RUN go build -o main .
EXPOSE 8800
CMD ["sh", "-c", "go run main.go"]

View File

@ -0,0 +1,7 @@
package entity
type ActivityLogTypes struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool"`
}

View File

@ -0,0 +1,15 @@
package entity
import (
"time"
)
type ActivityLogs struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ActivityTypeId int `json:"activity_type_id" gorm:"type:int4"`
Url string `json:"url" gorm:"type:varchar"`
VisitorIp *string `json:"visitor_ip" gorm:"type:varchar"`
ArticleId *uint `json:"article_id" gorm:"type:int4"`
UserId *uint `json:"user_id" gorm:"type:int4"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,20 @@
package entity
import (
"time"
)
type Advertisement struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
RedirectLink string `json:"redirect_link" gorm:"type:varchar"`
ContentFilePath *string `json:"content_file_path" gorm:"type:varchar"`
ContentFileName *string `json:"content_file_name" gorm:"type:varchar"`
Placement string `json:"placement" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish bool `json:"is_publish" gorm:"type:bool"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type AIChatLogs struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
SessionID uint `json:"session_id" gorm:"type:int4;not null;index"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
StartDate time.Time `json:"start_date" gorm:"not null"`
EndDate *time.Time `json:"end_date"`
TotalDuration int64 `json:"total_duration" gorm:"type:bigint;default:0"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
Session *AIChatSessions `json:"session" gorm:"foreignKey:SessionID;references:ID"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,14 @@
package entity
import (
"time"
)
type AIChatMessages struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
SessionID uint `json:"session_id" gorm:"type:int4;not null;index"`
MessageType string `json:"message_type" gorm:"type:varchar;not null"`
Content string `json:"content" gorm:"type:text;not null"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
Session *AIChatSessions `json:"session" gorm:"foreignKey:SessionID;references:ID"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type AIChatSessions struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
AISessionID string `json:"ai_session_id" gorm:"type:varchar;not null;unique"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
AgentID *string `json:"agent_id" gorm:"type:varchar"`
Title string `json:"title" gorm:"type:varchar;not null"`
MessageCount int `json:"message_count" gorm:"type:int4;default:0"`
Status string `json:"status" gorm:"type:varchar;default:'active'"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,15 @@
package entity
import (
"time"
)
type ArticleApprovals struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
ApprovalBy uint `json:"approval_by" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
Message string `json:"message" gorm:"type:varchar"`
ApprovalAtLevel *int `json:"approval_at_level" gorm:"type:int4"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,24 @@
package entity
import (
"time"
)
type ArticleCategories struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
Slug *string `json:"slug" gorm:"type:varchar"`
ParentId *int `json:"parent_id" gorm:"type:int4"`
Tags *string `json:"tags" gorm:"type:varchar"`
Position *int `json:"position" gorm:"type:int4"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
OldCategoryId *uint `json:"old_category_id" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4;default:1"`
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,16 @@
package article_category_details
import (
entity "narasi-ahli-be/app/database/entity"
"time"
)
type ArticleCategoryDetails struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
CategoryId int `json:"category_id" gorm:"type:int4"`
Category *entity.ArticleCategories `json:"category" gorm:"foreignKey:CategoryId;references:ID"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,21 @@
package entity
import (
"time"
)
type ArticleComments struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Message string `json:"message" gorm:"type:varchar"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
CommentFrom *uint `json:"comment_from" gorm:"type:int4"`
ParentId *int `json:"parent_id" gorm:"type:int4"`
IsPublic bool `json:"is_public" gorm:"type:bool;default:false"`
StatusId int `json:"status_id" gorm:"type:int4;default:0"`
ApprovedAt *time.Time `json:"approved_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}
// statusId => 0: waiting, 1: accepted, 2: replied, 3: rejected

View File

@ -0,0 +1,27 @@
package entity
import (
"time"
)
type ArticleFiles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ArticleId uint `json:"article_id" gorm:"type:int4"`
UploadID *string `json:"upload_id" gorm:"type:varchar"`
FilePath *string `json:"file_path" gorm:"type:varchar"`
FileUrl *string `json:"file_url" gorm:"type:varchar"`
FileName *string `json:"file_name" gorm:"type:varchar"`
FileThumbnail *string `json:"file_thumbnail" gorm:"type:varchar"`
FileAlt *string `json:"file_alt" gorm:"type:varchar"`
WidthPixel *string `json:"width_pixel" gorm:"type:varchar"`
HeightPixel *string `json:"height_pixel" gorm:"type:varchar"`
Size *string `json:"size" gorm:"type:varchar"`
DownloadCount *int `json:"download_count" gorm:"type:int4;default:0"`
CreatedById int `json:"created_by_id" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,37 @@
package entity
import (
"time"
)
type Articles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Slug string `json:"slug" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
CategoryId int `json:"category_id" gorm:"type:int4"`
HtmlDescription string `json:"html_description" gorm:"type:varchar"`
TypeId int `json:"type_id" gorm:"type:int4"`
Tags string `json:"tags" gorm:"type:varchar"`
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
PageUrl *string `json:"page_url" gorm:"type:varchar"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
AiArticleId *int `json:"ai_article_id" gorm:"type:int4"`
CommentCount *int `json:"comment_count" gorm:"type:int4;default:0"`
ShareCount *int `json:"share_count" gorm:"type:int4;default:0"`
ViewCount *int `json:"view_count" gorm:"type:int4;default:0"`
StatusId *int `json:"status_id" gorm:"type:int4"`
OldId *uint `json:"old_id" gorm:"type:int4"`
NeedApprovalFrom *int `json:"need_approval_from" gorm:"type:int4"`
HasApprovedBy *string `json:"has_approved_by" gorm:"type:varchar"`
IsPublish *bool `json:"is_publish" gorm:"type:bool;default:false"`
IsBanner *bool `json:"is_banner" gorm:"type:bool;default:false"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsDraft *bool `json:"is_draft" gorm:"type:bool;default:false"`
DraftedAt *time.Time `json:"drafted_at" gorm:"type:timestamp"`
PublishSchedule *string `json:"publish_schedule" gorm:"type:varchar"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"time"
)
type AuditTrails struct {
ID uint `gorm:"primaryKey"`
Method string
Path string
IP string
Status int
UserID *string
RequestHeaders string
RequestBody string
ResponseBody string
DurationMs int64
CreatedAt time.Time
}

View File

@ -0,0 +1,21 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type ChatMessages struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ConversationID uint `json:"conversation_id" gorm:"type:int4;not null;index"`
SenderID uint `json:"sender_id" gorm:"type:int4;not null;index"`
MessageText *string `json:"message_text" gorm:"type:text"`
MessageType string `json:"message_type" gorm:"type:varchar;not null;default:'text'"`
FileURL *string `json:"file_url" gorm:"type:varchar"`
FileName *string `json:"file_name" gorm:"type:varchar"`
FileSize *int64 `json:"file_size" gorm:"type:bigint"`
IsRead bool `json:"is_read" gorm:"default:false"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
Conversation *Conversations `json:"conversation" gorm:"foreignKey:ConversationID;references:ID"`
Sender *users.Users `json:"sender" gorm:"foreignKey:SenderID;references:ID"`
}

View File

@ -0,0 +1,7 @@
package entity
type Cities struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
CityName string `json:"city_name" gorm:"type:varchar"`
ProvId int `json:"prov_id" gorm:"type:int4"`
}

View File

@ -0,0 +1,17 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type Conversations struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Participant1ID uint `json:"participant1_id" gorm:"type:int4;not null;index"`
Participant2ID uint `json:"participant2_id" gorm:"type:int4;not null;index"`
LastMessageAt *time.Time `json:"last_message_at"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
Participant1 *users.Users `json:"participant1" gorm:"foreignKey:Participant1ID;references:ID"`
Participant2 *users.Users `json:"participant2" gorm:"foreignKey:Participant2ID;references:ID"`
}

View File

@ -0,0 +1,13 @@
package entity
import (
"time"
)
type CsrfTokenRecords struct {
ID uint `gorm:"primaryKey"`
Token string `gorm:"uniqueIndex;size:255"`
Value []byte `gorm:"value"`
ExpireAt time.Time `gorm:"index"`
CreatedAt time.Time
}

View File

@ -0,0 +1,16 @@
package entity
import (
"time"
)
type CustomStaticPages struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
Slug string `json:"slug" gorm:"type:varchar"`
HtmlBody string `json:"html_body" gorm:"type:text"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,7 @@
package entity
type Districts struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
DisName string `json:"dis_name" gorm:"type:varchar"`
CityId int `json:"city_id" gorm:"type:int4"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type EducationHistory struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
SchoolName string `json:"school_name" gorm:"type:varchar;not null"`
Major string `json:"major" gorm:"type:varchar;not null"`
EducationLevel string `json:"education_level" gorm:"type:varchar;not null"`
GraduationYear int `json:"graduation_year" gorm:"type:int4;not null"`
CertificateImage *string `json:"certificate_image" gorm:"type:varchar"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,20 @@
package entity
import (
"time"
)
type Feedbacks struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Message string `json:"message" gorm:"type:varchar"`
CommentFromName string `json:"comment_from_name" gorm:"type:varchar"`
CommentFromEmail string `json:"comment_from_email" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4;default:0"`
ApprovedAt *time.Time `json:"approved_at" gorm:"type:timestamp"`
ReplyMessage *string `json:"reply_message" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}
// statusId => 0: waiting, 1: accepted, 2: replied, 3: nothing

View File

@ -0,0 +1,14 @@
package entity
import (
"time"
)
type ForgotPasswords struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
KeycloakID string `json:"keycloak_id" gorm:"type:varchar"`
CodeRequest string `json:"code_request" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,24 @@
package entity
import "time"
type MagazineFiles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
MagazineId uint `json:"magazine_id" gorm:"type:int4"`
DownloadCount *int `json:"download_count" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish *bool `json:"is_publish" gorm:"type:bool"`
FilePath *string `json:"file_path" gorm:"type:varchar"`
FileUrl *string `json:"file_url" gorm:"type:varchar"`
FileName *string `json:"file_name" gorm:"type:varchar"`
FileAlt *string `json:"file_alt" gorm:"type:varchar"`
WidthPixel *string `json:"width_pixel" gorm:"type:varchar"`
HeightPixel *string `json:"height_pixel" gorm:"type:varchar"`
Size *string `json:"size" gorm:"type:varchar"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,20 @@
package entity
import "time"
type Magazines struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Title string `json:"title" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
ThumbnailName *string `json:"thumbnail_name" gorm:"type:varchar"`
ThumbnailPath *string `json:"thumbnail_path" gorm:"type:varchar"`
ThumbnailUrl *string `json:"thumbnail_url" gorm:"type:varchar"`
PageUrl *string `json:"page_url" gorm:"type:varchar"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsPublish *bool `json:"is_publish" gorm:"type:bool"`
PublishedAt *time.Time `json:"published_at" gorm:"type:timestamp"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,7 @@
package entity
type MasterApprovalStatuses struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool"`
}

View File

@ -0,0 +1,18 @@
package entity
import "time"
type MasterMenus struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
ModuleId int `json:"module_id" gorm:"type:int4"`
ParentMenuId *int `json:"parent_menu_id" gorm:"type:int4"`
Icon *string `json:"icon" gorm:"type:varchar"`
Group string `json:"group" gorm:"type:varchar"`
Position *int `json:"position" gorm:"type:int4"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,16 @@
package entity
import (
"time"
)
type MasterModules struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
PathUrl string `json:"path_url" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,7 @@
package entity
type MasterStatuses struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool"`
}

View File

@ -0,0 +1,16 @@
package entity
import (
"time"
)
type OneTimePasswords struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Email string `json:"email" gorm:"type:varchar"`
Name *string `json:"name" gorm:"type:varchar"`
Identity *string `json:"identity" gorm:"type:varchar"`
OtpCode string `json:"otp_code" gorm:"type:varchar"`
ValidUntil time.Time `json:"valid_until" gorm:"default:(NOW() + INTERVAL '10 minutes')"`
IsActive bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,9 @@
package entity
type Provinces struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
ProvName string `json:"prov_name" gorm:"type:varchar"`
LocationId int `json:"location_id" gorm:"type:int4"`
Status int `json:"status" gorm:"type:int4"`
Timezone string `json:"timezone" gorm:"type:varchar"`
}

View File

@ -0,0 +1,18 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type ResearchJournals struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
JournalTitle string `json:"journal_title" gorm:"type:varchar;not null"`
Publisher string `json:"publisher" gorm:"type:varchar;not null"`
JournalURL string `json:"journal_url" gorm:"type:varchar;not null"`
PublishedDate *time.Time `json:"published_date"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,14 @@
package entity
import (
"time"
)
type Subscription struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Email string `json:"email" gorm:"type:varchar"`
IsActive bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,17 @@
package user_levels
import "time"
type UserLevels struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
AliasName string `json:"alias_name" gorm:"type:varchar"`
LevelNumber int `json:"level_number" gorm:"type:int4"`
ParentLevelId *int `json:"parent_level_id" gorm:"type:int4"`
ProvinceId *int `json:"province_id" gorm:"type:int4"`
Group *string `json:"group" gorm:"type:varchar"`
IsApprovalActive *bool `json:"is_approval_active" gorm:"type:bool;default:false"`
IsActive *bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,17 @@
package entity
import "time"
type UserLevels struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
AliasName string `json:"alias_name" gorm:"type:varchar"`
LevelNumber int `json:"level_number" gorm:"type:int4"`
ParentLevelId *int `json:"parent_level_id" gorm:"type:int4"`
ProvinceId *int `json:"province_id" gorm:"type:int4"`
Group *string `json:"group" gorm:"type:varchar"`
IsApprovalActive *bool `json:"is_approval_active" gorm:"type:bool;default:false"`
IsActive *bool `json:"is_active" gorm:"type:bool"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,20 @@
package entity
import (
"time"
)
type UserRoleAccesses struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
MenuId int `json:"menu_id" gorm:"type:int4"`
IsViewEnabled bool `json:"is_view_enabled" gorm:"type:bool"`
IsInsertEnabled bool `json:"is_insert_enabled" gorm:"type:bool"`
IsUpdateEnabled bool `json:"is_update_enabled" gorm:"type:bool"`
IsDeleteEnabled bool `json:"is_delete_enabled" gorm:"type:bool"`
IsApprovalEnabled bool `json:"is_approval_enabled" gorm:"type:bool"`
IsAdminEnabled bool `json:"is_admin_enabled" gorm:"type:bool"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,15 @@
package entity
import (
"time"
)
type UserRoleLevelDetails struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,19 @@
package entity
import (
"time"
)
type UserRoles struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Name string `json:"name" gorm:"type:varchar"`
Description string `json:"description" gorm:"type:varchar"`
Code string `json:"code" gorm:"type:varchar"`
StatusId int `json:"status_id" gorm:"type:int4;default:1"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,38 @@
package users
import (
userLevels "narasi-ahli-be/app/database/entity/user_levels"
"time"
)
type Users struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Username string `json:"username" gorm:"type:varchar"`
Email string `json:"email" gorm:"type:varchar"`
Fullname string `json:"fullname" gorm:"type:varchar"`
Address *string `json:"address" gorm:"type:varchar"`
PhoneNumber *string `json:"phone_number" gorm:"type:varchar"`
WorkType *string `json:"work_type" gorm:"type:varchar"`
GenderType *string `json:"gender_type" gorm:"type:varchar"`
IdentityType *string `json:"identity_type" gorm:"type:varchar"`
IdentityGroup *string `json:"identity_group" gorm:"type:varchar"`
IdentityGroupNumber *string `json:"identity_group_number" gorm:"type:varchar"`
IdentityNumber *string `json:"identity_number" gorm:"type:varchar"`
DateOfBirth *string `json:"date_of_birth" gorm:"type:varchar"`
LastEducation *string `json:"last_education" gorm:"type:varchar"`
Degree *string `json:"degree" gorm:"type:varchar"`
WhatsappNumber *string `json:"whatsapp_number" gorm:"type:varchar"`
LastJobTitle *string `json:"last_job_title" gorm:"type:varchar"`
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
UserLevel *userLevels.UserLevels `json:"user_levels" gorm:"foreignKey:UserLevelId;references:ID"`
KeycloakId *string `json:"keycloak_id" gorm:"type:varchar"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
ProfilePicturePath *string `json:"profile_picture_path" gorm:"type:varchar"`
TempPassword *string `json:"temp_password" gorm:"type:varchar"`
IsEmailUpdated *bool `json:"is_email_updated" gorm:"type:bool;default:false"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,38 @@
package users
import (
userLevels "narasi-ahli-be/app/database/entity/user_levels"
"time"
)
type Users struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
Username string `json:"username" gorm:"type:varchar"`
Email string `json:"email" gorm:"type:varchar"`
Fullname string `json:"fullname" gorm:"type:varchar"`
Address *string `json:"address" gorm:"type:varchar"`
PhoneNumber *string `json:"phone_number" gorm:"type:varchar"`
WorkType *string `json:"work_type" gorm:"type:varchar"`
GenderType *string `json:"gender_type" gorm:"type:varchar"`
IdentityType *string `json:"identity_type" gorm:"type:varchar"`
IdentityGroup *string `json:"identity_group" gorm:"type:varchar"`
IdentityGroupNumber *string `json:"identity_group_number" gorm:"type:varchar"`
IdentityNumber *string `json:"identity_number" gorm:"type:varchar"`
DateOfBirth *string `json:"date_of_birth" gorm:"type:varchar"`
LastEducation *string `json:"last_education" gorm:"type:varchar"`
Degree *string `json:"degree" gorm:"type:varchar"`
WhatsappNumber *string `json:"whatsapp_number" gorm:"type:varchar"`
LastJobTitle *string `json:"last_job_title" gorm:"type:varchar"`
UserRoleId uint `json:"user_role_id" gorm:"type:int4"`
UserLevelId uint `json:"user_level_id" gorm:"type:int4"`
UserLevel *userLevels.UserLevels `json:"user_levels" gorm:"foreignKey:UserLevelId;references:ID"`
KeycloakId *string `json:"keycloak_id" gorm:"type:varchar"`
StatusId *int `json:"status_id" gorm:"type:int4;default:1"`
CreatedById *uint `json:"created_by_id" gorm:"type:int4"`
ProfilePicturePath *string `json:"profile_picture_path" gorm:"type:varchar"`
TempPassword *string `json:"temp_password" gorm:"type:varchar"`
IsEmailUpdated *bool `json:"is_email_updated" gorm:"type:bool;default:false"`
IsActive *bool `json:"is_active" gorm:"type:bool;default:true"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
}

View File

@ -0,0 +1,18 @@
package entity
import (
"narasi-ahli-be/app/database/entity/users"
"time"
)
type WorkHistory struct {
ID uint `json:"id" gorm:"primaryKey;type:int4;autoIncrement"`
UserID uint `json:"user_id" gorm:"type:int4;not null;index"`
JobTitle string `json:"job_title" gorm:"type:varchar;not null"`
CompanyName string `json:"company_name" gorm:"type:varchar;not null"`
StartDate time.Time `json:"start_date" gorm:"not null"`
EndDate *time.Time `json:"end_date"`
CreatedAt time.Time `json:"created_at" gorm:"default:now()"`
UpdatedAt time.Time `json:"updated_at" gorm:"default:now()"`
User *users.Users `json:"user" gorm:"foreignKey:UserID;references:ID"`
}

View File

@ -0,0 +1,151 @@
package database
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/database/entity/article_category_details"
"narasi-ahli-be/app/database/entity/user_levels"
"narasi-ahli-be/app/database/entity/users"
"narasi-ahli-be/config/config"
"github.com/rs/zerolog"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// Database setup database with gorm
type Database struct {
DB *gorm.DB
Log zerolog.Logger
Cfg *config.Config
}
type Seeder interface {
Seed(*gorm.DB) error
Count(*gorm.DB) (int, error)
}
func NewDatabase(cfg *config.Config, log zerolog.Logger) *Database {
db := &Database{
Cfg: cfg,
Log: log,
}
return db
}
// ConnectDatabase connect database
func (_db *Database) ConnectDatabase() {
logMode := _db.Cfg.DB.Postgres.LogMode
var logLevel logger.LogLevel
if logMode == "INFO" {
logLevel = logger.Info
} else if logMode == "WARN" {
logLevel = logger.Warn
} else if logMode == "ERROR" {
logLevel = logger.Error
} else if logMode == "NONE" {
logLevel = logger.Silent
}
conn, err := gorm.Open(postgres.Open(_db.Cfg.DB.Postgres.DSN), &gorm.Config{
Logger: logger.Default.LogMode(logLevel),
})
if err != nil {
_db.Log.Error().Err(err).Msg("An unknown error occurred when to connect the database!")
} else {
_db.Log.Info().Msg("Connected the database succesfully!")
}
_db.DB = conn
}
// ShutdownDatabase shutdown database
func (_db *Database) ShutdownDatabase() {
sqlDB, err := _db.DB.DB()
if err != nil {
_db.Log.Error().Err(err).Msg("An unknown error occurred when to shutdown the database!")
} else {
_db.Log.Info().Msg("Shutdown the database succesfully!")
}
sqlDB.Close()
}
// MigrateModels migrate models
func (_db *Database) MigrateModels() {
err := _db.DB.AutoMigrate(
Models()...,
)
if err != nil {
_db.Log.Error().Err(err).Msg("An unknown error occurred when to migrate the database!")
} else {
_db.Log.Info().Msg("Migrate the database entity succesfully!")
}
}
// Models list of models for migration
func Models() []interface{} {
return []interface{}{
entity.ActivityLogs{},
entity.ActivityLogTypes{},
entity.Advertisement{},
entity.Articles{},
entity.ArticleCategories{},
entity.ArticleApprovals{},
article_category_details.ArticleCategoryDetails{},
entity.ArticleFiles{},
entity.ArticleComments{},
entity.AuditTrails{},
entity.Cities{},
entity.CsrfTokenRecords{},
entity.CustomStaticPages{},
entity.Districts{},
entity.Feedbacks{},
entity.ForgotPasswords{},
entity.Magazines{},
entity.MagazineFiles{},
entity.MasterMenus{},
entity.MasterModules{},
entity.MasterStatuses{},
entity.MasterApprovalStatuses{},
entity.Provinces{},
entity.OneTimePasswords{},
entity.Subscription{},
user_levels.UserLevels{},
entity.UserRoles{},
entity.UserRoleAccesses{},
users.Users{},
entity.UserRoleLevelDetails{},
// Narasi Ahli entities
entity.EducationHistory{},
entity.WorkHistory{},
entity.ResearchJournals{},
entity.Conversations{},
entity.ChatMessages{},
entity.AIChatSessions{},
entity.AIChatMessages{},
entity.AIChatLogs{},
}
}
// SeedModels seed data
func (_db *Database) SeedModels(seeder []Seeder) {
for _, seed := range seeder {
count, err := seed.Count(_db.DB)
if err != nil {
_db.Log.Error().Err(err).Msg("An unknown error occurred when to seed the database!")
}
if count == 0 {
if err := seed.Seed(_db.DB); err != nil {
_db.Log.Error().Err(err).Msg("An unknown error occurred when to seed the database!")
}
_db.Log.Info().Msg("Seeded the database successfully!")
} else {
_db.Log.Info().Msg("Database is already seeded!")
}
}
_db.Log.Info().Msg("Seeded the database succesfully!")
}

View File

@ -0,0 +1,51 @@
package seeds
import (
"narasi-ahli-be/app/database/entity"
"gorm.io/gorm"
)
type ActivityLogsSeeder struct{}
var activityLogTypes = []entity.ActivityLogTypes{
{
ID: 1,
Name: "Login",
IsActive: true,
},
{
ID: 2,
Name: "View",
IsActive: true,
},
{
ID: 3,
Name: "Share",
IsActive: true,
},
{
ID: 4,
Name: "Comment",
IsActive: true,
},
}
func (ActivityLogsSeeder) Seed(conn *gorm.DB) error {
for _, row := range activityLogTypes {
if err := conn.Create(&row).Error; err != nil {
return err
}
}
return nil
}
func (ActivityLogsSeeder) Count(conn *gorm.DB) (int, error) {
var count int64
if err := conn.Model(&entity.ActivityLogTypes{}).Count(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}

View File

@ -0,0 +1,46 @@
package seeds
import (
"narasi-ahli-be/app/database/entity"
"gorm.io/gorm"
)
type MasterApprovalStatusesSeeder struct{}
var masterApprovalStatuses = []entity.MasterApprovalStatuses{
{
ID: 1,
Name: "Accepted",
IsActive: true,
},
{
ID: 2,
Name: "Need Update",
IsActive: true,
},
{
ID: 3,
Name: "Rejected",
IsActive: true,
},
}
func (MasterApprovalStatusesSeeder) Seed(conn *gorm.DB) error {
for _, row := range masterApprovalStatuses {
if err := conn.Create(&row).Error; err != nil {
return err
}
}
return nil
}
func (MasterApprovalStatusesSeeder) Count(conn *gorm.DB) (int, error) {
var count int64
if err := conn.Model(&entity.MasterApprovalStatuses{}).Count(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}

View File

@ -0,0 +1,46 @@
package seeds
import (
"narasi-ahli-be/app/database/entity"
"gorm.io/gorm"
)
type MasterStatusesSeeder struct{}
var masterStatuses = []entity.MasterStatuses{
{
ID: 1,
Name: "Waiting",
IsActive: true,
},
{
ID: 2,
Name: "Active",
IsActive: true,
},
{
ID: 3,
Name: "Inactive",
IsActive: true,
},
}
func (MasterStatusesSeeder) Seed(conn *gorm.DB) error {
for _, row := range masterStatuses {
if err := conn.Create(&row).Error; err != nil {
return err
}
}
return nil
}
func (MasterStatusesSeeder) Count(conn *gorm.DB) (int, error) {
var count int64
if err := conn.Model(&entity.MasterStatuses{}).Count(&count).Error; err != nil {
return 0, err
}
return int(count), nil
}

BIN
app/go-humas-be.exe Normal file

Binary file not shown.

View File

@ -0,0 +1,69 @@
package middleware
import (
"encoding/json"
"log"
"narasi-ahli-be/app/database/entity"
utilSvc "narasi-ahli-be/utils/service"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
func AuditTrailsMiddleware(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
start := time.Now()
requestBody := c.Body()
headersMap := c.GetReqHeaders()
headersJSON, _ := json.Marshal(headersMap)
authHeader := c.Get("Authorization")
userId := utilSvc.GetUserId(authHeader)
err := c.Next()
audit := entity.AuditTrails{
Method: c.Method(),
Path: c.OriginalURL(),
IP: getIP(c),
Status: c.Response().StatusCode(),
UserID: userId,
RequestHeaders: string(headersJSON),
RequestBody: string(requestBody),
ResponseBody: string(c.Response().Body()),
DurationMs: time.Since(start).Milliseconds(),
CreatedAt: time.Now(),
}
go db.Create(&audit)
return err
}
}
func StartAuditTrailCleanup(db *gorm.DB, retention int) {
go func() {
for {
time.Sleep(24 * time.Hour)
cutoff := time.Now().AddDate(0, 0, retention)
db.Where("created_at < ?", cutoff).Delete(&entity.AuditTrails{})
log.Printf(" at: %s", cutoff)
}
}()
}
func getIP(c *fiber.Ctx) string {
ip := c.Get("X-Forwarded-For")
if ip == "" {
ip = c.IP()
}
if strings.Contains(ip, ":") {
ip = strings.Split(ip, ":")[0]
}
return ip
}

View File

@ -0,0 +1,79 @@
package middleware
import (
"fmt"
"narasi-ahli-be/app/database/entity"
"time"
"gorm.io/gorm"
)
type PostgresStorage struct {
DB *gorm.DB
}
func (s *PostgresStorage) Get(key string) ([]byte, error) {
//log.Printf("CSRF Storage: Get token %s", key)
var record entity.CsrfTokenRecords
result := s.DB.Where("token = ?", key).First(&record)
if result.Error != nil {
//log.Printf("CSRF Storage Get error: %v for token: %s", result.Error, key)
return nil, result.Error
}
if record.ExpireAt.Before(time.Now()) {
//log.Printf("CSRF token %s is expired", key)
return nil, fmt.Errorf("CSRF token is expired")
}
return record.Value, nil
}
func (s *PostgresStorage) Set(key string, value []byte, exp time.Duration) error {
//log.Printf("CSRF Storage: Setting token %s with expiration %v", key, exp)
// Calculate expiration time
expireAt := time.Now().Add(exp)
// Try to update existing record first
result := s.DB.Model(&entity.CsrfTokenRecords{}).
Where("token = ?", key).
Updates(map[string]interface{}{
"expire_at": expireAt,
})
// If no rows were affected (not found), create a new record
if result.RowsAffected == 0 {
record := entity.CsrfTokenRecords{
Token: key,
Value: value,
ExpireAt: expireAt,
CreatedAt: time.Now(),
}
if err := s.DB.Create(&record).Error; err != nil {
//log.Printf("CSRF Storage: Error saving token: %v", err)
return err
}
} else if result.Error != nil {
//log.Printf("CSRF Storage: Error updating token: %v", result.Error)
return result.Error
}
//log.Printf("CSRF Storage: Successfully saved/updated token")
return nil
}
func (s *PostgresStorage) Delete(key string) error {
return s.DB.Where("token = ?", key).Delete(&entity.CsrfTokenRecords{}).Error
}
func (s *PostgresStorage) Reset() error {
return s.DB.Where("expire_at < ?", time.Now()).Delete(&entity.CsrfTokenRecords{}).Error
}
func (s *PostgresStorage) Close() error {
return nil
}

View File

@ -0,0 +1,166 @@
package middleware
import (
"log"
"narasi-ahli-be/app/database"
"narasi-ahli-be/config/config"
utilsSvc "narasi-ahli-be/utils"
"time"
"github.com/gofiber/fiber/v2/middleware/csrf"
"github.com/gofiber/fiber/v2/middleware/session"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/compress"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/gofiber/fiber/v2/middleware/monitor"
"github.com/gofiber/fiber/v2/middleware/pprof"
"github.com/gofiber/fiber/v2/middleware/recover"
"github.com/gofiber/fiber/v2/utils"
)
// Middleware is a struct that contains all the middleware functions
type Middleware struct {
App *fiber.App
Cfg *config.Config
}
func NewMiddleware(app *fiber.App, cfg *config.Config) *Middleware {
return &Middleware{
App: app,
Cfg: cfg,
}
}
// Register registers all the middleware functions
func (m *Middleware) Register(db *database.Database) {
// Add Extra Middlewares
m.App.Use(limiter.New(limiter.Config{
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Limiter.Enable),
Max: m.Cfg.Middleware.Limiter.Max,
Expiration: m.Cfg.Middleware.Limiter.Expiration * time.Second,
}))
m.App.Use(compress.New(compress.Config{
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Compress.Enable),
Level: m.Cfg.Middleware.Compress.Level,
}))
m.App.Use(recover.New(recover.Config{
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Recover.Enable),
}))
m.App.Use(pprof.New(pprof.Config{
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Pprof.Enable),
}))
m.App.Use(cors.New(cors.Config{
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Cors.Enable),
AllowOrigins: "http://localhost:3000, http://localhost:4000, https://dev.mikulnews.com, https://n8n.qudoco.com, https://narasiahli.com",
AllowMethods: "HEAD, GET, POST, PUT, DELETE, OPTION, PATCH",
AllowHeaders: "Origin, Content-Type, Accept, Accept-Language, Authorization, X-Requested-With, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, X-Csrf-Token, Cookie, Set-Cookie, X-Client-Key",
ExposeHeaders: "Content-Length, Content-Type",
AllowCredentials: true,
MaxAge: 12,
}))
//===============================
// CSRF CONFIG
//===============================
// Custom storage for CSRF
csrfSessionStorage := &PostgresStorage{
DB: db.DB,
}
// Store initialization for session
store := session.New(session.Config{
CookieSameSite: m.Cfg.Middleware.Csrf.CookieSameSite,
CookieSecure: m.Cfg.Middleware.Csrf.CookieSecure,
CookieSessionOnly: m.Cfg.Middleware.Csrf.CookieSessionOnly,
CookieHTTPOnly: m.Cfg.Middleware.Csrf.CookieHttpOnly,
Storage: csrfSessionStorage,
})
m.App.Use(func(c *fiber.Ctx) error {
sess, err := store.Get(c)
if err != nil {
return err
}
c.Locals("session", sess)
return c.Next()
})
// Cleanup the expired token
go func() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
if err := csrfSessionStorage.Reset(); err != nil {
log.Printf("Error cleaning up expired CSRF tokens: %v", err)
}
}
}()
m.App.Use(csrf.New(csrf.Config{
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Csrf.Enable),
KeyLookup: "header:" + csrf.HeaderName,
CookieName: m.Cfg.Middleware.Csrf.CookieName,
CookieSameSite: m.Cfg.Middleware.Csrf.CookieSameSite,
CookieSecure: m.Cfg.Middleware.Csrf.CookieSecure,
CookieSessionOnly: m.Cfg.Middleware.Csrf.CookieSessionOnly,
CookieHTTPOnly: m.Cfg.Middleware.Csrf.CookieHttpOnly,
Expiration: 1 * time.Hour,
KeyGenerator: utils.UUIDv4,
ContextKey: "csrf",
ErrorHandler: func(c *fiber.Ctx, err error) error {
return utilsSvc.CsrfErrorHandler(c, err)
},
Extractor: csrf.CsrfFromHeader(csrf.HeaderName),
Session: store,
SessionKey: "fiber.csrf.token",
}))
//===============================
m.App.Use(AuditTrailsMiddleware(db.DB))
StartAuditTrailCleanup(db.DB, m.Cfg.Middleware.AuditTrails.Retention)
//m.App.Use(filesystem.New(filesystem.Config{
// Next: utils.IsEnabled(m.Cfg.Middleware.FileSystem.Enable),
// Root: http.Dir(m.Cfg.Middleware.FileSystem.Root),
// Browse: m.Cfg.Middleware.FileSystem.Browse,
// MaxAge: m.Cfg.Middleware.FileSystem.MaxAge,
//}))
// ==================================================
m.App.Get(m.Cfg.Middleware.Monitor.Path, monitor.New(monitor.Config{
Next: utilsSvc.IsEnabled(m.Cfg.Middleware.Monitor.Enable),
}))
// Route for generate CSRF token
m.App.Get("/csrf-token", func(c *fiber.Ctx) error {
// Retrieve CSRF token from Fiber's middleware context
token, ok := c.Locals("csrf").(string)
//c.Context().VisitUserValues(func(key []byte, value interface{}) {
// log.Printf("Local Key: %s, Value: %v", key, value)
//})
if !ok || token == "" {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"success": false,
"code": 500,
"messages": []string{"Failed to retrieve CSRF token"},
})
}
return c.JSON(fiber.Map{
"success": true,
"csrf_token": token,
})
})
}

View File

@ -0,0 +1,55 @@
package activity_logs
import (
"narasi-ahli-be/app/module/activity_logs/controller"
"narasi-ahli-be/app/module/activity_logs/repository"
"narasi-ahli-be/app/module/activity_logs/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ActivityLogsRouter
type ActivityLogsRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ActivityLogs module
var NewActivityLogsModule = fx.Options(
// register repository of ActivityLogs module
fx.Provide(repository.NewActivityLogsRepository),
// register service of ActivityLogs module
fx.Provide(service.NewActivityLogsService),
// register controller of ActivityLogs module
fx.Provide(controller.NewController),
// register router of ActivityLogs module
fx.Provide(NewActivityLogsRouter),
)
// init ActivityLogsRouter
func NewActivityLogsRouter(fiber *fiber.App, controller *controller.Controller) *ActivityLogsRouter {
return &ActivityLogsRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ActivityLogs module
func (_i *ActivityLogsRouter) RegisterActivityLogsRoutes() {
// define controllers
activityLogsController := _i.Controller.ActivityLogs
// define routes
_i.App.Route("/activity-logs", func(router fiber.Router) {
router.Get("/", activityLogsController.All)
router.Get("/statistics", activityLogsController.GetActivityStats)
router.Get("/detail/:id", activityLogsController.Show)
router.Post("/", activityLogsController.Save)
router.Put("/:id", activityLogsController.Update)
router.Delete("/:id", activityLogsController.Delete)
})
}

View File

@ -0,0 +1,250 @@
package controller
import (
"narasi-ahli-be/app/module/activity_logs/request"
"narasi-ahli-be/app/module/activity_logs/service"
"narasi-ahli-be/utils/paginator"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
)
type activityLogsController struct {
activityLogsService service.ActivityLogsService
Log zerolog.Logger
}
type ActivityLogsController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
GetActivityStats(c *fiber.Ctx) error
}
func NewActivityLogsController(activityLogsService service.ActivityLogsService, log zerolog.Logger) ActivityLogsController {
return &activityLogsController{
activityLogsService: activityLogsService,
Log: log,
}
}
// All get all ActivityLogs
// @Summary Get all ActivityLogs
// @Description API for getting all ActivityLogs
// @Tags ActivityLogs
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param req query request.ActivityLogsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /activity-logs [get]
func (_i *activityLogsController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.ActivityLogsQueryRequestContext{
ActivityTypeId: c.Query("activityTypeId"),
Url: c.Query("url"),
ArticleId: c.Query("articleId"),
UserId: c.Query("userId"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
activityLogsData, paging, err := _i.activityLogsService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ActivityLogs list successfully retrieved"},
Data: activityLogsData,
Meta: paging,
})
}
// Show get one ActivityLogs
// @Summary Get one ActivityLogs
// @Description API for getting one ActivityLogs
// @Tags ActivityLogs
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param id path int true "ActivityLogs ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /activity-logs/detail/{id} [get]
func (_i *activityLogsController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
activityLogsData, err := _i.activityLogsService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ActivityLogs successfully retrieved"},
Data: activityLogsData,
})
}
// Save create ActivityLogs
// @Summary Create ActivityLogs
// @Description API for create ActivityLogs
// @Tags ActivityLogs
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ActivityLogsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /activity-logs [post]
func (_i *activityLogsController) Save(c *fiber.Ctx) error {
req := new(request.ActivityLogsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
var authToken *string
getTokenFromHeader := c.Get("Authorization")
if getTokenFromHeader == "" {
authToken = nil
} else {
authToken = &getTokenFromHeader
}
visitorIp := GetVisitorIP(c)
req.VisitorIp = &visitorIp
dataResult, err := _i.activityLogsService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ActivityLogs successfully created"},
Data: dataResult,
})
}
// Update update ActivityLogs
// @Summary update ActivityLogs
// @Description API for update ActivityLogs
// @Tags ActivityLogs
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ActivityLogsUpdateRequest true "Required payload"
// @Param id path int true "ActivityLogs ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /activity-logs/{id} [put]
func (_i *activityLogsController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ActivityLogsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.activityLogsService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ActivityLogs successfully updated"},
})
}
// Delete delete ActivityLogs
// @Summary delete ActivityLogs
// @Description API for delete ActivityLogs
// @Tags ActivityLogs
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ActivityLogs ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /activity-logs/{id} [delete]
func (_i *activityLogsController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.activityLogsService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ActivityLogs successfully deleted"},
})
}
// GetActivityStats get activity stats ActivityLogs
// @Summary Get activity stats ActivityLogs
// @Description API for get activity stats ActivityLogs
// @Tags ActivityLogs
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /activity-logs/statistics [get]
func (_i *activityLogsController) GetActivityStats(c *fiber.Ctx) error {
_i.Log.Info().Interface("GetActivityStats", "checker controller").Msg("")
activityStatsData, err := _i.activityLogsService.GetActivityStats()
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ActivityLogs Stats successfully retrieved"},
Data: activityStatsData,
})
}
func GetVisitorIP(c *fiber.Ctx) string {
ip := c.Get("X-Forwarded-For")
if ip == "" {
ip = c.IP()
}
if strings.Contains(ip, ":") {
ip = strings.Split(ip, ":")[0]
}
return ip
}

View File

@ -0,0 +1,17 @@
package controller
import (
"narasi-ahli-be/app/module/activity_logs/service"
"github.com/rs/zerolog"
)
type Controller struct {
ActivityLogs ActivityLogsController
}
func NewController(ActivityLogsService service.ActivityLogsService, log zerolog.Logger) *Controller {
return &Controller{
ActivityLogs: NewActivityLogsController(ActivityLogsService, log),
}
}

View File

@ -0,0 +1,20 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
res "narasi-ahli-be/app/module/activity_logs/response"
)
func ActivityLogsResponseMapper(activityLogsReq *entity.ActivityLogs) (activityLogsRes *res.ActivityLogsResponse) {
if activityLogsReq != nil {
activityLogsRes = &res.ActivityLogsResponse{
ID: activityLogsReq.ID,
ActivityTypeId: activityLogsReq.ActivityTypeId,
Url: activityLogsReq.Url,
ArticleId: activityLogsReq.ArticleId,
UserId: activityLogsReq.UserId,
CreatedAt: activityLogsReq.CreatedAt,
}
}
return activityLogsRes
}

View File

@ -0,0 +1,124 @@
package repository
import (
"fmt"
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/activity_logs/request"
"narasi-ahli-be/utils/paginator"
"strings"
"time"
"github.com/rs/zerolog"
)
type activityLogsRepository struct {
DB *database.Database
Log zerolog.Logger
}
// ActivityLogsRepository define interface of IActivityLogsRepository
type ActivityLogsRepository interface {
GetAll(req request.ActivityLogsQueryRequest) (activityLogss []*entity.ActivityLogs, paging paginator.Pagination, err error)
FindOne(id uint) (activityLogs *entity.ActivityLogs, err error)
Create(activityLogs *entity.ActivityLogs) (activityLogsReturn *entity.ActivityLogs, err error)
Update(id uint, activityLogs *entity.ActivityLogs) (err error)
Delete(id uint) (err error)
CountUniqueVisitorAllTime() (count int64, err error)
CountUniqueVisitorToday() (count int64, err error)
CountTotalViewAllTime() (count int64, err error)
}
func NewActivityLogsRepository(db *database.Database, logger zerolog.Logger) ActivityLogsRepository {
return &activityLogsRepository{
DB: db,
Log: logger,
}
}
// implement interface of IActivityLogsRepository
func (_i *activityLogsRepository) GetAll(req request.ActivityLogsQueryRequest) (activityLogss []*entity.ActivityLogs, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.ActivityLogs{})
if req.ActivityTypeId != nil {
query = query.Where("activity_type_id = ?", req.ActivityTypeId)
}
if req.Url != nil && *req.Url != "" {
url := strings.ToLower(*req.Url)
query = query.Where("LOWER(url) LIKE ?", "%"+strings.ToLower(url)+"%")
}
if req.ArticleId != nil {
query = query.Where("article_id = ?", req.ArticleId)
}
if req.UserId != nil {
query = query.Where("user_id = ?", req.UserId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&activityLogss).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *activityLogsRepository) FindOne(id uint) (activityLogs *entity.ActivityLogs, err error) {
query := _i.DB.DB.Where("id = ?", id)
if err := query.First(&activityLogs).Error; err != nil {
return nil, err
}
return activityLogs, nil
}
func (_i *activityLogsRepository) Create(activityLogs *entity.ActivityLogs) (activityLogsReturn *entity.ActivityLogs, err error) {
result := _i.DB.DB.Create(activityLogs)
return activityLogs, result.Error
}
func (_i *activityLogsRepository) Update(id uint, activityLogs *entity.ActivityLogs) (err error) {
query := _i.DB.DB.Model(&entity.ActivityLogs{}).Where(&entity.ActivityLogs{ID: id})
return query.Updates(activityLogs).Error
}
func (_i *activityLogsRepository) Delete(id uint) error {
query := _i.DB.DB.Model(&entity.ActivityLogs{}).Where("id = ?", id)
return query.Delete(&entity.ActivityLogs{}).Error
}
func (_i *activityLogsRepository) CountUniqueVisitorAllTime() (count int64, err error) {
query := _i.DB.DB.Model(&entity.ActivityLogs{})
err = query.Distinct("visitor_ip").Count(&count).Error
return
}
func (_i *activityLogsRepository) CountTotalViewAllTime() (count int64, err error) {
query := _i.DB.DB.Model(&entity.ActivityLogs{}).Where("activity_type_id = ?", 2)
err = query.Count(&count).Error
return
}
func (_i *activityLogsRepository) CountUniqueVisitorToday() (count int64, err error) {
tenMinutesAgo := time.Now().Add(-10 * time.Minute)
query := _i.DB.DB.Model(&entity.AuditTrails{}).Where("created_at >= ?", tenMinutesAgo)
err = query.Select("ip").Group("ip").Count(&count).Error
return
}

View File

@ -0,0 +1,92 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"strconv"
"time"
)
type ActivityLogsGeneric interface {
ToEntity()
}
type ActivityLogsQueryRequest struct {
ActivityTypeId *int `json:"activityTypeId"`
Url *string `json:"url"`
ArticleId *int `json:"articleId"`
UserId *int `json:"userId"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ActivityLogsCreateRequest struct {
ActivityTypeId int `json:"activityTypeId" validate:"required"`
Url string `json:"url" validate:"required"`
ArticleId *uint `json:"articleId"`
UserId *uint `json:"userId"`
VisitorIp *string `json:"visitorIp"`
}
func (req ActivityLogsCreateRequest) ToEntity() *entity.ActivityLogs {
return &entity.ActivityLogs{
ActivityTypeId: req.ActivityTypeId,
Url: req.Url,
ArticleId: req.ArticleId,
UserId: req.UserId,
VisitorIp: req.VisitorIp,
CreatedAt: time.Now(),
}
}
type ActivityLogsUpdateRequest struct {
ID uint `json:"id" validate:"required"`
ActivityTypeId int `json:"activityTypeId" validate:"required"`
Url string `json:"url" validate:"required"`
ArticleId *uint `json:"articleId"`
UserId *uint `json:"userId"`
}
func (req ActivityLogsUpdateRequest) ToEntity() *entity.ActivityLogs {
return &entity.ActivityLogs{
ID: req.ID,
ActivityTypeId: req.ActivityTypeId,
Url: req.Url,
ArticleId: req.ArticleId,
UserId: req.UserId,
}
}
type ActivityLogsQueryRequestContext struct {
ActivityTypeId string `json:"activityTypeId"`
Url string `json:"url"`
ArticleId string `json:"articleId"`
UserId string `json:"userId"`
}
func (req ActivityLogsQueryRequestContext) ToParamRequest() ActivityLogsQueryRequest {
var request ActivityLogsQueryRequest
if activityTypeIdStr := req.ActivityTypeId; activityTypeIdStr != "" {
activityTypeId, err := strconv.Atoi(activityTypeIdStr)
if err == nil {
request.ActivityTypeId = &activityTypeId
}
}
if url := req.Url; url != "" {
request.Url = &url
}
if articleIdStr := req.ArticleId; articleIdStr != "" {
articleId, err := strconv.Atoi(articleIdStr)
if err == nil {
request.ArticleId = &articleId
}
}
if userIdStr := req.UserId; userIdStr != "" {
userId, err := strconv.Atoi(userIdStr)
if err == nil {
request.UserId = &userId
}
}
return request
}

View File

@ -0,0 +1,18 @@
package response
import "time"
type ActivityLogsResponse struct {
ID uint `json:"id"`
ActivityTypeId int `json:"activityTypeId"`
Url string `json:"url"`
ArticleId *uint `json:"articleId"`
UserId *uint `json:"userId"`
CreatedAt time.Time `json:"createdAt"`
}
type ActivityStatsResponse struct {
TotalVisitorAllTime int64 `json:"totalVisitorAllTime"`
TotalVisitorToday int64 `json:"totalVisitorToday"`
TotalViewAllTime int64 `json:"totalViewAllTime"`
}

View File

@ -0,0 +1,124 @@
package service
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/activity_logs/mapper"
"narasi-ahli-be/app/module/activity_logs/repository"
"narasi-ahli-be/app/module/activity_logs/request"
"narasi-ahli-be/app/module/activity_logs/response"
"narasi-ahli-be/app/module/articles/service"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"github.com/rs/zerolog"
)
// ActivityLogsService
type activityLogsService struct {
Repo repository.ActivityLogsRepository
UsersRepo usersRepository.UsersRepository
ArticleService service.ArticlesService
Log zerolog.Logger
}
// ActivityLogsService define interface of IActivityLogsService
type ActivityLogsService interface {
All(req request.ActivityLogsQueryRequest) (activityLogs []*response.ActivityLogsResponse, paging paginator.Pagination, err error)
Show(id uint) (activityLogs *response.ActivityLogsResponse, err error)
Save(req request.ActivityLogsCreateRequest, authToken *string) (activityLogs *entity.ActivityLogs, err error)
Update(id uint, req request.ActivityLogsUpdateRequest) (err error)
Delete(id uint) error
GetActivityStats() (activityStats *response.ActivityStatsResponse, err error)
}
// NewActivityLogsService init ActivityLogsService
func NewActivityLogsService(repo repository.ActivityLogsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, articleService service.ArticlesService) ActivityLogsService {
return &activityLogsService{
Repo: repo,
Log: log,
UsersRepo: usersRepo,
ArticleService: articleService,
}
}
// All implement interface of ActivityLogsService
func (_i *activityLogsService) All(req request.ActivityLogsQueryRequest) (activityLogss []*response.ActivityLogsResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
activityLogss = append(activityLogss, mapper.ActivityLogsResponseMapper(result))
}
return
}
func (_i *activityLogsService) Show(id uint) (activityLogs *response.ActivityLogsResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.ActivityLogsResponseMapper(result), nil
}
func (_i *activityLogsService) Save(req request.ActivityLogsCreateRequest, authToken *string) (activityLogs *entity.ActivityLogs, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
if authToken != nil {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, *authToken)
newReq.UserId = &createdBy.ID
}
result, err := _i.Repo.Create(newReq)
if err != nil {
return nil, err
}
// update article
err = _i.ArticleService.UpdateActivityCount(*req.ArticleId, req.ActivityTypeId)
if err != nil {
return nil, err
}
return result, nil
}
func (_i *activityLogsService) Update(id uint, req request.ActivityLogsUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
return _i.Repo.Update(id, newReq)
}
func (_i *activityLogsService) Delete(id uint) error {
return _i.Repo.Delete(id)
}
func (_i *activityLogsService) GetActivityStats() (activityStats *response.ActivityStatsResponse, err error) {
_i.Log.Info().Interface("GetActivityStats", "checker").Msg("")
countUniqueVisitorAllTime, err := _i.Repo.CountUniqueVisitorAllTime()
if err != nil {
return nil, err
}
countUniqueVisitorToday, err := _i.Repo.CountUniqueVisitorToday()
if err != nil {
return nil, err
}
countTotalViewAllTime, err := _i.Repo.CountTotalViewAllTime()
if err != nil {
return nil, err
}
getActivityStats := &response.ActivityStatsResponse{
TotalVisitorAllTime: countUniqueVisitorAllTime,
TotalVisitorToday: countUniqueVisitorToday,
TotalViewAllTime: countTotalViewAllTime,
}
return getActivityStats, nil
}

View File

@ -0,0 +1,57 @@
package advertisement
import (
"narasi-ahli-be/app/module/advertisement/controller"
"narasi-ahli-be/app/module/advertisement/repository"
"narasi-ahli-be/app/module/advertisement/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of AdvertisementRouter
type AdvertisementRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of Advertisement module
var NewAdvertisementModule = fx.Options(
// register repository of Advertisement module
fx.Provide(repository.NewAdvertisementRepository),
// register service of Advertisement module
fx.Provide(service.NewAdvertisementService),
// register controller of Advertisement module
fx.Provide(controller.NewController),
// register router of Advertisement module
fx.Provide(NewAdvertisementRouter),
)
// init AdvertisementRouter
func NewAdvertisementRouter(fiber *fiber.App, controller *controller.Controller) *AdvertisementRouter {
return &AdvertisementRouter{
App: fiber,
Controller: controller,
}
}
// register routes of Advertisement module
func (_i *AdvertisementRouter) RegisterAdvertisementRoutes() {
// define controllers
advertisementController := _i.Controller.Advertisement
// define routes
_i.App.Route("/advertisement", func(router fiber.Router) {
router.Get("/", advertisementController.All)
router.Get("/:id", advertisementController.Show)
router.Post("/", advertisementController.Save)
router.Post("/upload/:id", advertisementController.Upload)
router.Get("/viewer/:filename", advertisementController.Viewer)
router.Put("/:id", advertisementController.Update)
router.Put("/publish/:id", advertisementController.UpdatePublish)
router.Delete("/:id", advertisementController.Delete)
})
}

View File

@ -0,0 +1,292 @@
package controller
import (
"narasi-ahli-be/app/module/advertisement/request"
"narasi-ahli-be/app/module/advertisement/service"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
)
type advertisementController struct {
advertisementService service.AdvertisementService
Log zerolog.Logger
}
type AdvertisementController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Upload(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
UpdatePublish(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewAdvertisementController(advertisementService service.AdvertisementService, log zerolog.Logger) AdvertisementController {
return &advertisementController{
advertisementService: advertisementService,
Log: log,
}
}
// All get all Advertisement
// @Summary Get all Advertisement
// @Description API for getting all Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param req query request.AdvertisementQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement [get]
func (_i *advertisementController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.AdvertisementQueryRequestContext{
Title: c.Query("title"),
Description: c.Query("description"),
RedirectLink: c.Query("redirectLink"),
Placement: c.Query("placement"),
StatusId: c.Query("statusId"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
advertisementData, paging, err := _i.advertisementService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement list successfully retrieved"},
Data: advertisementData,
Meta: paging,
})
}
// Show get one Advertisement
// @Summary Get one Advertisement
// @Description API for getting one Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/{id} [get]
func (_i *advertisementController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
advertisementData, err := _i.advertisementService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully retrieved"},
Data: advertisementData,
})
}
// Save create Advertisement
// @Summary Create Advertisement
// @Description API for create Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.AdvertisementCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement [post]
func (_i *advertisementController) Save(c *fiber.Ctx) error {
req := new(request.AdvertisementCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
dataResult, err := _i.advertisementService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully created"},
Data: dataResult,
})
}
// Upload Advertisement
// @Summary Upload Advertisement
// @Description API for Upload File Advertisement
// @Tags Advertisement
// @Security Bearer
// @Produce json
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param file formData file true "Upload file" multiple false
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/upload/{id} [post]
func (_i *advertisementController) Upload(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.advertisementService.Upload(c, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully upload"},
})
}
// Update update Advertisement
// @Summary update Advertisement
// @Description API for update Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.AdvertisementUpdateRequest true "Required payload"
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/{id} [put]
func (_i *advertisementController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.AdvertisementUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.advertisementService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully updated"},
})
}
// UpdatePublish Advertisement
// @Summary Update Publish Advertisement
// @Description API for Update Publish Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param id path int true "Advertisement ID"
// @Param isPublish query bool true "Advertisement Publish Status"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/publish/{id} [put]
func (_i *advertisementController) UpdatePublish(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
isPublish, err := strconv.ParseBool(c.Query("isPublish"))
if err != nil {
return err
}
err = _i.advertisementService.UpdatePublish(uint(id), isPublish)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully publish updated"},
})
}
// Delete delete Advertisement
// @Summary delete Advertisement
// @Description API for delete Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Advertisement ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/{id} [delete]
func (_i *advertisementController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.advertisementService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Advertisement successfully deleted"},
})
}
// Viewer Advertisement
// @Summary Viewer Advertisement
// @Description API for Viewer Advertisement
// @Tags Advertisement
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param filename path string true "Content File Name"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /advertisement/viewer/{filename} [get]
func (_i *advertisementController) Viewer(c *fiber.Ctx) error {
return _i.advertisementService.Viewer(c)
}

View File

@ -0,0 +1,17 @@
package controller
import (
"narasi-ahli-be/app/module/advertisement/service"
"github.com/rs/zerolog"
)
type Controller struct {
Advertisement AdvertisementController
}
func NewController(AdvertisementService service.AdvertisementService, log zerolog.Logger) *Controller {
return &Controller{
Advertisement: NewAdvertisementController(AdvertisementService, log),
}
}

View File

@ -0,0 +1,28 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
res "narasi-ahli-be/app/module/advertisement/response"
)
func AdvertisementResponseMapper(advertisementReq *entity.Advertisement, host string) (advertisementRes *res.AdvertisementResponse) {
if advertisementReq != nil {
advertisementRes = &res.AdvertisementResponse{
ID: advertisementReq.ID,
Title: advertisementReq.Title,
Description: advertisementReq.Description,
RedirectLink: advertisementReq.RedirectLink,
Placement: advertisementReq.Placement,
StatusId: advertisementReq.StatusId,
IsActive: advertisementReq.IsActive,
IsPublish: advertisementReq.IsPublish,
CreatedAt: advertisementReq.CreatedAt,
UpdatedAt: advertisementReq.UpdatedAt,
}
if advertisementReq.ContentFilePath != nil {
advertisementRes.ContentFileUrl = host + "/advertisement/viewer/" + *advertisementReq.ContentFileName
}
}
return advertisementRes
}

View File

@ -0,0 +1,124 @@
package repository
import (
"fmt"
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/advertisement/request"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"strings"
"github.com/rs/zerolog"
)
type advertisementRepository struct {
DB *database.Database
Log zerolog.Logger
}
// AdvertisementRepository define interface of IAdvertisementRepository
type AdvertisementRepository interface {
GetAll(req request.AdvertisementQueryRequest) (advertisements []*entity.Advertisement, paging paginator.Pagination, err error)
FindOne(id uint) (advertisement *entity.Advertisement, err error)
FindByFilename(contentFilename string) (advertisement *entity.Advertisement, err error)
Create(advertisement *entity.Advertisement) (advertisementReturn *entity.Advertisement, err error)
Update(id uint, advertisement *entity.Advertisement) (err error)
Delete(id uint) (err error)
}
func NewAdvertisementRepository(db *database.Database, logger zerolog.Logger) AdvertisementRepository {
return &advertisementRepository{
DB: db,
Log: logger,
}
}
// implement interface of IAdvertisementRepository
func (_i *advertisementRepository) GetAll(req request.AdvertisementQueryRequest) (advertisements []*entity.Advertisement, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.Advertisement{})
query = query.Where("is_active = ?", true)
if req.Title != nil && *req.Title != "" {
title := strings.ToLower(*req.Title)
query = query.Where("LOWER(title) LIKE ?", "%"+strings.ToLower(title)+"%")
}
if req.Description != nil && *req.Description != "" {
description := strings.ToLower(*req.Description)
query = query.Where("LOWER(description) LIKE ?", "%"+strings.ToLower(description)+"%")
}
if req.RedirectLink != nil && *req.RedirectLink != "" {
redirectLink := strings.ToLower(*req.RedirectLink)
query = query.Where("LOWER(redirect_link) LIKE ?", "%"+strings.ToLower(redirectLink)+"%")
}
if req.Placement != nil && *req.Placement != "" {
placement := strings.ToLower(*req.Placement)
query = query.Where("LOWER(placement) LIKE ?", "%"+strings.ToLower(placement)+"%")
}
if req.StatusId != nil {
query = query.Where("status_id = ?", req.StatusId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&advertisements).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *advertisementRepository) FindOne(id uint) (advertisement *entity.Advertisement, err error) {
query := _i.DB.DB.Where("id = ?", id)
if err := query.First(&advertisement).Error; err != nil {
return nil, err
}
return advertisement, nil
}
func (_i *advertisementRepository) FindByFilename(contentFilename string) (advertisement *entity.Advertisement, err error) {
query := _i.DB.DB.Where("content_file_name = ?", contentFilename)
if err := query.First(&advertisement).Error; err != nil {
return nil, err
}
return advertisement, nil
}
func (_i *advertisementRepository) Create(advertisement *entity.Advertisement) (advertisementReturn *entity.Advertisement, err error) {
result := _i.DB.DB.Create(advertisement)
return advertisement, result.Error
}
func (_i *advertisementRepository) Update(id uint, advertisement *entity.Advertisement) (err error) {
advertisementMap, err := utilSvc.StructToMap(advertisement)
if err != nil {
return err
}
query := _i.DB.DB.Model(&entity.Advertisement{}).Where(&entity.Advertisement{ID: id})
return query.Updates(advertisementMap).Error
}
func (_i *advertisementRepository) Delete(id uint) error {
query := _i.DB.DB.Model(&entity.Advertisement{}).Where("id = ?", id)
return query.Delete(&entity.Advertisement{}).Error
}

View File

@ -0,0 +1,100 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"strconv"
"time"
)
type AdvertisementGeneric interface {
ToEntity()
}
type AdvertisementQueryRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
RedirectLink *string `json:"redirectLink"`
Placement *string `json:"placement"`
IsPublish *bool `json:"isPublish"`
StatusId *int `json:"statusId"`
Pagination *paginator.Pagination `json:"pagination"`
}
type AdvertisementCreateRequest struct {
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
RedirectLink string `json:"redirectLink" validate:"required"`
Placement string `json:"placement" validate:"required"`
}
func (req AdvertisementCreateRequest) ToEntity() *entity.Advertisement {
return &entity.Advertisement{
Title: req.Title,
Description: req.Description,
RedirectLink: req.RedirectLink,
Placement: req.Placement,
StatusId: 1,
IsPublish: true,
IsActive: true,
}
}
type AdvertisementUpdateRequest struct {
ID uint `json:"id" validate:"required"`
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
RedirectLink string `json:"redirectLink" validate:"required"`
Placement string `json:"placement" validate:"required"`
}
func (req AdvertisementUpdateRequest) ToEntity() *entity.Advertisement {
return &entity.Advertisement{
ID: req.ID,
Title: req.Title,
Description: req.Description,
RedirectLink: req.RedirectLink,
Placement: req.Placement,
UpdatedAt: time.Now(),
}
}
type AdvertisementQueryRequestContext struct {
Title string `json:"title"`
Description string `json:"description"`
RedirectLink string `json:"redirectLink"`
Placement string `json:"placement"`
StatusId string `json:"statusId"`
IsPublish string `json:"isPublish"`
}
func (req AdvertisementQueryRequestContext) ToParamRequest() AdvertisementQueryRequest {
var request AdvertisementQueryRequest
if title := req.Title; title != "" {
request.Title = &title
}
if description := req.Description; description != "" {
request.Description = &description
}
if redirectLink := req.RedirectLink; redirectLink != "" {
request.RedirectLink = &redirectLink
}
if placement := req.Placement; placement != "" {
request.Placement = &placement
}
if isPublishStr := req.IsPublish; isPublishStr != "" {
isPublish, err := strconv.ParseBool(isPublishStr)
if err == nil {
request.IsPublish = &isPublish
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
return request
}

View File

@ -0,0 +1,17 @@
package response
import "time"
type AdvertisementResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
RedirectLink string `json:"redirectLink"`
ContentFileUrl string `json:"contentFileUrl"`
Placement string `json:"placement"`
StatusId int `json:"statusId"`
IsPublish bool `json:"isPublish"`
IsActive bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,264 @@
package service
import (
"context"
"fmt"
"io"
"log"
"math/rand"
"mime"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/advertisement/mapper"
"narasi-ahli-be/app/module/advertisement/repository"
"narasi-ahli-be/app/module/advertisement/request"
"narasi-ahli-be/app/module/advertisement/response"
usersRepository "narasi-ahli-be/app/module/users/repository"
config "narasi-ahli-be/config/config"
minioStorage "narasi-ahli-be/config/config"
"narasi-ahli-be/utils/paginator"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
// AdvertisementService
type advertisementService struct {
Repo repository.AdvertisementRepository
UsersRepo usersRepository.UsersRepository
Log zerolog.Logger
Cfg *config.Config
MinioStorage *minioStorage.MinioStorage
}
// AdvertisementService define interface of IAdvertisementService
type AdvertisementService interface {
All(req request.AdvertisementQueryRequest) (advertisement []*response.AdvertisementResponse, paging paginator.Pagination, err error)
Show(id uint) (advertisement *response.AdvertisementResponse, err error)
Save(req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, err error)
Upload(c *fiber.Ctx, id uint) (err error)
Update(id uint, req request.AdvertisementUpdateRequest) (err error)
UpdatePublish(id uint, isPublish bool) (err error)
Delete(id uint) error
Viewer(c *fiber.Ctx) (err error)
}
// NewAdvertisementService init AdvertisementService
func NewAdvertisementService(repo repository.AdvertisementRepository, minioStorage *minioStorage.MinioStorage, usersRepo usersRepository.UsersRepository, log zerolog.Logger, cfg *config.Config) AdvertisementService {
return &advertisementService{
Repo: repo,
UsersRepo: usersRepo,
MinioStorage: minioStorage,
Log: log,
Cfg: cfg,
}
}
// All implement interface of AdvertisementService
func (_i *advertisementService) All(req request.AdvertisementQueryRequest) (advertisements []*response.AdvertisementResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
host := _i.Cfg.App.Domain
for _, result := range results {
advertisements = append(advertisements, mapper.AdvertisementResponseMapper(result, host))
}
return
}
func (_i *advertisementService) Show(id uint) (advertisement *response.AdvertisementResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.AdvertisementResponseMapper(result, host), nil
}
func (_i *advertisementService) Save(req request.AdvertisementCreateRequest) (advertisement *entity.Advertisement, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
return _i.Repo.Create(newReq)
}
func (_i *advertisementService) Upload(c *fiber.Ctx, id uint) (err error) {
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
form, err := c.MultipartForm()
if err != nil {
return err
}
//filess := form.File["files"]
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
result, err := _i.Repo.FindOne(id)
if result == nil {
// Return status 400. Id not found.
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
for _, files := range form.File {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: top").
Interface("files", files).Msg("")
for _, fileHeader := range files {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop").
Interface("data", fileHeader).Msg("")
src, err := fileHeader.Open()
if err != nil {
return err
}
defer src.Close()
filename := filepath.Base(fileHeader.Filename)
filename = strings.ReplaceAll(filename, " ", "")
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(fileHeader.Filename)[1:]
now := time.Now()
rand.New(rand.NewSource(now.UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
objectName := fmt.Sprintf("advertisement/upload/%d/%d/%s", now.Year(), now.Month(), newFilename)
result.ContentFileName = &newFilename
result.ContentFilePath = &objectName
err = _i.Repo.Update(id, result)
if err != nil {
return err
}
// Upload file ke MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, fileHeader.Size, minio.PutObjectOptions{})
if err != nil {
return err
}
}
}
return
}
func (_i *advertisementService) Update(id uint, req request.AdvertisementUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
return _i.Repo.Update(id, newReq)
}
func (_i *advertisementService) UpdatePublish(id uint, isPublish bool) (err error) {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
Interface("ids", id).Msg("")
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
Interface("isPublish", isPublish).Msg("")
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.IsPublish = isPublish
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "UpdatePublish").
Interface("result", result).Msg("")
return _i.Repo.Update(id, result)
}
func (_i *advertisementService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
result.IsActive = false
return _i.Repo.Update(id, result)
}
func (_i *advertisementService) Viewer(c *fiber.Ctx) (err error) {
filename := c.Params("filename")
result, err := _i.Repo.FindByFilename(filename)
if err != nil {
return err
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := *result.ContentFilePath
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
log.Fatalln(err)
}
defer fileContent.Close()
// Tentukan Content-Type berdasarkan ekstensi file
contentType := mime.TypeByExtension("." + getFileExtension(objectName))
if contentType == "" {
contentType = "application/octet-stream" // fallback jika tidak ada tipe MIME yang cocok
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -0,0 +1,27 @@
package ai_chat
import (
"narasi-ahli-be/app/module/ai_chat/controller"
"narasi-ahli-be/app/module/ai_chat/repository"
"narasi-ahli-be/app/module/ai_chat/service"
usersRepository "narasi-ahli-be/app/module/users/repository"
"github.com/rs/zerolog"
"go.uber.org/fx"
)
var Module = fx.Options(
fx.Provide(
repository.NewAIChatRepository,
service.NewAIChatService,
controller.NewAIChatController,
),
fx.Invoke(func(
aiChatController controller.AIChatController,
usersRepo usersRepository.UsersRepository,
log zerolog.Logger,
) {
log.Info().Msg("AI Chat module initialized successfully")
}),
)

View File

@ -0,0 +1,459 @@
package controller
import (
"narasi-ahli-be/app/module/ai_chat/request"
"narasi-ahli-be/app/module/ai_chat/service"
"narasi-ahli-be/utils/paginator"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
)
type aiChatController struct {
aiChatService service.AIChatService
Log zerolog.Logger
}
type AIChatController interface {
// AI Chat Sessions
GetUserSessions(c *fiber.Ctx) error
GetSession(c *fiber.Ctx) error
CreateSession(c *fiber.Ctx) error
UpdateSession(c *fiber.Ctx) error
DeleteSession(c *fiber.Ctx) error
// AI Chat Messages
GetSessionMessages(c *fiber.Ctx) error
SendMessage(c *fiber.Ctx) error
UpdateMessage(c *fiber.Ctx) error
DeleteMessage(c *fiber.Ctx) error
// AI Chat Logs
GetUserLogs(c *fiber.Ctx) error
GetLog(c *fiber.Ctx) error
}
func NewAIChatController(aiChatService service.AIChatService, log zerolog.Logger) AIChatController {
return &aiChatController{
aiChatService: aiChatService,
Log: log,
}
}
// Get User Sessions
// @Summary Get user AI chat sessions
// @Description API for getting all AI chat sessions for authenticated user
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param req query request.AIChatSessionsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions [get]
func (_i *aiChatController) GetUserSessions(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
reqContext := request.AIChatSessionsQueryRequestContext{
Status: c.Query("status"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
sessionsData, paging, err := _i.aiChatService.GetUserSessions(authHeader, req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat sessions successfully retrieved"},
Data: sessionsData,
Meta: paging,
})
}
// Get Session
// @Summary Get one AI chat session
// @Description API for getting one AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param id path int true "Session ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{id} [get]
func (_i *aiChatController) GetSession(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
sessionData, err := _i.aiChatService.GetSession(authHeader, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully retrieved"},
Data: sessionData,
})
}
// Create Session
// @Summary Create AI chat session
// @Description API for create AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.AIChatSessionsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions [post]
func (_i *aiChatController) CreateSession(c *fiber.Ctx) error {
req := new(request.AIChatSessionsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
dataResult, err := _i.aiChatService.CreateSession(authHeader, *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully created"},
Data: dataResult,
})
}
// Update Session
// @Summary Update AI chat session
// @Description API for update AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Session ID"
// @Param payload body request.AIChatSessionsUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{id} [put]
func (_i *aiChatController) UpdateSession(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.AIChatSessionsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.UpdateSession(authHeader, uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully updated"},
})
}
// Delete Session
// @Summary Delete AI chat session
// @Description API for delete AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "Session ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{id} [delete]
func (_i *aiChatController) DeleteSession(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.DeleteSession(authHeader, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat session successfully deleted"},
})
}
// Get Session Messages
// @Summary Get AI chat session messages
// @Description API for getting all messages in an AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param sessionId path int true "Session ID"
// @Param req query request.AIChatMessagesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{sessionId}/messages [get]
func (_i *aiChatController) GetSessionMessages(c *fiber.Ctx) error {
sessionId, err := strconv.ParseUint(c.Params("sessionId"), 10, 0)
if err != nil {
return err
}
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
reqContext := request.AIChatMessagesQueryRequestContext{
SessionID: c.Query("sessionId"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
messagesData, paging, err := _i.aiChatService.GetSessionMessages(authHeader, uint(sessionId), req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat messages successfully retrieved"},
Data: messagesData,
Meta: paging,
})
}
// Send Message
// @Summary Send message to AI chat session
// @Description API for sending a message to an AI chat session
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param sessionId path int true "Session ID"
// @Param payload body request.AIChatMessagesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{sessionId}/messages [post]
func (_i *aiChatController) SendMessage(c *fiber.Ctx) error {
sessionId, err := strconv.ParseUint(c.Params("sessionId"), 10, 0)
if err != nil {
return err
}
req := new(request.AIChatMessagesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
dataResult, err := _i.aiChatService.SendMessage(authHeader, uint(sessionId), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Message successfully sent"},
Data: dataResult,
})
}
// Update Message
// @Summary Update AI chat message
// @Description API for update AI chat message
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param sessionId path int true "Session ID"
// @Param messageId path int true "Message ID"
// @Param payload body request.AIChatMessagesUpdateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{sessionId}/messages/{messageId} [put]
func (_i *aiChatController) UpdateMessage(c *fiber.Ctx) error {
sessionId, err := strconv.ParseUint(c.Params("sessionId"), 10, 0)
if err != nil {
return err
}
messageId, err := strconv.ParseUint(c.Params("messageId"), 10, 0)
if err != nil {
return err
}
req := new(request.AIChatMessagesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.UpdateMessage(authHeader, uint(sessionId), uint(messageId), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat message successfully updated"},
})
}
// Delete Message
// @Summary Delete AI chat message
// @Description API for delete AI chat message
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param sessionId path int true "Session ID"
// @Param messageId path int true "Message ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/sessions/{sessionId}/messages/{messageId} [delete]
func (_i *aiChatController) DeleteMessage(c *fiber.Ctx) error {
sessionId, err := strconv.ParseUint(c.Params("sessionId"), 10, 0)
if err != nil {
return err
}
messageId, err := strconv.ParseUint(c.Params("messageId"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
err = _i.aiChatService.DeleteMessage(authHeader, uint(sessionId), uint(messageId))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat message successfully deleted"},
})
}
// Get User Logs
// @Summary Get user AI chat logs
// @Description API for getting all AI chat logs for authenticated user
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param req query request.AIChatLogsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/logs [get]
func (_i *aiChatController) GetUserLogs(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
reqContext := request.AIChatLogsQueryRequestContext{
LogType: c.Query("logType"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
logsData, paging, err := _i.aiChatService.GetUserLogs(authHeader, req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat logs successfully retrieved"},
Data: logsData,
Meta: paging,
})
}
// Get Log
// @Summary Get one AI chat log
// @Description API for getting one AI chat log
// @Tags AI Chat
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param id path int true "Log ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /ai-chat/logs/{id} [get]
func (_i *aiChatController) GetLog(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
authHeader := c.Get("Authorization")
logData, err := _i.aiChatService.GetLog(authHeader, uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"AI chat log successfully retrieved"},
Data: logData,
})
}

View File

@ -0,0 +1,70 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/ai_chat/response"
)
func AIChatSessionsResponseMapper(session *entity.AIChatSessions) *response.AIChatSessionsResponse {
result := &response.AIChatSessionsResponse{
ID: session.ID,
AISessionID: session.AISessionID,
UserID: session.UserID,
AgentID: session.AgentID,
Title: session.Title,
MessageCount: session.MessageCount,
Status: session.Status,
CreatedAt: session.CreatedAt,
UpdatedAt: session.UpdatedAt,
}
if session.User != nil {
result.User = &response.UserBasicInfo{
ID: session.User.ID,
Username: session.User.Username,
Fullname: session.User.Fullname,
Email: session.User.Email,
}
}
return result
}
func AIChatMessagesResponseMapper(message *entity.AIChatMessages) *response.AIChatMessagesResponse {
return &response.AIChatMessagesResponse{
ID: message.ID,
SessionID: message.SessionID,
MessageType: message.MessageType,
Content: message.Content,
CreatedAt: message.CreatedAt,
}
}
func AIChatSessionWithMessagesResponseMapper(session *entity.AIChatSessions, messages []*entity.AIChatMessages) *response.AIChatSessionWithMessagesResponse {
sessionResponse := AIChatSessionsResponseMapper(session)
var messagesResponse []*response.AIChatMessagesResponse
for _, message := range messages {
messagesResponse = append(messagesResponse, AIChatMessagesResponseMapper(message))
}
return &response.AIChatSessionWithMessagesResponse{
Session: sessionResponse,
Messages: messagesResponse,
}
}
func AIChatLogsResponseMapper(log *entity.AIChatLogs) *response.AIChatLogsResponse {
result := &response.AIChatLogsResponse{
ID: log.ID,
UserID: log.UserID,
SessionID: log.SessionID,
StartDate: log.StartDate,
EndDate: log.EndDate,
TotalDuration: log.TotalDuration,
CreatedAt: log.CreatedAt,
UpdatedAt: log.UpdatedAt,
}
return result
}

View File

@ -0,0 +1,198 @@
package repository
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/ai_chat/request"
"narasi-ahli-be/utils/paginator"
)
type aiChatRepository struct {
DB *database.Database
}
type AIChatRepository interface {
// AI Chat Sessions
GetUserSessions(userId uint, req request.AIChatSessionsQueryRequest) (sessions []*entity.AIChatSessions, paging paginator.Pagination, err error)
FindSessionByUserAndId(userId uint, sessionId uint) (session *entity.AIChatSessions, err error)
FindSessionByAISessionId(aiSessionId string) (session *entity.AIChatSessions, err error)
CreateSession(session *entity.AIChatSessions) (result *entity.AIChatSessions, err error)
UpdateSession(userId uint, sessionId uint, session *entity.AIChatSessions) (err error)
DeleteSession(userId uint, sessionId uint) (err error)
IncrementMessageCount(sessionId uint) (err error)
// AI Chat Messages
GetSessionMessages(sessionId uint, req request.AIChatMessagesQueryRequest) (messages []*entity.AIChatMessages, paging paginator.Pagination, err error)
CreateMessage(message *entity.AIChatMessages) (result *entity.AIChatMessages, err error)
UpdateMessage(messageId uint, message *entity.AIChatMessages) (err error)
DeleteMessage(messageId uint) (err error)
GetLastMessage(sessionId uint) (message *entity.AIChatMessages, err error)
// AI Chat Logs
GetUserLogs(userId uint, req request.AIChatLogsQueryRequest) (logs []*entity.AIChatLogs, paging paginator.Pagination, err error)
FindLogByUserAndId(userId uint, logId uint) (log *entity.AIChatLogs, err error)
}
func NewAIChatRepository(db *database.Database) AIChatRepository {
return &aiChatRepository{
DB: db,
}
}
// AI Chat Sessions methods
func (_i *aiChatRepository) GetUserSessions(userId uint, req request.AIChatSessionsQueryRequest) (sessions []*entity.AIChatSessions, paging paginator.Pagination, err error) {
query := _i.DB.DB.Where("user_id = ?", userId)
// Apply filters
if req.Status != nil {
query = query.Where("status = ?", *req.Status)
}
// Include user relationship
query = query.Preload("User")
// Order by updated_at desc (most recent first)
query = query.Order("updated_at DESC")
// Apply pagination
var count int64
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&sessions).Error
paging = *req.Pagination
return
}
func (_i *aiChatRepository) FindSessionByUserAndId(userId uint, sessionId uint) (session *entity.AIChatSessions, err error) {
err = _i.DB.DB.Where("user_id = ? AND id = ?", userId, sessionId).Preload("User").First(&session).Error
return
}
func (_i *aiChatRepository) FindSessionByAISessionId(aiSessionId string) (session *entity.AIChatSessions, err error) {
err = _i.DB.DB.Where("ai_session_id = ?", aiSessionId).Preload("User").First(&session).Error
return
}
func (_i *aiChatRepository) CreateSession(session *entity.AIChatSessions) (result *entity.AIChatSessions, err error) {
err = _i.DB.DB.Create(session).Error
if err != nil {
return nil, err
}
// Reload with relationships
err = _i.DB.DB.Preload("User").First(&result, session.ID).Error
return
}
func (_i *aiChatRepository) UpdateSession(userId uint, sessionId uint, session *entity.AIChatSessions) (err error) {
err = _i.DB.DB.Where("user_id = ? AND id = ?", userId, sessionId).Updates(session).Error
return
}
func (_i *aiChatRepository) DeleteSession(userId uint, sessionId uint) (err error) {
err = _i.DB.DB.Where("user_id = ? AND id = ?", userId, sessionId).Delete(&entity.AIChatSessions{}).Error
return
}
func (_i *aiChatRepository) IncrementMessageCount(sessionId uint) (err error) {
err = _i.DB.DB.Exec("UPDATE ai_chat_sessions SET message_count = message_count + 1 WHERE id = ?", sessionId).Error
return
}
// AI Chat Messages methods
func (_i *aiChatRepository) GetSessionMessages(sessionId uint, req request.AIChatMessagesQueryRequest) (messages []*entity.AIChatMessages, paging paginator.Pagination, err error) {
query := _i.DB.DB.Where("session_id = ?", sessionId)
// Order by created_at asc (oldest first for chat)
query = query.Order("created_at ASC")
// Apply pagination
var count int64
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&messages).Error
paging = *req.Pagination
return
}
func (_i *aiChatRepository) CreateMessage(message *entity.AIChatMessages) (result *entity.AIChatMessages, err error) {
err = _i.DB.DB.Create(message).Error
if err != nil {
return nil, err
}
// Reload
err = _i.DB.DB.First(&result, message.ID).Error
return
}
func (_i *aiChatRepository) UpdateMessage(messageId uint, message *entity.AIChatMessages) (err error) {
err = _i.DB.DB.Model(&entity.AIChatMessages{}).Where("id = ?", messageId).Updates(message).Error
return
}
func (_i *aiChatRepository) DeleteMessage(messageId uint) (err error) {
err = _i.DB.DB.Where("id = ?", messageId).Delete(&entity.AIChatMessages{}).Error
return
}
// AI Chat Logs methods
func (_i *aiChatRepository) GetUserLogs(userId uint, req request.AIChatLogsQueryRequest) (logs []*entity.AIChatLogs, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Where("user_id = ?", userId)
// Note: AIChatLogs entity doesn't have LogType field, so we skip this filter
// if req.LogType != nil && *req.LogType != "" {
// query = query.Where("log_type = ?", *req.LogType)
// }
// Count total records
err = query.Model(&entity.AIChatLogs{}).Count(&count).Error
if err != nil {
return
}
// Apply pagination
if req.Pagination != nil {
query = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit)
}
// Order by created_at desc (newest first)
query = query.Order("created_at DESC")
err = query.Find(&logs).Error
if err != nil {
return
}
// Set pagination info
if req.Pagination != nil {
paging = *req.Pagination
paging.Count = count
paging.TotalPage = int((count + int64(req.Pagination.Limit) - 1) / int64(req.Pagination.Limit))
}
return
}
func (_i *aiChatRepository) FindLogByUserAndId(userId uint, logId uint) (log *entity.AIChatLogs, err error) {
query := _i.DB.DB.Where("user_id = ? AND id = ?", userId, logId)
if err := query.First(&log).Error; err != nil {
return nil, err
}
return
}
func (_i *aiChatRepository) GetLastMessage(sessionId uint) (message *entity.AIChatMessages, err error) {
err = _i.DB.DB.Where("session_id = ?", sessionId).Order("created_at DESC").First(&message).Error
return
}

View File

@ -0,0 +1,118 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
)
// AI Chat Sessions Request DTOs
type AIChatSessionsQueryRequest struct {
Status *string `json:"status"`
Pagination *paginator.Pagination `json:"pagination"`
}
type AIChatSessionsCreateRequest struct {
Title string `json:"title" validate:"required,min=2,max=255"`
AgentID *string `json:"agentId"`
}
func (req AIChatSessionsCreateRequest) ToEntity() *entity.AIChatSessions {
return &entity.AIChatSessions{
AISessionID: "", // Will be generated in service layer
AgentID: req.AgentID,
Title: req.Title,
MessageCount: 0,
Status: "active",
}
}
type AIChatSessionsUpdateRequest struct {
Title string `json:"title" validate:"required,min=2,max=255"`
Status string `json:"status" validate:"required,oneof=active archived deleted"`
}
func (req AIChatSessionsUpdateRequest) ToEntity() *entity.AIChatSessions {
return &entity.AIChatSessions{
Title: req.Title,
Status: req.Status,
}
}
// AI Chat Messages Request DTOs
type AIChatMessagesQueryRequest struct {
SessionID uint `json:"sessionId" validate:"required"`
Pagination *paginator.Pagination `json:"pagination"`
}
type AIChatMessagesCreateRequest struct {
SessionID uint `json:"sessionId" validate:"required"`
MessageType string `json:"messageType" validate:"required,oneof=user assistant"`
Content string `json:"content" validate:"required,min=1"`
}
func (req AIChatMessagesCreateRequest) ToEntity() *entity.AIChatMessages {
return &entity.AIChatMessages{
SessionID: req.SessionID,
MessageType: req.MessageType,
Content: req.Content,
}
}
type AIChatMessagesUpdateRequest struct {
Content string `json:"content" validate:"required,min=1"`
}
func (req AIChatMessagesUpdateRequest) ToEntity() *entity.AIChatMessages {
return &entity.AIChatMessages{
Content: req.Content,
}
}
type AIChatSessionsQueryRequestContext struct {
Status string `json:"status"`
}
func (req AIChatSessionsQueryRequestContext) ToParamRequest() AIChatSessionsQueryRequest {
var request AIChatSessionsQueryRequest
if status := req.Status; status != "" {
request.Status = &status
}
return request
}
type AIChatMessagesQueryRequestContext struct {
SessionID string `json:"sessionId"`
}
func (req AIChatMessagesQueryRequestContext) ToParamRequest() AIChatMessagesQueryRequest {
var request AIChatMessagesQueryRequest
if sessionIDStr := req.SessionID; sessionIDStr != "" {
// Parse session ID from string to uint
// This will be handled in the controller
}
return request
}
// AI Chat Logs Request DTOs
type AIChatLogsQueryRequest struct {
LogType *string `json:"logType"`
Pagination *paginator.Pagination `json:"pagination"`
}
type AIChatLogsQueryRequestContext struct {
LogType string `json:"logType"`
}
func (req AIChatLogsQueryRequestContext) ToParamRequest() AIChatLogsQueryRequest {
var request AIChatLogsQueryRequest
if logType := req.LogType; logType != "" {
request.LogType = &logType
}
return request
}

View File

@ -0,0 +1,62 @@
package response
import (
"time"
)
// AI Chat Sessions Response DTOs
type AIChatSessionsResponse struct {
ID uint `json:"id"`
AISessionID string `json:"aiSessionId"`
UserID uint `json:"userId"`
AgentID *string `json:"agentId"`
Title string `json:"title"`
MessageCount int `json:"messageCount"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Nested user info
User *UserBasicInfo `json:"user,omitempty"`
// Last message preview
LastMessage *AIChatMessagesResponse `json:"lastMessage,omitempty"`
}
// AI Chat Messages Response DTOs
type AIChatMessagesResponse struct {
ID uint `json:"id"`
SessionID uint `json:"sessionId"`
MessageType string `json:"messageType"`
Content string `json:"content"`
CreatedAt time.Time `json:"createdAt"`
}
type UserBasicInfo struct {
ID uint `json:"id"`
Username string `json:"username"`
Fullname string `json:"fullname"`
Email string `json:"email"`
}
// Session with messages
type AIChatSessionWithMessagesResponse struct {
Session *AIChatSessionsResponse `json:"session"`
Messages []*AIChatMessagesResponse `json:"messages"`
Pagination interface{} `json:"pagination"`
}
// AI Chat Logs Response DTOs
type AIChatLogsResponse struct {
ID uint `json:"id"`
UserID uint `json:"userId"`
SessionID uint `json:"sessionId"`
StartDate time.Time `json:"startDate"`
EndDate *time.Time `json:"endDate"`
TotalDuration int64 `json:"totalDuration"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// Nested user info
User *UserBasicInfo `json:"user,omitempty"`
}

View File

@ -0,0 +1,246 @@
package service
import (
"errors"
"fmt"
"narasi-ahli-be/app/module/ai_chat/mapper"
"narasi-ahli-be/app/module/ai_chat/repository"
"narasi-ahli-be/app/module/ai_chat/request"
"narasi-ahli-be/app/module/ai_chat/response"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"github.com/rs/zerolog"
)
type aiChatService struct {
Repo repository.AIChatRepository
UsersRepo usersRepository.UsersRepository
Log zerolog.Logger
}
type AIChatService interface {
// Sessions
GetUserSessions(authToken string, req request.AIChatSessionsQueryRequest) (sessions []*response.AIChatSessionsResponse, paging paginator.Pagination, err error)
GetSession(authToken string, sessionId uint) (session *response.AIChatSessionsResponse, err error)
CreateSession(authToken string, req request.AIChatSessionsCreateRequest) (session *response.AIChatSessionsResponse, err error)
UpdateSession(authToken string, sessionId uint, req request.AIChatSessionsUpdateRequest) (err error)
DeleteSession(authToken string, sessionId uint) error
ArchiveSession(authToken string, sessionId uint) error
// Messages
GetSessionMessages(authToken string, sessionId uint, req request.AIChatMessagesQueryRequest) (messages []*response.AIChatMessagesResponse, paging paginator.Pagination, err error)
SendMessage(authToken string, sessionId uint, req request.AIChatMessagesCreateRequest) (message *response.AIChatMessagesResponse, err error)
UpdateMessage(authToken string, sessionId uint, messageId uint, req request.AIChatMessagesUpdateRequest) (err error)
DeleteMessage(authToken string, sessionId uint, messageId uint) error
// Logs
GetUserLogs(authToken string, req request.AIChatLogsQueryRequest) (logs []*response.AIChatLogsResponse, paging paginator.Pagination, err error)
GetLog(authToken string, logId uint) (log *response.AIChatLogsResponse, err error)
}
func NewAIChatService(repo repository.AIChatRepository, usersRepo usersRepository.UsersRepository, log zerolog.Logger) AIChatService {
return &aiChatService{
Repo: repo,
UsersRepo: usersRepo,
Log: log,
}
}
// Sessions methods
func (_i *aiChatService) GetUserSessions(authToken string, req request.AIChatSessionsQueryRequest) (sessions []*response.AIChatSessionsResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
results, paging, err := _i.Repo.GetUserSessions(userInfo.ID, req)
if err != nil {
return
}
for _, result := range results {
sessions = append(sessions, mapper.AIChatSessionsResponseMapper(result))
}
return
}
func (_i *aiChatService) GetSession(authToken string, sessionId uint) (session *response.AIChatSessionsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
result, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return nil, err
}
return mapper.AIChatSessionsResponseMapper(result), nil
}
func (_i *aiChatService) CreateSession(authToken string, req request.AIChatSessionsCreateRequest) (session *response.AIChatSessionsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
entity := req.ToEntity()
entity.UserID = userInfo.ID
// Generate unique AI session ID
entity.AISessionID = fmt.Sprintf("ai_session_%d_%d", userInfo.ID, entity.ID)
result, err := _i.Repo.CreateSession(entity)
if err != nil {
return nil, err
}
return mapper.AIChatSessionsResponseMapper(result), nil
}
func (_i *aiChatService) UpdateSession(authToken string, sessionId uint, req request.AIChatSessionsUpdateRequest) (err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Interface("data", req).Msg("Updating AI chat session")
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return err
}
if existing == nil {
return errors.New("AI chat session not found")
}
entity := req.ToEntity()
return _i.Repo.UpdateSession(userInfo.ID, sessionId, entity)
}
func (_i *aiChatService) DeleteSession(authToken string, sessionId uint) error {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Uint("userId", userInfo.ID).Uint("sessionId", sessionId).Msg("Deleting AI chat session")
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return err
}
if existing == nil {
return errors.New("AI chat session not found")
}
return _i.Repo.DeleteSession(userInfo.ID, sessionId)
}
func (_i *aiChatService) ArchiveSession(authToken string, sessionId uint) error {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Uint("userId", userInfo.ID).Uint("sessionId", sessionId).Msg("Archiving AI chat session")
// Check if session exists and belongs to user
existing, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return err
}
if existing == nil {
return errors.New("AI chat session not found")
}
// Update status to archived
existing.Status = "archived"
return _i.Repo.UpdateSession(userInfo.ID, sessionId, existing)
}
// Messages methods
func (_i *aiChatService) GetSessionMessages(authToken string, sessionId uint, req request.AIChatMessagesQueryRequest) (messages []*response.AIChatMessagesResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Verify session belongs to user
_, err = _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return nil, paginator.Pagination{}, err
}
results, paging, err := _i.Repo.GetSessionMessages(sessionId, req)
if err != nil {
return
}
for _, result := range results {
messages = append(messages, mapper.AIChatMessagesResponseMapper(result))
}
return
}
func (_i *aiChatService) SendMessage(authToken string, sessionId uint, req request.AIChatMessagesCreateRequest) (message *response.AIChatMessagesResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
_i.Log.Info().Interface("data", req).Msg("Sending AI chat message")
// Verify session belongs to user
_, err = _i.Repo.FindSessionByUserAndId(userInfo.ID, req.SessionID)
if err != nil {
return nil, err
}
entity := req.ToEntity()
result, err := _i.Repo.CreateMessage(entity)
if err != nil {
return nil, err
}
// Increment message count in session
err = _i.Repo.IncrementMessageCount(req.SessionID)
if err != nil {
_i.Log.Error().Err(err).Msg("Failed to increment message count")
}
return mapper.AIChatMessagesResponseMapper(result), nil
}
// Update Message
func (_i *aiChatService) UpdateMessage(authToken string, sessionId uint, messageId uint, req request.AIChatMessagesUpdateRequest) (err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Verify session belongs to user
_, err = _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return err
}
entity := req.ToEntity()
return _i.Repo.UpdateMessage(messageId, entity)
}
// Delete Message
func (_i *aiChatService) DeleteMessage(authToken string, sessionId uint, messageId uint) error {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
// Verify session belongs to user
_, err := _i.Repo.FindSessionByUserAndId(userInfo.ID, sessionId)
if err != nil {
return err
}
return _i.Repo.DeleteMessage(messageId)
}
// Logs methods
func (_i *aiChatService) GetUserLogs(authToken string, req request.AIChatLogsQueryRequest) (logs []*response.AIChatLogsResponse, paging paginator.Pagination, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
results, paging, err := _i.Repo.GetUserLogs(userInfo.ID, req)
if err != nil {
return
}
for _, result := range results {
logs = append(logs, mapper.AIChatLogsResponseMapper(result))
}
return
}
func (_i *aiChatService) GetLog(authToken string, logId uint) (log *response.AIChatLogsResponse, err error) {
userInfo := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
result, err := _i.Repo.FindLogByUserAndId(userInfo.ID, logId)
if err != nil {
return nil, err
}
return mapper.AIChatLogsResponseMapper(result), nil
}

View File

@ -0,0 +1,54 @@
package article_approvals
import (
"narasi-ahli-be/app/module/article_approvals/controller"
"narasi-ahli-be/app/module/article_approvals/repository"
"narasi-ahli-be/app/module/article_approvals/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleApprovalsRouter
type ArticleApprovalsRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleApprovals module
var NewArticleApprovalsModule = fx.Options(
// register repository of ArticleApprovals module
fx.Provide(repository.NewArticleApprovalsRepository),
// register service of ArticleApprovals module
fx.Provide(service.NewArticleApprovalsService),
// register controller of ArticleApprovals module
fx.Provide(controller.NewController),
// register router of ArticleApprovals module
fx.Provide(NewArticleApprovalsRouter),
)
// init ArticleApprovalsRouter
func NewArticleApprovalsRouter(fiber *fiber.App, controller *controller.Controller) *ArticleApprovalsRouter {
return &ArticleApprovalsRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleApprovals module
func (_i *ArticleApprovalsRouter) RegisterArticleApprovalsRoutes() {
// define controllers
articleApprovalsController := _i.Controller.ArticleApprovals
// define routes
_i.App.Route("/article-approvals", func(router fiber.Router) {
router.Get("/", articleApprovalsController.All)
router.Get("/:id", articleApprovalsController.Show)
router.Post("/", articleApprovalsController.Save)
router.Put("/:id", articleApprovalsController.Update)
router.Delete("/:id", articleApprovalsController.Delete)
})
}

View File

@ -0,0 +1,203 @@
package controller
import (
"narasi-ahli-be/app/module/article_approvals/request"
"narasi-ahli-be/app/module/article_approvals/service"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
)
type articleApprovalsController struct {
articleApprovalsService service.ArticleApprovalsService
Log zerolog.Logger
}
type ArticleApprovalsController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
func NewArticleApprovalsController(articleApprovalsService service.ArticleApprovalsService, log zerolog.Logger) ArticleApprovalsController {
return &articleApprovalsController{
articleApprovalsService: articleApprovalsService,
Log: log,
}
}
// All get all ArticleApprovals
// @Summary Get all ArticleApprovals
// @Description API for getting all ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param req query request.ArticleApprovalsQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals [get]
func (_i *articleApprovalsController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
reqContext := request.ArticleApprovalsQueryRequestContext{
ArticleId: c.Query("articleId"),
ApprovalBy: c.Query("approvalBy"),
StatusId: c.Query("statusId"),
Message: c.Query("message"),
ApprovalAtLevel: c.Query("approvalAtLevel"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
articleApprovalsData, paging, err := _i.articleApprovalsService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals list successfully retrieved"},
Data: articleApprovalsData,
Meta: paging,
})
}
// Show get one ArticleApprovals
// @Summary Get one ArticleApprovals
// @Description API for getting one ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param id path int true "ArticleApprovals ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals/{id} [get]
func (_i *articleApprovalsController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleApprovalsData, err := _i.articleApprovalsService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully retrieved"},
Data: articleApprovalsData,
})
}
// Save create ArticleApprovals
// @Summary Create ArticleApprovals
// @Description API for create ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ArticleApprovalsCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals [post]
func (_i *articleApprovalsController) Save(c *fiber.Ctx) error {
req := new(request.ArticleApprovalsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
// Get from context
dataResult, err := _i.articleApprovalsService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully created"},
Data: dataResult,
})
}
// Update update ArticleApprovals
// @Summary update ArticleApprovals
// @Description API for update ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticleApprovalsUpdateRequest true "Required payload"
// @Param id path int true "ArticleApprovals ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals/{id} [put]
func (_i *articleApprovalsController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleApprovalsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleApprovalsService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully updated"},
})
}
// Delete delete ArticleApprovals
// @Summary delete ArticleApprovals
// @Description API for delete ArticleApprovals
// @Tags ArticleApprovals
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleApprovals ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-approvals/{id} [delete]
func (_i *articleApprovalsController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleApprovalsService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleApprovals successfully deleted"},
})
}

View File

@ -0,0 +1,17 @@
package controller
import (
"narasi-ahli-be/app/module/article_approvals/service"
"github.com/rs/zerolog"
)
type Controller struct {
ArticleApprovals ArticleApprovalsController
}
func NewController(ArticleApprovalsService service.ArticleApprovalsService, log zerolog.Logger) *Controller {
return &Controller{
ArticleApprovals: NewArticleApprovalsController(ArticleApprovalsService, log),
}
}

View File

@ -0,0 +1,21 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
res "narasi-ahli-be/app/module/article_approvals/response"
)
func ArticleApprovalsResponseMapper(articleApprovalsReq *entity.ArticleApprovals) (articleApprovalsRes *res.ArticleApprovalsResponse) {
if articleApprovalsReq != nil {
articleApprovalsRes = &res.ArticleApprovalsResponse{
ID: articleApprovalsReq.ID,
ArticleId: articleApprovalsReq.ArticleId,
ApprovalBy: articleApprovalsReq.ApprovalBy,
StatusId: articleApprovalsReq.StatusId,
Message: articleApprovalsReq.Message,
ApprovalAtLevel: articleApprovalsReq.ApprovalAtLevel,
CreatedAt: articleApprovalsReq.CreatedAt,
}
}
return articleApprovalsRes
}

View File

@ -0,0 +1,105 @@
package repository
import (
"fmt"
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/article_approvals/request"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"strings"
"github.com/rs/zerolog"
)
type articleApprovalsRepository struct {
DB *database.Database
Log zerolog.Logger
}
// ArticleApprovalsRepository define interface of IArticleApprovalsRepository
type ArticleApprovalsRepository interface {
GetAll(req request.ArticleApprovalsQueryRequest) (articleApprovalss []*entity.ArticleApprovals, paging paginator.Pagination, err error)
FindOne(id uint) (articleApprovals *entity.ArticleApprovals, err error)
Create(articleApprovals *entity.ArticleApprovals) (articleApprovalsReturn *entity.ArticleApprovals, err error)
Update(id uint, articleApprovals *entity.ArticleApprovals) (err error)
Delete(id uint) (err error)
}
func NewArticleApprovalsRepository(db *database.Database, logger zerolog.Logger) ArticleApprovalsRepository {
return &articleApprovalsRepository{
DB: db,
Log: logger,
}
}
// implement interface of IArticleApprovalsRepository
func (_i *articleApprovalsRepository) GetAll(req request.ArticleApprovalsQueryRequest) (articleApprovalss []*entity.ArticleApprovals, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.ArticleApprovals{})
if req.ArticleId != nil {
query = query.Where("article_id = ?", req.ArticleId)
}
if req.ApprovalBy != nil {
query = query.Where("approval_by = ?", req.ApprovalBy)
}
if req.StatusId != nil {
query = query.Where("status_id = ?", req.StatusId)
}
if req.Message != nil && *req.Message != "" {
message := strings.ToLower(*req.Message)
query = query.Where("LOWER(message) LIKE ?", "%"+strings.ToLower(message)+"%")
}
if req.ApprovalAtLevel != nil {
query = query.Where("approval_at_level = ?", req.ApprovalAtLevel)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleApprovalss).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleApprovalsRepository) FindOne(id uint) (articleApprovals *entity.ArticleApprovals, err error) {
if err := _i.DB.DB.First(&articleApprovals, id).Error; err != nil {
return nil, err
}
return articleApprovals, nil
}
func (_i *articleApprovalsRepository) Create(articleApprovals *entity.ArticleApprovals) (articleApprovalsReturn *entity.ArticleApprovals, err error) {
result := _i.DB.DB.Create(articleApprovals)
return articleApprovals, result.Error
}
func (_i *articleApprovalsRepository) Update(id uint, articleApprovals *entity.ArticleApprovals) (err error) {
articleApprovalsMap, err := utilSvc.StructToMap(articleApprovals)
if err != nil {
return err
}
return _i.DB.DB.Model(&entity.ArticleApprovals{}).
Where(&entity.ArticleApprovals{ID: id}).
Updates(articleApprovalsMap).Error
}
func (_i *articleApprovalsRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&entity.ArticleApprovals{}, id).Error
}

View File

@ -0,0 +1,92 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"strconv"
)
type ArticleApprovalsGeneric interface {
ToEntity()
}
type ArticleApprovalsQueryRequest struct {
ArticleId *int `json:"articleId"`
ApprovalBy *int `json:"approvalBy"`
StatusId *int `json:"statusId"`
Message *string `json:"message"`
ApprovalAtLevel *int `json:"approvalAtLevel"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleApprovalsCreateRequest struct {
ArticleId uint `json:"articleId" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Message string `json:"message" validate:"required"`
}
func (req ArticleApprovalsCreateRequest) ToEntity() *entity.ArticleApprovals {
return &entity.ArticleApprovals{
ArticleId: req.ArticleId,
StatusId: req.StatusId,
Message: req.Message,
}
}
type ArticleApprovalsUpdateRequest struct {
ID uint `json:"id" validate:"required"`
ArticleId uint `json:"articleId" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Message string `json:"message" validate:"required"`
}
func (req ArticleApprovalsUpdateRequest) ToEntity() *entity.ArticleApprovals {
return &entity.ArticleApprovals{
ID: req.ID,
ArticleId: req.ArticleId,
StatusId: req.StatusId,
Message: req.Message,
}
}
type ArticleApprovalsQueryRequestContext struct {
ArticleId string `json:"articleId"`
ApprovalBy string `json:"approvalBy"`
StatusId string `json:"statusId"`
Message string `json:"message"`
ApprovalAtLevel string `json:"approvalAtLevel"`
}
func (req ArticleApprovalsQueryRequestContext) ToParamRequest() ArticleApprovalsQueryRequest {
var request ArticleApprovalsQueryRequest
if articleIdStr := req.ArticleId; articleIdStr != "" {
articleId, err := strconv.Atoi(articleIdStr)
if err == nil {
request.ArticleId = &articleId
}
}
if approvalByStr := req.ApprovalBy; approvalByStr != "" {
approvalBy, err := strconv.Atoi(approvalByStr)
if err == nil {
request.ApprovalBy = &approvalBy
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
if message := req.Message; message != "" {
request.Message = &message
}
if approvalAtLevelStr := req.ApprovalAtLevel; approvalAtLevelStr != "" {
approvalAtLevel, err := strconv.Atoi(approvalAtLevelStr)
if err == nil {
request.ApprovalAtLevel = &approvalAtLevel
}
}
return request
}

View File

@ -0,0 +1,13 @@
package response
import "time"
type ArticleApprovalsResponse struct {
ID uint `json:"id"`
ArticleId uint `json:"articleId"`
ApprovalBy uint `json:"approvalBy"`
StatusId int `json:"statusId"`
Message string `json:"message"`
ApprovalAtLevel *int `json:"approvalAtLevel"`
CreatedAt time.Time `json:"createdAt"`
}

View File

@ -0,0 +1,96 @@
package service
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/article_approvals/mapper"
"narasi-ahli-be/app/module/article_approvals/repository"
"narasi-ahli-be/app/module/article_approvals/request"
"narasi-ahli-be/app/module/article_approvals/response"
articlesService "narasi-ahli-be/app/module/articles/service"
usersRepository "narasi-ahli-be/app/module/users/repository"
"narasi-ahli-be/utils/paginator"
"github.com/rs/zerolog"
utilSvc "narasi-ahli-be/utils/service"
)
// ArticleApprovalsService
type articleApprovalsService struct {
Repo repository.ArticleApprovalsRepository
UsersRepo usersRepository.UsersRepository
ArticlesService articlesService.ArticlesService
Log zerolog.Logger
}
// ArticleApprovalsService define interface of IArticleApprovalsService
type ArticleApprovalsService interface {
All(req request.ArticleApprovalsQueryRequest) (articleApprovals []*response.ArticleApprovalsResponse, paging paginator.Pagination, err error)
Show(id uint) (articleApprovals *response.ArticleApprovalsResponse, err error)
Save(req request.ArticleApprovalsCreateRequest, authToken string) (articleApprovals *entity.ArticleApprovals, err error)
Update(id uint, req request.ArticleApprovalsUpdateRequest) (err error)
Delete(id uint) error
}
// NewArticleApprovalsService init ArticleApprovalsService
func NewArticleApprovalsService(repo repository.ArticleApprovalsRepository, log zerolog.Logger, usersRepo usersRepository.UsersRepository, articlesSvc articlesService.ArticlesService) ArticleApprovalsService {
return &articleApprovalsService{
Repo: repo,
Log: log,
UsersRepo: usersRepo,
ArticlesService: articlesSvc,
}
}
// All implement interface of ArticleApprovalsService
func (_i *articleApprovalsService) All(req request.ArticleApprovalsQueryRequest) (articleApprovalss []*response.ArticleApprovalsResponse, paging paginator.Pagination, err error) {
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
for _, result := range results {
articleApprovalss = append(articleApprovalss, mapper.ArticleApprovalsResponseMapper(result))
}
return
}
func (_i *articleApprovalsService) Show(id uint) (articleApprovals *response.ArticleApprovalsResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
return mapper.ArticleApprovalsResponseMapper(result), nil
}
func (_i *articleApprovalsService) Save(req request.ArticleApprovalsCreateRequest, authToken string) (articleApprovals *entity.ArticleApprovals, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
newReq.ApprovalBy = createdBy.ID
newReq.ApprovalAtLevel = &createdBy.UserLevel.LevelNumber
approvalByUserLevelId := createdBy.UserLevelId
approvalParentLevelId := createdBy.UserLevel.ParentLevelId
err = _i.ArticlesService.UpdateApproval(newReq.ArticleId, newReq.StatusId, int(approvalByUserLevelId), *newReq.ApprovalAtLevel, *approvalParentLevelId)
if err != nil {
return nil, err
}
return _i.Repo.Create(newReq)
}
func (_i *articleApprovalsService) Update(id uint, req request.ArticleApprovalsUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
return _i.Repo.Update(id, req.ToEntity())
}
func (_i *articleApprovalsService) Delete(id uint) error {
return _i.Repo.Delete(id)
}

View File

@ -0,0 +1,58 @@
package article_categories
import (
"narasi-ahli-be/app/module/article_categories/controller"
"narasi-ahli-be/app/module/article_categories/repository"
"narasi-ahli-be/app/module/article_categories/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleCategoriesRouter
type ArticleCategoriesRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleCategories module
var NewArticleCategoriesModule = fx.Options(
// register repository of ArticleCategories module
fx.Provide(repository.NewArticleCategoriesRepository),
// register service of ArticleCategories module
fx.Provide(service.NewArticleCategoriesService),
// register controller of ArticleCategories module
fx.Provide(controller.NewController),
// register router of ArticleCategories module
fx.Provide(NewArticleCategoriesRouter),
)
// init ArticleCategoriesRouter
func NewArticleCategoriesRouter(fiber *fiber.App, controller *controller.Controller) *ArticleCategoriesRouter {
return &ArticleCategoriesRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleCategories module
func (_i *ArticleCategoriesRouter) RegisterArticleCategoriesRoutes() {
// define controllers
articleCategoriesController := _i.Controller.ArticleCategories
// define routes
_i.App.Route("/article-categories", func(router fiber.Router) {
router.Get("/", articleCategoriesController.All)
router.Get("/:id", articleCategoriesController.Show)
router.Get("/old/:id", articleCategoriesController.ShowByOldId)
router.Get("/slug/:slug", articleCategoriesController.ShowBySlug)
router.Post("/", articleCategoriesController.Save)
router.Put("/:id", articleCategoriesController.Update)
router.Post("/thumbnail/:id", articleCategoriesController.SaveThumbnail)
router.Get("/thumbnail/viewer/:id", articleCategoriesController.Viewer)
router.Delete("/:id", articleCategoriesController.Delete)
})
}

View File

@ -0,0 +1,307 @@
package controller
import (
"narasi-ahli-be/app/module/article_categories/request"
"narasi-ahli-be/app/module/article_categories/service"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
)
type articleCategoriesController struct {
articleCategoriesService service.ArticleCategoriesService
}
type ArticleCategoriesController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
ShowByOldId(c *fiber.Ctx) error
ShowBySlug(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
SaveThumbnail(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
Viewer(c *fiber.Ctx) error
}
func NewArticleCategoriesController(articleCategoriesService service.ArticleCategoriesService) ArticleCategoriesController {
return &articleCategoriesController{
articleCategoriesService: articleCategoriesService,
}
}
// All ArticleCategories
// @Summary Get all ArticleCategories
// @Description API for getting all ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param req query request.ArticleCategoriesQueryRequest false "query parameters"
// @Param req query paginator.Pagination false "pagination parameters"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories [get]
func (_i *articleCategoriesController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
authToken := c.Get("Authorization")
reqContext := request.ArticleCategoriesQueryRequestContext{
Title: c.Query("title"),
Description: c.Query("description"),
ParentId: c.Query("parentId"),
IsPublish: c.Query("isPublish"),
StatusId: c.Query("statusId"),
}
req := reqContext.ToParamRequest()
req.Pagination = paginate
articleCategoriesData, paging, err := _i.articleCategoriesService.All(req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories list successfully retrieved"},
Data: articleCategoriesData,
Meta: paging,
})
}
// Show ArticleCategories
// @Summary Get one ArticleCategories
// @Description API for getting one ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/{id} [get]
func (_i *articleCategoriesController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleCategoriesData, err := _i.articleCategoriesService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully retrieved"},
Data: articleCategoriesData,
})
}
// ShowByOldId ArticleCategories
// @Summary Get one ArticleCategories
// @Description API for getting one ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param id path int true "ArticleCategories Old ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/old/{id} [get]
func (_i *articleCategoriesController) ShowByOldId(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleCategoriesData, err := _i.articleCategoriesService.ShowByOldId(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully retrieved"},
Data: articleCategoriesData,
})
}
// ShowBySlug ArticleCategories
// @Summary Get one ArticleCategories
// @Description API for getting one ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param slug path string true "ArticleCategories Slug"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/slug/{slug} [get]
func (_i *articleCategoriesController) ShowBySlug(c *fiber.Ctx) error {
slug := c.Params("slug")
articleCategoriesData, err := _i.articleCategoriesService.ShowBySlug(slug)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully retrieved"},
Data: articleCategoriesData,
})
}
// Save ArticleCategories
// @Summary Create ArticleCategories
// @Description API for create ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param Authorization header string false "Insert your access token" default(Bearer <Add access token here>)
// @Param payload body request.ArticleCategoriesCreateRequest true "Required payload"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories [post]
func (_i *articleCategoriesController) Save(c *fiber.Ctx) error {
req := new(request.ArticleCategoriesCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
authToken := c.Get("Authorization")
dataResult, err := _i.articleCategoriesService.Save(*req, authToken)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully created"},
Data: dataResult,
})
}
// SaveThumbnail ArticleCategories
// @Summary Upload ArticleCategories Thumbnail
// @Description API for Upload ArticleCategories Thumbnail
// @Tags Article Categories
// @Security Bearer
// @Produce json
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param files formData file true "Upload thumbnail"
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/thumbnail/{id} [post]
func (_i *articleCategoriesController) SaveThumbnail(c *fiber.Ctx) error {
err := _i.articleCategoriesService.SaveThumbnail(c)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"Thumbnail of ArticleCategories successfully created"},
})
}
// Update ArticleCategories
// @Summary update ArticleCategories
// @Description API for update ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param payload body request.ArticleCategoriesUpdateRequest true "Required payload"
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/{id} [put]
func (_i *articleCategoriesController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleCategoriesUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleCategoriesService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully updated"},
})
}
// Delete ArticleCategories
// @Summary delete ArticleCategories
// @Description API for delete ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/{id} [delete]
func (_i *articleCategoriesController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleCategoriesService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Success: true,
Messages: utilRes.Messages{"ArticleCategories successfully deleted"},
})
}
// Viewer ArticleCategories
// @Summary Viewer ArticleCategories
// @Description API for View Thumbnail of ArticleCategories
// @Tags Article Categories
// @Security Bearer
// @Param X-Client-Key header string false "Insert the X-Client-Key"
// @Param id path string true "ArticleCategories ID"
// @Success 200 {object} response.Response
// @Failure 400 {object} response.BadRequestError
// @Failure 401 {object} response.UnauthorizedError
// @Failure 500 {object} response.InternalServerError
// @Router /article-categories/thumbnail/viewer/{id} [get]
func (_i *articleCategoriesController) Viewer(c *fiber.Ctx) error {
return _i.articleCategoriesService.Viewer(c)
}

View File

@ -0,0 +1,13 @@
package controller
import "narasi-ahli-be/app/module/article_categories/service"
type Controller struct {
ArticleCategories ArticleCategoriesController
}
func NewController(ArticleCategoriesService service.ArticleCategoriesService) *Controller {
return &Controller{
ArticleCategories: NewArticleCategoriesController(ArticleCategoriesService),
}
}

View File

@ -0,0 +1,39 @@
package mapper
import (
"narasi-ahli-be/app/database/entity"
res "narasi-ahli-be/app/module/article_categories/response"
"strconv"
"strings"
)
func ArticleCategoriesResponseMapper(articleCategoriesReq *entity.ArticleCategories, host string) (articleCategoriesRes *res.ArticleCategoriesResponse) {
if articleCategoriesReq != nil {
articleCategoriesRes = &res.ArticleCategoriesResponse{
ID: articleCategoriesReq.ID,
Title: articleCategoriesReq.Title,
Description: articleCategoriesReq.Description,
Slug: articleCategoriesReq.Slug,
ThumbnailPath: articleCategoriesReq.ThumbnailPath,
ParentId: articleCategoriesReq.ParentId,
OldCategoryId: articleCategoriesReq.OldCategoryId,
CreatedById: articleCategoriesReq.CreatedById,
StatusId: articleCategoriesReq.StatusId,
IsPublish: articleCategoriesReq.IsPublish,
PublishedAt: articleCategoriesReq.PublishedAt,
IsActive: articleCategoriesReq.IsActive,
CreatedAt: articleCategoriesReq.CreatedAt,
UpdatedAt: articleCategoriesReq.UpdatedAt,
}
if articleCategoriesReq.Tags != nil {
tagsValue := *articleCategoriesReq.Tags
articleCategoriesRes.Tags = strings.Split(tagsValue, ",")
}
if articleCategoriesRes.ThumbnailPath != nil {
articleCategoriesRes.ThumbnailUrl = host + "/article-categories/thumbnail/viewer/" + strconv.Itoa(int(articleCategoriesReq.ID))
}
}
return articleCategoriesRes
}

View File

@ -0,0 +1,141 @@
package repository
import (
"fmt"
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/article_categories/request"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"strings"
"github.com/rs/zerolog"
)
type articleCategoriesRepository struct {
DB *database.Database
Log zerolog.Logger
}
// ArticleCategoriesRepository define interface of IArticleCategoriesRepository
type ArticleCategoriesRepository interface {
GetAll(req request.ArticleCategoriesQueryRequest) (articleCategoriess []*entity.ArticleCategories, paging paginator.Pagination, err error)
FindOne(id uint) (articleCategories *entity.ArticleCategories, err error)
FindOneByOldId(id uint) (articleCategories *entity.ArticleCategories, err error)
FindOneBySlug(slug string) (articleCategories *entity.ArticleCategories, err error)
Create(articleCategories *entity.ArticleCategories) (articleCategoriesReturn *entity.ArticleCategories, err error)
Update(id uint, articleCategories *entity.ArticleCategories) (err error)
Delete(id uint) (err error)
}
func NewArticleCategoriesRepository(db *database.Database, log zerolog.Logger) ArticleCategoriesRepository {
return &articleCategoriesRepository{
DB: db,
Log: log,
}
}
// implement interface of IArticleCategoriesRepository
func (_i *articleCategoriesRepository) GetAll(req request.ArticleCategoriesQueryRequest) (articleCategoriess []*entity.ArticleCategories, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&entity.ArticleCategories{})
if req.UserLevelId != nil {
query = _i.DB.DB.Model(&entity.ArticleCategories{}).
Joins("LEFT JOIN users ON article_categories.created_by_id = users.id").
Joins("LEFT JOIN user_levels ON users.user_level_id = user_levels.id").
Where("user_levels.id = ? or user_levels.parent_level_id = ?", *req.UserLevelId, *req.UserLevelId)
}
query = query.Where("article_categories.is_active = ?", true)
if req.Title != nil && *req.Title != "" {
title := strings.ToLower(*req.Title)
query = query.Where("LOWER(article_categories.title) LIKE ?", "%"+strings.ToLower(title)+"%")
}
if req.Description != nil && *req.Description != "" {
description := strings.ToLower(*req.Description)
query = query.Where("LOWER(article_categories.description) LIKE ?", "%"+strings.ToLower(description)+"%")
}
if req.ParentId != nil {
query = query.Where("article_categories.parent_id = ?", req.ParentId)
}
if req.IsPublish != nil {
query = query.Where("article_categories.is_publish = ?", req.IsPublish)
}
if req.StatusId != nil {
query = query.Where("article_categories.status_id = ?", req.StatusId)
}
query.Count(&count)
if req.Pagination.SortBy != "" {
direction := "ASC"
if req.Pagination.Sort == "desc" {
direction = "DESC"
}
query.Order(fmt.Sprintf("%s %s", req.Pagination.SortBy, direction))
}
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleCategoriess).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleCategoriesRepository) FindOne(id uint) (articleCategories *entity.ArticleCategories, err error) {
query := _i.DB.DB.Where("id = ?", id)
if err := query.First(&articleCategories).Error; err != nil {
return nil, err
}
return articleCategories, nil
}
func (_i *articleCategoriesRepository) FindOneByOldId(id uint) (articleCategories *entity.ArticleCategories, err error) {
query := _i.DB.DB.Where("old_category_id = ?", id)
if err := query.First(&articleCategories).Error; err != nil {
return nil, err
}
return articleCategories, nil
}
func (_i *articleCategoriesRepository) FindOneBySlug(slug string) (articleCategories *entity.ArticleCategories, err error) {
query := _i.DB.DB.Where("slug = ?", slug)
if err := query.First(&articleCategories).Error; err != nil {
return nil, err
}
return articleCategories, nil
}
func (_i *articleCategoriesRepository) Create(articleCategories *entity.ArticleCategories) (articleCategoriesReturn *entity.ArticleCategories, err error) {
result := _i.DB.DB.Create(articleCategories)
return articleCategories, result.Error
}
func (_i *articleCategoriesRepository) Update(id uint, articleCategories *entity.ArticleCategories) (err error) {
articleCategoriesMap, err := utilSvc.StructToMap(articleCategories)
if err != nil {
return err
}
query := _i.DB.DB.Model(&entity.ArticleCategories{}).Where(&entity.ArticleCategories{ID: id})
return query.Updates(articleCategoriesMap).Error
}
func (_i *articleCategoriesRepository) Delete(id uint) error {
query := _i.DB.DB.Model(&entity.ArticleCategories{}).Where("id = ?", id)
return query.Delete(&entity.ArticleCategories{}).Error
}

View File

@ -0,0 +1,128 @@
package request
import (
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/utils/paginator"
"strconv"
"time"
)
type ArticleCategoriesGeneric interface {
ToEntity()
}
type ArticleCategoriesQueryRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
UserLevelId *uint `json:"UserLevelId"`
UserLevelNumber *int `json:"UserLevelNumber"`
ParentId *int `json:"parentId"`
StatusId *int `json:"statusId"`
IsPublish *bool `json:"isPublish"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleCategoriesCreateRequest struct {
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Tags *string `json:"tags"`
Slug *string `json:"slug"`
CreatedById *uint `json:"createdById"`
ParentId *int `json:"parentId"`
OldCategoryId *uint `json:"oldCategoryId"`
}
func (req ArticleCategoriesCreateRequest) ToEntity() *entity.ArticleCategories {
return &entity.ArticleCategories{
Title: req.Title,
Description: req.Description,
Tags: req.Tags,
ParentId: req.ParentId,
Slug: req.Slug,
OldCategoryId: req.OldCategoryId,
StatusId: req.StatusId,
}
}
type ArticleCategoriesUpdateRequest struct {
ID uint `json:"id" validate:"required"`
Title string `json:"title" validate:"required"`
Description string `json:"description" validate:"required"`
StatusId int `json:"statusId" validate:"required"`
Tags *string `json:"tags"`
Slug *string `json:"slug"`
ParentId *int `json:"parentId"`
CreatedById *uint `json:"createdById"`
IsPublish *bool `json:"isPublish"`
PublishedAt *time.Time `json:"publishedAt"`
}
func (req ArticleCategoriesUpdateRequest) ToEntity() *entity.ArticleCategories {
return &entity.ArticleCategories{
ID: req.ID,
Title: req.Title,
Description: req.Description,
ParentId: req.ParentId,
Slug: req.Slug,
Tags: req.Tags,
StatusId: req.StatusId,
IsPublish: req.IsPublish,
PublishedAt: req.PublishedAt,
UpdatedAt: time.Now(),
}
}
type ArticleCategoriesQueryRequestContext struct {
Title string `json:"title"`
Description string `json:"description"`
ParentId string `json:"parentId"`
StatusId string `json:"statusId"`
IsPublish string `json:"isPublish"`
UserLevelId string `json:"UserLevelId"`
UserLevelNumber string `json:"UserLevelNumber"`
}
func (req ArticleCategoriesQueryRequestContext) ToParamRequest() ArticleCategoriesQueryRequest {
var request ArticleCategoriesQueryRequest
if title := req.Title; title != "" {
request.Title = &title
}
if description := req.Description; description != "" {
request.Description = &description
}
if parentIdStr := req.ParentId; parentIdStr != "" {
parentId, err := strconv.Atoi(parentIdStr)
if err == nil {
request.ParentId = &parentId
}
}
if isPublishStr := req.IsPublish; isPublishStr != "" {
isPublish, err := strconv.ParseBool(isPublishStr)
if err == nil {
request.IsPublish = &isPublish
}
}
if statusIdStr := req.StatusId; statusIdStr != "" {
statusId, err := strconv.Atoi(statusIdStr)
if err == nil {
request.StatusId = &statusId
}
}
if userLevelIdStr := req.UserLevelId; userLevelIdStr != "" {
userLevelId, err := strconv.Atoi(userLevelIdStr)
if err == nil {
userLevelIdUint := uint(userLevelId)
request.UserLevelId = &userLevelIdUint
}
}
if userLevelNumberStr := req.UserLevelNumber; userLevelNumberStr != "" {
userLevelNumber, err := strconv.Atoi(userLevelNumberStr)
if err == nil {
request.UserLevelNumber = &userLevelNumber
}
}
return request
}

View File

@ -0,0 +1,23 @@
package response
import "time"
type ArticleCategoriesResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
ThumbnailUrl string `json:"thumbnailUrl"`
Slug *string `json:"slug"`
Tags []string `json:"tags"`
ThumbnailPath *string `json:"thumbnailPath"`
ParentId *int `json:"parentId"`
OldCategoryId *uint `json:"oldCategoryId"`
CreatedById *uint `json:"createdById"`
StatusId int `json:"statusId"`
IsPublish *bool `json:"isPublish"`
PublishedAt *time.Time `json:"publishedAt"`
IsEnabled *bool `json:"isEnabled"`
IsActive *bool `json:"isActive"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}

View File

@ -0,0 +1,281 @@
package service
import (
"context"
"io"
"log"
"math/rand"
"mime"
"narasi-ahli-be/app/database/entity"
"narasi-ahli-be/app/module/article_categories/mapper"
"narasi-ahli-be/app/module/article_categories/repository"
"narasi-ahli-be/app/module/article_categories/request"
"narasi-ahli-be/app/module/article_categories/response"
usersRepository "narasi-ahli-be/app/module/users/repository"
config "narasi-ahli-be/config/config"
minioStorage "narasi-ahli-be/config/config"
"narasi-ahli-be/utils/paginator"
utilSvc "narasi-ahli-be/utils/service"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog"
)
// ArticleCategoriesService
type articleCategoriesService struct {
Repo repository.ArticleCategoriesRepository
UsersRepo usersRepository.UsersRepository
MinioStorage *minioStorage.MinioStorage
Log zerolog.Logger
Cfg *config.Config
}
// ArticleCategoriesService define interface of IArticleCategoriesService
type ArticleCategoriesService interface {
All(req request.ArticleCategoriesQueryRequest, authToken string) (articleCategories []*response.ArticleCategoriesResponse, paging paginator.Pagination, err error)
Show(id uint) (articleCategories *response.ArticleCategoriesResponse, err error)
ShowByOldId(id uint) (articleCategories *response.ArticleCategoriesResponse, err error)
ShowBySlug(slug string) (articleCategories *response.ArticleCategoriesResponse, err error)
Save(req request.ArticleCategoriesCreateRequest, authToken string) (articleCategories *entity.ArticleCategories, err error)
SaveThumbnail(c *fiber.Ctx) (err error)
Update(id uint, req request.ArticleCategoriesUpdateRequest) (err error)
Delete(id uint) error
Viewer(c *fiber.Ctx) error
}
// NewArticleCategoriesService init ArticleCategoriesService
func NewArticleCategoriesService(repo repository.ArticleCategoriesRepository, usersRepo usersRepository.UsersRepository, minioStorage *minioStorage.MinioStorage, log zerolog.Logger, cfg *config.Config) ArticleCategoriesService {
return &articleCategoriesService{
Repo: repo,
UsersRepo: usersRepo,
MinioStorage: minioStorage,
Log: log,
Cfg: cfg,
}
}
// All implement interface of ArticleCategoriesService
func (_i *articleCategoriesService) All(req request.ArticleCategoriesQueryRequest, authToken string) (articleCategoriess []*response.ArticleCategoriesResponse, paging paginator.Pagination, err error) {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
if createdBy != nil {
if createdBy.UserLevel.LevelNumber > 1 {
req.UserLevelId = &createdBy.UserLevelId
}
}
results, paging, err := _i.Repo.GetAll(req)
if err != nil {
return
}
host := _i.Cfg.App.Domain
for _, result := range results {
articleCategoriess = append(articleCategoriess, mapper.ArticleCategoriesResponseMapper(result, host))
}
return
}
func (_i *articleCategoriesService) Show(id uint) (articleCategories *response.ArticleCategoriesResponse, err error) {
result, err := _i.Repo.FindOne(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil
}
func (_i *articleCategoriesService) ShowByOldId(id uint) (articleCategories *response.ArticleCategoriesResponse, err error) {
result, err := _i.Repo.FindOneByOldId(id)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil
}
func (_i *articleCategoriesService) ShowBySlug(slug string) (articleCategories *response.ArticleCategoriesResponse, err error) {
result, err := _i.Repo.FindOneBySlug(slug)
if err != nil {
return nil, err
}
host := _i.Cfg.App.Domain
return mapper.ArticleCategoriesResponseMapper(result, host), nil
}
func (_i *articleCategoriesService) Save(req request.ArticleCategoriesCreateRequest, authToken string) (articleCategories *entity.ArticleCategories, err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
if req.CreatedById != nil {
createdBy, err := _i.UsersRepo.FindOne(*req.CreatedById)
if err != nil {
return nil, err
}
newReq.CreatedById = &createdBy.ID
} else {
createdBy := utilSvc.GetUserInfo(_i.Log, _i.UsersRepo, authToken)
newReq.CreatedById = &createdBy.ID
}
return _i.Repo.Create(newReq)
}
func (_i *articleCategoriesService) SaveThumbnail(c *fiber.Ctx) (err error) {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
form, err := c.MultipartForm()
if err != nil {
return err
}
files := form.File["files"]
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
// Iterasi semua file yang diunggah
for _, file := range files {
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Uploader:: loop1").
Interface("data", file).Msg("")
src, err := file.Open()
if err != nil {
return err
}
defer src.Close()
filename := filepath.Base(file.Filename)
filenameWithoutExt := filepath.Clean(filename[:len(filename)-len(filepath.Ext(filename))])
extension := filepath.Ext(file.Filename)[1:]
rand.New(rand.NewSource(time.Now().UnixNano()))
randUniqueId := rand.Intn(1000000)
newFilenameWithoutExt := filenameWithoutExt + "_" + strconv.Itoa(randUniqueId)
newFilename := newFilenameWithoutExt + "." + extension
objectName := "articles/category/thumbnail/" + newFilename
findCategory, err := _i.Repo.FindOne(uint(id))
findCategory.ThumbnailPath = &objectName
err = _i.Repo.Update(uint(id), findCategory)
if err != nil {
return err
}
// Upload file ke MinIO
_, err = minioClient.PutObject(context.Background(), bucketName, objectName, src, file.Size, minio.PutObjectOptions{})
if err != nil {
return err
}
}
return
}
func (_i *articleCategoriesService) Update(id uint, req request.ArticleCategoriesUpdateRequest) (err error) {
_i.Log.Info().Interface("data", req).Msg("")
newReq := req.ToEntity()
if req.CreatedById != nil {
createdBy, err := _i.UsersRepo.FindOne(*req.CreatedById)
if err != nil {
return err
}
newReq.CreatedById = &createdBy.ID
}
return _i.Repo.Update(id, newReq)
}
func (_i *articleCategoriesService) Delete(id uint) error {
result, err := _i.Repo.FindOne(id)
if err != nil {
return err
}
isActive := false
result.IsActive = &isActive
return _i.Repo.Update(id, result)
}
func (_i *articleCategoriesService) Viewer(c *fiber.Ctx) (err error) {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
result, err := _i.Repo.FindOne(uint(id))
if err != nil {
return err
}
if result.ThumbnailPath == nil {
return nil
}
ctx := context.Background()
bucketName := _i.MinioStorage.Cfg.ObjectStorage.MinioStorage.BucketName
objectName := result.ThumbnailPath
_i.Log.Info().Str("timestamp", time.Now().
Format(time.RFC3339)).Str("Service:Resource", "Article:Uploads").
Interface("data", objectName).Msg("")
// Create minio connection.
minioClient, err := _i.MinioStorage.ConnectMinio()
if err != nil {
// Return status 500 and minio connection error.
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"msg": err.Error(),
})
}
fileContent, err := minioClient.GetObject(ctx, bucketName, *objectName, minio.GetObjectOptions{})
if err != nil {
log.Fatalln(err)
}
defer fileContent.Close()
contentType := mime.TypeByExtension("." + getFileExtension(*objectName))
if contentType == "" {
contentType = "application/octet-stream"
}
c.Set("Content-Type", contentType)
if _, err := io.Copy(c.Response().BodyWriter(), fileContent); err != nil {
return err
}
return
}
func getFileExtension(filename string) string {
// split file name
parts := strings.Split(filename, ".")
// jika tidak ada ekstensi, kembalikan string kosong
if len(parts) == 1 || (len(parts) == 2 && parts[0] == "") {
return ""
}
// ambil ekstensi terakhir
return parts[len(parts)-1]
}

View File

@ -0,0 +1,54 @@
package article_category_details
import (
"narasi-ahli-be/app/module/article_category_details/controller"
"narasi-ahli-be/app/module/article_category_details/repository"
"narasi-ahli-be/app/module/article_category_details/service"
"github.com/gofiber/fiber/v2"
"go.uber.org/fx"
)
// struct of ArticleCategoryDetailsRouter
type ArticleCategoryDetailsRouter struct {
App fiber.Router
Controller *controller.Controller
}
// register bulky of ArticleCategoryDetails module
var NewArticleCategoryDetailsModule = fx.Options(
// register repository of ArticleCategoryDetails module
fx.Provide(repository.NewArticleCategoryDetailsRepository),
// register service of ArticleCategoryDetails module
fx.Provide(service.NewArticleCategoryDetailsService),
// register controller of ArticleCategoryDetails module
fx.Provide(controller.NewController),
// register router of ArticleCategoryDetails module
fx.Provide(NewArticleCategoryDetailsRouter),
)
// init ArticleCategoryDetailsRouter
func NewArticleCategoryDetailsRouter(fiber *fiber.App, controller *controller.Controller) *ArticleCategoryDetailsRouter {
return &ArticleCategoryDetailsRouter{
App: fiber,
Controller: controller,
}
}
// register routes of ArticleCategoryDetails module
func (_i *ArticleCategoryDetailsRouter) RegisterArticleCategoryDetailsRoutes() {
// define controllers
articleCategoryDetailsController := _i.Controller.ArticleCategoryDetails
// define routes
_i.App.Route("/article-category-details", func(router fiber.Router) {
router.Get("/", articleCategoryDetailsController.All)
router.Get("/:id", articleCategoryDetailsController.Show)
router.Post("/", articleCategoryDetailsController.Save)
router.Put("/:id", articleCategoryDetailsController.Update)
router.Delete("/:id", articleCategoryDetailsController.Delete)
})
}

View File

@ -0,0 +1,185 @@
package controller
import (
"narasi-ahli-be/app/module/article_category_details/request"
"narasi-ahli-be/app/module/article_category_details/service"
"narasi-ahli-be/utils/paginator"
"strconv"
"github.com/gofiber/fiber/v2"
utilRes "narasi-ahli-be/utils/response"
utilVal "narasi-ahli-be/utils/validator"
)
type articleCategoryDetailsController struct {
articleCategoryDetailsService service.ArticleCategoryDetailsService
}
type ArticleCategoryDetailsController interface {
All(c *fiber.Ctx) error
Show(c *fiber.Ctx) error
Save(c *fiber.Ctx) error
Update(c *fiber.Ctx) error
Delete(c *fiber.Ctx) error
}
func NewArticleCategoryDetailsController(articleCategoryDetailsService service.ArticleCategoryDetailsService) ArticleCategoryDetailsController {
return &articleCategoryDetailsController{
articleCategoryDetailsService: articleCategoryDetailsService,
}
}
// All get all ArticleCategoryDetails
// @Summary Get all ArticleCategoryDetails
// @Description API for getting all ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details [get]
func (_i *articleCategoryDetailsController) All(c *fiber.Ctx) error {
paginate, err := paginator.Paginate(c)
if err != nil {
return err
}
var req request.ArticleCategoryDetailsQueryRequest
req.Pagination = paginate
articleCategoryDetailsData, paging, err := _i.articleCategoryDetailsService.All(req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails list successfully retrieved"},
Data: articleCategoryDetailsData,
Meta: paging,
})
}
// Show get one ArticleCategoryDetails
// @Summary Get one ArticleCategoryDetails
// @Description API for getting one ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param id path int true "ArticleCategoryDetails ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details/{id} [get]
func (_i *articleCategoryDetailsController) Show(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
articleCategoryDetailsData, err := _i.articleCategoryDetailsService.Show(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully retrieved"},
Data: articleCategoryDetailsData,
})
}
// Save create ArticleCategoryDetails
// @Summary Create ArticleCategoryDetails
// @Description API for create ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Body request.ArticleCategoryDetailsCreateRequest
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details [post]
func (_i *articleCategoryDetailsController) Save(c *fiber.Ctx) error {
req := new(request.ArticleCategoryDetailsCreateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err := _i.articleCategoryDetailsService.Save(*req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully created"},
})
}
// Update update ArticleCategoryDetails
// @Summary update ArticleCategoryDetails
// @Description API for update ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Body request.ArticleCategoryDetailsUpdateRequest
// @Param id path int true "ArticleCategoryDetails ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details/{id} [put]
func (_i *articleCategoryDetailsController) Update(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
req := new(request.ArticleCategoryDetailsUpdateRequest)
if err := utilVal.ParseAndValidate(c, req); err != nil {
return err
}
err = _i.articleCategoryDetailsService.Update(uint(id), *req)
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully updated"},
})
}
// Delete delete ArticleCategoryDetails
// @Summary delete ArticleCategoryDetails
// @Description API for delete ArticleCategoryDetails
// @Tags Untags
// @Security Bearer
// @Param X-Csrf-Token header string true "Insert the X-Csrf-Token"
// @Param id path int true "ArticleCategoryDetails ID"
// @Success 200 {object} response.Response
// @Failure 401 {object} response.Response
// @Failure 404 {object} response.Response
// @Failure 422 {object} response.Response
// @Failure 500 {object} response.Response
// @Router /article-category-details/{id} [delete]
func (_i *articleCategoryDetailsController) Delete(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 0)
if err != nil {
return err
}
err = _i.articleCategoryDetailsService.Delete(uint(id))
if err != nil {
return err
}
return utilRes.Resp(c, utilRes.Response{
Messages: utilRes.Messages{"ArticleCategoryDetails successfully deleted"},
})
}

View File

@ -0,0 +1,13 @@
package controller
import "narasi-ahli-be/app/module/article_category_details/service"
type Controller struct {
ArticleCategoryDetails ArticleCategoryDetailsController
}
func NewController(ArticleCategoryDetailsService service.ArticleCategoryDetailsService) *Controller {
return &Controller{
ArticleCategoryDetails: NewArticleCategoryDetailsController(ArticleCategoryDetailsService),
}
}

View File

@ -0,0 +1,21 @@
package mapper
import (
"narasi-ahli-be/app/database/entity/article_category_details"
res "narasi-ahli-be/app/module/article_category_details/response"
)
func ArticleCategoryDetailsResponseMapper(articleCategoryDetailsReq *article_category_details.ArticleCategoryDetails) (articleCategoryDetailsRes *res.ArticleCategoryDetailsResponse) {
if articleCategoryDetailsReq != nil {
articleCategoryDetailsRes = &res.ArticleCategoryDetailsResponse{
ID: articleCategoryDetailsReq.ID,
ArticleId: articleCategoryDetailsReq.ArticleId,
CategoryId: articleCategoryDetailsReq.CategoryId,
IsActive: articleCategoryDetailsReq.IsActive,
CreatedAt: articleCategoryDetailsReq.CreatedAt,
UpdatedAt: articleCategoryDetailsReq.UpdatedAt,
}
}
return articleCategoryDetailsRes
}

View File

@ -0,0 +1,78 @@
package repository
import (
"narasi-ahli-be/app/database"
"narasi-ahli-be/app/database/entity/article_category_details"
"narasi-ahli-be/app/module/article_category_details/request"
"narasi-ahli-be/utils/paginator"
)
type articleCategoryDetailsRepository struct {
DB *database.Database
}
// ArticleCategoryDetailsRepository define interface of IArticleCategoryDetailsRepository
type ArticleCategoryDetailsRepository interface {
GetAll(req request.ArticleCategoryDetailsQueryRequest) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, paging paginator.Pagination, err error)
FindOne(id uint) (articleCategoryDetails *article_category_details.ArticleCategoryDetails, err error)
FindByArticleId(articleId uint) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, err error)
Create(articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error)
Update(id uint, articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error)
Delete(id uint) (err error)
}
func NewArticleCategoryDetailsRepository(db *database.Database) ArticleCategoryDetailsRepository {
return &articleCategoryDetailsRepository{
DB: db,
}
}
// implement interface of IArticleCategoryDetailsRepository
func (_i *articleCategoryDetailsRepository) GetAll(req request.ArticleCategoryDetailsQueryRequest) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, paging paginator.Pagination, err error) {
var count int64
query := _i.DB.DB.Model(&article_category_details.ArticleCategoryDetails{})
query.Count(&count)
req.Pagination.Count = count
req.Pagination = paginator.Paging(req.Pagination)
err = query.Offset(req.Pagination.Offset).Limit(req.Pagination.Limit).Find(&articleCategoryDetailss).Error
if err != nil {
return
}
paging = *req.Pagination
return
}
func (_i *articleCategoryDetailsRepository) FindOne(id uint) (articleCategoryDetails *article_category_details.ArticleCategoryDetails, err error) {
if err := _i.DB.DB.First(&articleCategoryDetails, id).Error; err != nil {
return nil, err
}
return articleCategoryDetails, nil
}
func (_i *articleCategoryDetailsRepository) FindByArticleId(articleId uint) (articleCategoryDetailss []*article_category_details.ArticleCategoryDetails, err error) {
if err := _i.DB.DB.Where("article_id = ?", articleId).Preload("Category").Find(&articleCategoryDetailss).Error; err != nil {
return nil, err
}
return articleCategoryDetailss, nil
}
func (_i *articleCategoryDetailsRepository) Create(articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error) {
return _i.DB.DB.Create(articleCategoryDetails).Error
}
func (_i *articleCategoryDetailsRepository) Update(id uint, articleCategoryDetails *article_category_details.ArticleCategoryDetails) (err error) {
return _i.DB.DB.Model(&article_category_details.ArticleCategoryDetails{}).
Where(&article_category_details.ArticleCategoryDetails{ID: id}).
Updates(articleCategoryDetails).Error
}
func (_i *articleCategoryDetailsRepository) Delete(id uint) error {
return _i.DB.DB.Delete(&article_category_details.ArticleCategoryDetails{}, id).Error
}

View File

@ -0,0 +1,52 @@
package request
import (
"narasi-ahli-be/app/database/entity/article_category_details"
"narasi-ahli-be/utils/paginator"
"time"
)
type ArticleCategoryDetailsGeneric interface {
ToEntity()
}
type ArticleCategoryDetailsQueryRequest struct {
ArticleId int `json:"article_id" validate:"required"`
CategoryId int `json:"category_id" validate:"required"`
IsActive bool `json:"is_active" validate:"required"`
Pagination *paginator.Pagination `json:"pagination"`
}
type ArticleCategoryDetailsCreateRequest struct {
ArticleId uint `json:"article_id" validate:"required"`
CategoryId int `json:"category_id" validate:"required"`
IsActive bool `json:"is_active" validate:"required"`
}
func (req ArticleCategoryDetailsCreateRequest) ToEntity() *article_category_details.ArticleCategoryDetails {
return &article_category_details.ArticleCategoryDetails{
ArticleId: req.ArticleId,
CategoryId: req.CategoryId,
IsActive: req.IsActive,
}
}
type ArticleCategoryDetailsUpdateRequest struct {
ID uint `json:"id" validate:"required"`
ArticleId uint `json:"article_id" validate:"required"`
CategoryId int `json:"category_id" validate:"required"`
IsActive bool `json:"is_active" validate:"required"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (req ArticleCategoryDetailsUpdateRequest) ToEntity() *article_category_details.ArticleCategoryDetails {
return &article_category_details.ArticleCategoryDetails{
ID: req.ID,
ArticleId: req.ArticleId,
CategoryId: req.CategoryId,
IsActive: req.IsActive,
CreatedAt: req.CreatedAt,
UpdatedAt: req.UpdatedAt,
}
}

View File

@ -0,0 +1,12 @@
package response
import "time"
type ArticleCategoryDetailsResponse struct {
ID uint `json:"id"`
ArticleId uint `json:"article_id"`
CategoryId int `json:"category_id"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

Some files were not shown because too many files have changed in this diff Show More