package core import ( "context" "database/sql" "errors" "fmt" "time" "github.com/google/uuid" "github.com/uptrace/bun" "github.com/uptrace/bun/dialect/pgdialect" "github.com/uptrace/bun/driver/pgdriver" ) var DB *bun.DB type DSN struct { Hostname string Port string DBName string User string Password string } func (dsn *DSN) ToString() string { return fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable", dsn.User, dsn.Password, dsn.Hostname, dsn.Port, dsn.DBName) } type Status string const ( Active Status = "Active" Inactive Status = "Inactive" ) type Group string const ( LatosaGroup Group = "latosa" WingTsunGroup Group = "wing-tsun" ) type UserAttributes struct { Groups []Group `json:"groups"` } type PermissionConditions struct { Groups *[]Group `json:"groups,omitempty"` } type User struct { bun.BaseModel `bun:"table:users"` UserID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"userId"` FirstName string `bun:"firstname,notnull" json:"firstname"` LastName string `bun:"lastname,notnull" json:"lastname"` Email string `bun:"email,unique,notnull" json:"email"` Password string `bun:"password,notnull" json:"password,omitempty"` Phone string `bun:"phone,notnull" json:"phone"` CreatedAt time.Time `bun:"created_at,default:current_timestamp" json:"createdAt"` UpdatedAt time.Time `bun:"updated_at,default:current_timestamp" json:"updatedAt"` Events []Event `bun:"m2m:events_to_users,join:User=Event" json:"events,omitempty"` Articles []*Blog `bun:"rel:has-many,join:user_id=blog_id" json:"articles,omitempty"` Attributes UserAttributes `bun:"attributes,type:jsonb" json:"attributes"` } func (u *User) Insert(ctx context.Context) (sql.Result, error) { u.Password = fmt.Sprintf("crypt('%s', gen_salt('bf'))", u.Password) return DB.NewInsert(). Model(u). Value("password", u.Password). Exec(ctx) } func Verify(ctx context.Context, email, password string) (*User, error) { var user User count, err := DB.NewSelect(). Model(&user). Where("email = ? AND password = crypt(?, password)", email, password). Limit(1). ScanAndCount(context.Background()) if count == 0 { return nil, fmt.Errorf("invalid email or password") } if err != nil { if err == sql.ErrNoRows { return nil, fmt.Errorf("invalid email or password") } return nil, err } return &user, nil } type Permission struct { bun.BaseModel `bun:"table:permissions"` ID int `bun:"id,pk,autoincrement" json:"id"` Resource string `bun:"resource,notnull" json:"resource"` Action string `bun:"action,notnull" json:"action"` Conditions PermissionConditions `bun:"conditions,type:jsonb" json:"conditions"` } type Role struct { bun.BaseModel `bun:"table:roles"` ID uuid.UUID `bun:"id,pk,type:uuid,default:gen_random_uuid()" json:"id"` Name string `bun:"name,unique,notnull" json:"name"` } type PermissionToRole struct { bun.BaseModel `bun:"table:permissions_to_users"` PermissionID int `bun:"permission_id,pk"` RoleID uuid.UUID `bun:"type:uuid,pk"` Permission *Permission `bun:"rel:belongs-to,join:permission_id=id"` Role *Role `bun:"rel:belongs-to,join:role_id=id"` } type UserToRole struct { bun.BaseModel `bun:"table:users_to_roles"` UserID uuid.UUID `bun:"user_id,type:uuid,pk"` RoleID uuid.UUID `bun:"type:uuid,pk"` User *User `bun:"rel:belongs-to,join:user_id=user_id"` Role *Role `bun:"rel:belongs-to,join:role_id=id"` } type Event struct { bun.BaseModel `bun:"table:events"` EventID uuid.UUID `bun:"event_id,type:uuid,pk,default:gen_random_uuid()" json:"id"` CreationDate time.Time `bun:"creation_date,notnull,default:current_timestamp" json:"creationDate"` ScheduleStart time.Time `bun:"schedule_start,notnull" json:"start"` ScheduleEnd time.Time `bun:"schedule_end,notnull" json:"end"` Status Status `bun:"status,notnull,default:'Inactive'" json:"status"` } type EventToUser struct { bun.BaseModel `bun:"table:events_to_users"` EventID uuid.UUID `bun:"type:uuid,pk"` UserID uuid.UUID `bun:"type:uuid,pk"` Event *Event `bun:"rel:belongs-to,join:event_id=event_id"` User *User `bun:"rel:belongs-to,join:user_id=user_id"` } type Blog struct { bun.BaseModel `bun:"table:blogs"` BlogID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"blogID"` Slug string `bun:"slug,unique,notnull" json:"slug"` Content string `bun:"content,notnull" json:"content"` Label string `bun:"label" json:"label"` AuthorID uuid.UUID `bun:"author_id,type:uuid,notnull" json:"authorID"` Published time.Time `bun:"published,default:current_timestamp" json:"published"` Summary string `bun:"summary" json:"summary"` Image string `bun:"image" json:"image"` Href string `bun:"href" json:"href"` Author User `bun:"rel:belongs-to,join:author_id=user_id" json:"author"` } type WebsiteSettings struct { bun.BaseModel `bun:"table:website_settings"` ID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"id"` AutoAcceptDemand bool `bun:"auto_accept_demand,default:false" json:"autoAcceptDemand"` } type Media struct { bun.BaseModel `bun:"table:media"` ID uuid.UUID `bun:"type:uuid,pk,default:gen_random_uuid()" json:"id"` AuthorID uuid.UUID `bun:"author_id,type:uuid,notnull" json:"authorID"` Author *User `bun:"rel:belongs-to,join:author_id=user_id" json:"author,omitempty"` Type string `bun:"media_type" json:"type"` // Image, Video, GIF etc. Add support for PDFs? Alt string `bun:"media_alt" json:"alt"` Path string `bun:"media_path" json:"path"` Size int64 `bun:"media_size" json:"size"` URL string `bun:"-" json:"url"` } type ShortcodeType string const ( ShortcodeMedia ShortcodeType = "media" ShortcodeValue ShortcodeType = "value" ) type Shortcode struct { bun.BaseModel `bun:"table:shortcodes,alias:sc"` ID int64 `bun:"id,pk,autoincrement" json:"id"` // Primary key Code string `bun:"code,notnull,unique" json:"code"` // The shortcode value Type ShortcodeType `bun:"shortcode_type,notnull" json:"type"` Value *string `bun:"value" json:"value,omitempty"` MediaID *uuid.UUID `bun:"media_id,type:uuid" json:"media_id,omitempty"` // Nullable reference to another table's ID Media *Media `bun:"rel:belongs-to,join:media_id=id" json:"media,omitempty"` // Relation to Media } func (s *Shortcode) Validate() error { if s.Value != nil && s.MediaID != nil { return errors.New("both value and media_id cannot be set at the same time") } if s.Value == nil && s.MediaID == nil { return errors.New("either value or media_id must be set") } return nil } func InitDatabase(dsn DSN) (*bun.DB, error) { sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn.ToString()))) db := bun.NewDB(sqldb, pgdialect.New()) ctx := context.Background() _, err := db.ExecContext(ctx, "CREATE EXTENSION IF NOT EXISTS pgcrypto;") if err != nil { return nil, err } db.RegisterModel((*EventToUser)(nil)) db.RegisterModel((*PermissionToRole)(nil)) db.RegisterModel((*UserToRole)(nil)) _, err = db.NewCreateTable().Model((*User)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Event)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*EventToUser)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Blog)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*WebsiteSettings)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Media)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Shortcode)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Role)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*Permission)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*PermissionToRole)(nil)).IfNotExists().Exec(ctx) _, err = db.NewCreateTable().Model((*UserToRole)(nil)).IfNotExists().Exec(ctx) if err != nil { return nil, err } return db, nil }