first commit

This commit is contained in:
2026-08-08 23:12:37 +02:00
parent ff6ef46cd4
commit e3fd942c47
20 changed files with 494 additions and 271 deletions
@@ -70,13 +70,7 @@ func (s *PasswordAuthenticator) Register(ctx context.Context, registration auth.
Enabled: true, Enabled: true,
} }
count, err := s.userRepository.CountUsers() // TODO: create team "Espace personnel" and put user in it
if err != nil {
return nil, err
}
if count == 0 {
user.Administrator = true
}
user, err = s.userRepository.CreateUser(user) user, err = s.userRepository.CreateUser(user)
@@ -1,13 +1,13 @@
package turso package turso
import ( import (
"database/sql"
"embed" "embed"
"fmt" "fmt"
"github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs" "github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3" _ "github.com/mattn/go-sqlite3"
"trankilou.fr/lassistanoque/backend/internal/config" "trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
@@ -18,11 +18,11 @@ import (
var FS embed.FS var FS embed.FS
type TursoDB struct { type TursoDB struct {
DB *sqlx.DB DB *sql.DB
} }
func NewTursoDB(cfg *config.Config) (*TursoDB, error) { func NewTursoDB(cfg *config.Config) (*TursoDB, error) {
db, err := sqlx.Connect("turso", cfg.DatabaseURL) db, err := sql.Open("turso", cfg.DatabaseURL)
if err != nil { if err != nil {
return nil, fmt.Errorf("error connecting to db %: %s", cfg.DatabaseURL, err) return nil, fmt.Errorf("error connecting to db %: %s", cfg.DatabaseURL, err)
} }
@@ -36,7 +36,7 @@ func (db *TursoDB) Close() {
} }
func (db *TursoDB) Migrate() error { func (db *TursoDB) Migrate() error {
sqldb := db.DB.DB sqldb := db.DB
dbDriver, err := sqlite3.WithInstance(sqldb, &sqlite3.Config{}) dbDriver, err := sqlite3.WithInstance(sqldb, &sqlite3.Config{})
if err != nil { if err != nil {
return err return err
@@ -57,13 +57,13 @@ func (db *TursoDB) Migrate() error {
} }
func (db *TursoDB) UserRepository() domain.UserRepository { func (db *TursoDB) UserRepository() domain.UserRepository {
return &TursoUserRepository{DB: db.DB} return NewTursoUserRepository(db.DB)
} }
func (db *TursoDB) SettingsRepository() domain.SettingsRepository { func (db *TursoDB) SettingsRepository() domain.SettingsRepository {
return &TursoSettingsRepository{DB: db.DB} return NewTursoSettingsRepository(db.DB)
} }
func (db *TursoDB) FileRepository() domain.FileRepository { func (db *TursoDB) FileRepository() domain.FileRepository {
return &TursoFileRepository{DB: db.DB} return NewTursoFileRepository(db.DB)
} }
@@ -1,74 +1,47 @@
package turso package turso
import ( import (
"database/sql"
"io" "io"
"log"
"time" "time"
"github.com/jmoiron/sqlx" "gitea.trankilou.fr/fabien/lasebuche"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility" "trankilou.fr/lassistanoque/backend/internal/utility"
) )
type TursoFileRepository struct { type TursoFileRepository struct {
DB *sqlx.DB db *sql.DB
FileTable lasebuche.Table[domain.File]
}
func NewTursoFileRepository(db *sql.DB) *TursoFileRepository {
dialect := lasebuche.NewSqliteDialect()
fileTable, err := lasebuche.NewTable[domain.File](db, dialect, "settings")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
return &TursoFileRepository{
db: db,
FileTable: fileTable,
}
} }
func (fr *TursoFileRepository) FindByID(id string) (*domain.File, error) { func (fr *TursoFileRepository) FindByID(id string) (*domain.File, error) {
var file domain.File return fr.FileTable.Get(id)
err := fr.DB.Get(&file, "select id, name, content_type, storage_path, storage_filename, date_created, date_updated, _version from files where id=$1", id)
return &file, err
} }
func (fr *TursoFileRepository) Create(file *domain.File) (*domain.File, error) { func (fr *TursoFileRepository) Create(file *domain.File) (*domain.File, error) {
file.ID = utility.GenID() return fr.FileTable.Insert(file)
file.VersionId = utility.GenID()
file.DateCreated = time.Now()
file.DateUpdated = time.Now()
_, err := fr.DB.NamedExec(
`insert into files (id, name, content_type, storage_path, storage_filename, date_created, date_updated, _version)
values (:id, :name, :content_type, :storage_path, :storage_filename, :date_created, :date_updated, :_version)`,
file)
if err != nil {
return nil, err
}
return file, nil
} }
func (fr *TursoFileRepository) Update(file *domain.File) (*domain.File, error) { func (fr *TursoFileRepository) Update(file *domain.File) (*domain.File, error) {
newVersion := utility.GenID() return fr.FileTable.Update(file)
file.DateUpdated = time.Now()
res, err := fr.DB.Exec(
`update files
set name=$1,
content_type=$2,
storage_path=$3,
storage_filename=$4,
date_updated=$5,
_version=$6
where id=$7 and _version=$8`,
file.Name, file.ContentType, file.StoragePath, file.StorageFilename, file.DateUpdated,
newVersion, file.ID, file.VersionId,
)
if err != nil {
return nil, err
}
if count, _ := res.RowsAffected(); count == 0 {
return nil, dberrors.ErrNoRowUpdated
}
file.VersionId = newVersion
return file, nil
} }
func (fr *TursoFileRepository) Delete(id string) error { func (fr *TursoFileRepository) Delete(id string) error {
res, err := fr.DB.Exec("delete from files where id=$1", id) return fr.FileTable.Delete(id)
if count, _ := res.RowsAffected(); count == 0 {
return dberrors.ErrNoRowUpdated
}
return err
} }
func (fr *TursoFileRepository) Upload(reader io.Reader, path string, name string, contentType string, replace bool) (*domain.File, error) { func (fr *TursoFileRepository) Upload(reader io.Reader, path string, name string, contentType string, replace bool) (*domain.File, error) {
@@ -90,18 +63,9 @@ func (fr *TursoFileRepository) Upload(reader io.Reader, path string, name string
Content: bytes, Content: bytes,
} }
_, err = fr.DB.NamedExec( return fr.FileTable.Insert(file)
`insert into files (id, name, content_type, storage_path, storage_filename, date_created, date_updated, _version, content)
values (:id, :name, :content_type, :storage_path, :storage_filename, :date_created, :date_updated, :_version, :content)`,
file)
if err != nil {
return nil, err
}
return file, nil
} }
func (fr *TursoFileRepository) Download(id string) (*domain.File, error) { func (fr *TursoFileRepository) Download(id string) (*domain.File, error) {
var file domain.File return fr.FileTable.Get(id)
err := fr.DB.Get(&file, "select * from files where id=$1", id)
return &file, err
} }
@@ -4,7 +4,11 @@ drop table system;
drop table oidc; drop table oidc;
drop table users; drop table users;
drop table user_addresses; drop table user_addresses;
drop table teams;
drop table user_teams;
drop table channels; drop table channels;
drop table tools; drop table tools;
drop table tasks; drop table tasks;
drop table history; drop table history;
drop table files;
drop table share;
@@ -1,36 +1,46 @@
create table providers ( create table providers (
id text not null primary key, id text not null primary key,
name text, team_id text not null,
name text not null,
key text, key text,
url text, url text,
_version text _date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table models ( create table models (
id text not null primary key, id text not null primary key,
provider_id text, team_id text not null,
name text, provider_id text not null,
modelname text, name text not null default '',
configuration text, modelname text not null default '',
_version text configuration text not null default '{}',
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table settings ( create table settings (
chat_model_id text, id text not null primary key,
default_lang text, default_lang text not null default 'en',
register_enabled numeric, register_enabled numeric not null default 1,
password_enabled numeric, password_enabled numeric not null default 1,
_version text _date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table oidc ( create table oidc (
id text not null primary key, id text not null primary key,
label text, label text not null,
domain text, domain text not null,
client_id text, client_id text not null,
client_secret text, client_secret text not null,
wellknown_url text, wellknown_url text not null,
_version text _date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table users ( create table users (
@@ -41,55 +51,112 @@ create table users (
email text not null unique, email text not null unique,
picture text not null default '', picture text not null default '',
enabled numeric default true, enabled numeric default true,
administrator numeric default false, theme text default 'auto',
lang text default 'en',
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null _version text not null
); );
create table user_addresses ( create table user_addresses (
id text not null primary key, id text not null primary key,
user_id text, user_id text not null,
type text, type text not null,
address text, address text not null,
_version text _date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
);
create table teams (
id text not null primary key,
label text not null,
default_model_id text,
_date_created numeric not null default current_timestamp,
_date_updated numeric ,
_version text not null
);
create table user_teams (
id text not null primary key,
user_id text not null,
team_id text not null,
administrator bool default 0,
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table channels ( create table channels (
id text not null primary key, id text not null primary key,
name text, team_id text not null,
type text, name text not null,
enabled numeric, type text not null,
configuration text, enabled numeric not null default 1,
_version text configuration text not null default '{}',
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table tools ( create table tools (
id text not null primary key, id text not null primary key,
name text, team_id text not null,
type text, name text not null,
enabled numeric, type text not null,
configuration text, enabled numeric not null default 1,
_version text configuration text not null default '{}',
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table tasks ( create table tasks (
id text not null primary key, id text not null primary key,
owner_id text, team_id text not null,
model_id text, model_id text not null,
label text, label text not null,
prompt text, prompt text not null,
cron text, cron text not null,
status text, status text not null,
next_datetime numeric, next_datetime numeric,
_version text _date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
create table history ( create table history (
id text not null primary key, id text not null primary key,
task_id text, task_id text not null,
start_datetimle numeric, start_datetime numeric,
end_datetime numeric, end_datetime numeric,
prompt text, prompt text not null,
log text, log text not null,
response text, response text not null,
_version text _date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
);
create table files (
id text not null primary key,
name text not null default '',
content_type text not null default '',
storage_path text not null default '',
storage_filename text not null default '',
content blob,
_date_created numeric not null default current_timestamp,
_date_updated numeric ,
_version text not null
);
create table share (
id text not null primary key,
object_id text not null,
object_type text not null,
team_id text not null,
enabled numeric not null default 0,
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
); );
@@ -1,2 +1,2 @@
insert into settings (default_lang, register_enabled, password_enabled, _version) insert into settings (id, default_lang, register_enabled, password_enabled, _version)
values ('en', true, true, 'init'); values ('settings', 'en', true, true, 'init');
@@ -1 +0,0 @@
drop table files;
@@ -1,11 +0,0 @@
create table files (
id text not null primary key,
name text not null default '',
content_type text not null default '',
storage_path text not null default '',
storage_filename text not null default '',
date_created numeric not null default current_timestamp,
date_updated numeric not null default 0,
content blob,
_version text not null
);
@@ -1,45 +1,34 @@
package turso package turso
import ( import (
"github.com/jmoiron/sqlx" "database/sql"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors" "log"
"gitea.trankilou.fr/fabien/lasebuche"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility"
) )
type TursoSettingsRepository struct { type TursoSettingsRepository struct {
DB *sqlx.DB db *sql.DB
SettingsTable lasebuche.Table[domain.Settings]
}
func NewTursoSettingsRepository(db *sql.DB) *TursoSettingsRepository {
dialect := lasebuche.NewSqliteDialect()
settingsTable, err := lasebuche.NewTable[domain.Settings](db, dialect, "settings")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
return &TursoSettingsRepository{
db: db,
SettingsTable: settingsTable,
}
} }
func (sr *TursoSettingsRepository) GetSettings() (*domain.Settings, error) { func (sr *TursoSettingsRepository) GetSettings() (*domain.Settings, error) {
var settings domain.Settings return sr.SettingsTable.Get("settings")
err := sr.DB.Get(&settings, "select * from settings limit 1")
return &settings, err
} }
func (sr *TursoSettingsRepository) UpdateSettings(setting *domain.Settings) (*domain.Settings, error) { func (sr *TursoSettingsRepository) UpdateSettings(setting *domain.Settings) (*domain.Settings, error) {
newVersion := utility.GenID() return sr.SettingsTable.Update(setting)
res, err := sr.DB.Exec(
`update settings
set chat_model_id=$1,
default_lang=$2,
register_enabled=$3,
password_enabled=$4,
_version=:$5
where _version=$6`,
setting.ChatModelID, setting.DefaultLang, setting.RegisterEnabled,
setting.PasswordEnabled, newVersion, setting.VersionId,
)
if err != nil {
return nil, err
}
if count, _ := res.RowsAffected(); count == 0 {
return nil, dberrors.ErrNoRowUpdated
}
setting.VersionId = newVersion
return setting, nil
} }
@@ -1,90 +1,143 @@
package turso package turso
import ( import (
"github.com/jmoiron/sqlx" "database/sql"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors" "log"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility"
"gitea.trankilou.fr/fabien/lasebuche"
) )
type TursoUserRepository struct { type TursoUserRepository struct {
DB *sqlx.DB DB *sql.DB
UserTable lasebuche.Table[domain.User]
TeamTable lasebuche.Table[domain.Team]
UserTeamTable lasebuche.Table[domain.UserTeam]
AddressTable lasebuche.Table[domain.UserAddress]
}
func NewTursoUserRepository(db *sql.DB) *TursoUserRepository {
dialect := lasebuche.NewSqliteDialect()
userTable, err := lasebuche.NewTable[domain.User](db, dialect, "users")
if err != nil {
log.Fatalf("error creating lasebuche user table")
}
teamTable, err := lasebuche.NewTable[domain.Team](db, dialect, "teams")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
userTeamTable, err := lasebuche.NewTable[domain.UserTeam](db, dialect, "user_teams")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
userAddressTable, err := lasebuche.NewTable[domain.UserAddress](db, dialect, "user_addresses")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
return &TursoUserRepository{
DB: db,
UserTable: userTable,
TeamTable: teamTable,
UserTeamTable: userTeamTable,
AddressTable: userAddressTable,
}
} }
func (ur *TursoUserRepository) FindUser(id string) (*domain.User, error) { func (ur *TursoUserRepository) FindUser(id string) (*domain.User, error) {
var user domain.User return ur.UserTable.Get(id)
err := ur.DB.Get(&user, "select * from users where id=$1", id)
return &user, err
} }
func (ur *TursoUserRepository) FindUserByEmail(email string) (*domain.User, error) { func (ur *TursoUserRepository) FindUserByEmail(email string) (*domain.User, error) {
var user domain.User return ur.UserTable.SelectOne("email=$1", email)
err := ur.DB.Get(&user, "select * from users where email=$1", email)
return &user, err
}
func (ur *TursoUserRepository) CountUsers() (int, error) {
var count int
err := ur.DB.Get(&count, "select count(*) from users")
return count, err
} }
func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) { func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) {
var users []*domain.User return ur.UserTable.SelectWhere("")
err := ur.DB.Select(&users, "select * from users order by lastname, firstname")
return users, err
} }
func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) { func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) {
return ur.UserTable.Insert(user)
user.ID = utility.GenID()
user.VersionId = utility.GenID()
_, err := ur.DB.NamedExec(
`insert into users (id, email, firstname, lastname, enabled, password, administrator, _version)
values (:id, :email, :firstname, :lastname, :enabled, :password, :administrator, :_version)`,
user)
if err != nil {
return nil, err
}
return user, nil
} }
func (ur *TursoUserRepository) UpdateUser(user *domain.User) (*domain.User, error) { func (ur *TursoUserRepository) UpdateUser(user *domain.User) (*domain.User, error) {
return ur.UserTable.Update(user)
newVersion := utility.GenID()
res, err := ur.DB.Exec(
`update users
set email=$1,
firstname=$2,
lastname=$3,
enabled=$4,
administrator=$5
_version=:$6
where id=$7 and _version=$8`,
user.Email, user.Firstname, user.Lastname,
user.Enabled, user.Administrator,
newVersion, user.ID, user.VersionId,
)
if err != nil {
return nil, err
}
if count, _ := res.RowsAffected(); count == 0 {
return nil, dberrors.ErrNoRowUpdated
}
user.VersionId = newVersion
return user, nil
} }
func (ur *TursoUserRepository) DeleteUser(id string) error { func (ur *TursoUserRepository) DeleteUser(id string) error {
res, err := ur.DB.Exec("delete from users where id=$1", id) return ur.UserTable.Delete(id)
if count, _ := res.RowsAffected(); count == 0 { }
return dberrors.ErrNoRowUpdated
} func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.Team, error) {
return err return ur.TeamTable.SelectOne("id=$1 and user_id=$2", teamid, userid)
}
func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) {
return ur.TeamTable.SelectWhere("user_id=$1", userid)
}
func (ur *TursoUserRepository) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) {
// TODO : gérér dans une transaction
// TODO : déplacer dans le service
team, err := ur.TeamTable.Insert(team)
if err != nil {
return nil, err
}
userTeam := &domain.UserTeam{
UserID: userid,
TeamID: team.ID,
Administrator: true,
}
_, err = ur.UserTeamTable.Insert(userTeam)
if err != nil {
return nil, err
}
return team, nil
}
func (ur *TursoUserRepository) UpdateTeam(userid string, team *domain.Team) (*domain.Team, error) {
return ur.TeamTable.Update(team)
}
func (ur *TursoUserRepository) DeleteTeam(userid string, teamid string) error {
return ur.TeamTable.Delete(teamid)
}
func (ur *TursoUserRepository) FindUserTeam(userid string, teamid string) (*domain.UserTeam, error) {
return ur.UserTeamTable.SelectOne("team_id=$1 and user_id=$2", teamid, userid)
}
func (ur *TursoUserRepository) ListUserTeams(userid string) ([]*domain.UserTeam, error) {
return ur.UserTeamTable.SelectWhere("user_id=$1", userid)
}
func (ur *TursoUserRepository) CreateUserTeam(userid string, team *domain.UserTeam) (*domain.UserTeam, error) {
return ur.UserTeamTable.Insert(team)
}
func (ur *TursoUserRepository) UpdateUserTeam(userid string, team *domain.UserTeam) (*domain.UserTeam, error) {
return ur.UserTeamTable.Update(team)
}
func (ur *TursoUserRepository) DeleteUserTeam(userid string, teamid string) error {
return ur.UserTeamTable.Delete(teamid)
}
func (ur *TursoUserRepository) PopulateUserWithTeams(user *domain.User) (*domain.UserWithTeams, error) {
return nil, nil
}
func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddress, error) {
return ur.AddressTable.SelectWhere("user_id=$1", id)
}
func (ur *TursoUserRepository) CreateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) {
addr.UserID = userid
return ur.AddressTable.Insert(addr)
}
func (ur *TursoUserRepository) UpdateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) {
return ur.AddressTable.Update(addr)
}
func (ur *TursoUserRepository) DeleteUserAddress(userid string, addrID string) error {
return ur.AddressTable.DeleteWhere("id=$1 and user_id=$2", addrID, userid)
} }
+12 -16
View File
@@ -15,11 +15,10 @@ import (
// Claims personnalisées embarquées dans le JWT. // Claims personnalisées embarquées dans le JWT.
type Claims struct { type Claims struct {
UserID string `json:"uid"` UserID string `json:"uid"`
Email string `json:"email"` Email string `json:"email"`
Firstname string `json:"firstname"` Firstname string `json:"firstname"`
Lastname string `json:"lastname"` Lastname string `json:"lastname"`
Administrator bool `json:"administrator"`
jwt.RegisteredClaims jwt.RegisteredClaims
} }
@@ -45,11 +44,10 @@ func NewJwtTokenManager(accessTTL, refreshTTL time.Duration, issuer string) *Jwt
func (tm *JwtTokenManager) GenerateAccessToken(user *domain.User) (string, time.Time, error) { func (tm *JwtTokenManager) GenerateAccessToken(user *domain.User) (string, time.Time, error) {
expiresAt := time.Now().Add(tm.accessTTL) expiresAt := time.Now().Add(tm.accessTTL)
claims := Claims{ claims := Claims{
UserID: user.ID, UserID: user.ID,
Email: user.Email, Email: user.Email,
Firstname: user.Firstname, Firstname: user.Firstname,
Lastname: user.Lastname, Lastname: user.Lastname,
Administrator: user.Administrator,
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
Issuer: tm.issuer, Issuer: tm.issuer,
Subject: user.ID, Subject: user.ID,
@@ -93,11 +91,10 @@ func (tm *JwtTokenManager) ParseAndValidate(tokenString string) (*domain.User, e
return nil, errors.New("token invalide") return nil, errors.New("token invalide")
} }
return &domain.User{ return &domain.User{
ID: claims.UserID, ID: claims.UserID,
Email: claims.Email, Email: claims.Email,
Firstname: claims.Firstname, Firstname: claims.Firstname,
Lastname: claims.Lastname, Lastname: claims.Lastname,
Administrator: claims.Administrator,
}, nil }, nil
} }
@@ -122,7 +119,6 @@ func (tm *JwtTokenManager) TokenMiddleware(next echo.HandlerFunc) echo.HandlerFu
c.Set(auth.ContextUserIDKey, claims.ID) c.Set(auth.ContextUserIDKey, claims.ID)
c.Set(auth.ContextEmailKey, claims.Email) c.Set(auth.ContextEmailKey, claims.Email)
c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname) c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname)
c.Set(auth.ContextAdminKey, claims.Administrator)
return next(c) return next(c)
} }
+8 -6
View File
@@ -1,13 +1,15 @@
package domain package domain
import "database/sql" import "time"
type Settings struct { type Settings struct {
ChatModelID sql.NullString `db:"chat_model_id"` ID string `db:"id" json:"id"`
DefaultLang string `db:"default_lang"` DefaultLang string `db:"default_lang"`
RegisterEnabled bool `db:"register_enabled"` RegisterEnabled bool `db:"register_enabled"`
PasswordEnabled bool `db:"password_enabled"` PasswordEnabled bool `db:"password_enabled"`
VersionId string `db:"_version"` DateCreated *time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
} }
type SettingsRepository interface { type SettingsRepository interface {
+74 -10
View File
@@ -1,23 +1,87 @@
package domain package domain
import (
"time"
)
type User struct { type User struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Firstname string `db:"firstname" json:"firstname,omitempty"` Firstname string `db:"firstname" json:"firstname,omitempty"`
Lastname string `db:"lastname" json:"lastname,omitempty"` Lastname string `db:"lastname" json:"lastname,omitempty"`
Password string `db:"password" json:"-"` Password string `db:"password" json:"-"`
Email string `db:"email" json:"email,omitempty"` Email string `db:"email" json:"email,omitempty"`
PictureID string `db:"picture" json:"picture,omitempty"` PictureID string `db:"picture" json:"picture,omitempty"`
Enabled bool `db:"enabled" json:"enabled"` Enabled bool `db:"enabled" json:"enabled"`
Administrator bool `db:"administrator" json:"administrator"` Theme string `db:"theme" json:"theme"`
VersionId string `db:"_version" json:"-"` Lang string `db:"lang" json:"lang"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type Team struct {
ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"`
DefaultModelID *string `db:"default_model_id" json:"default_model_id"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type UserTeam struct {
ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"`
TeamID string `db:"team_id" json:"team_id"`
Administrator bool `db:"administrator" json:"administrator"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type UserAddress struct {
ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"`
Type string `db:"type" json:"type"`
Address string `db:"address_id" json:"address"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
}
type TeamRelation struct {
IsAdministrator bool `json:"administrator"`
Team *Team `json:"team"`
}
type UserWithTeams struct {
User *User `json:"user"`
Teams []*TeamRelation `json:"teams"`
} }
type UserRepository interface { type UserRepository interface {
CountUsers() (int, error)
FindUser(id string) (*User, error) FindUser(id string) (*User, error)
FindUserByEmail(email string) (*User, error) FindUserByEmail(email string) (*User, error)
ListUsers() ([]*User, error) ListUsers() ([]*User, error)
CreateUser(user *User) (*User, error) CreateUser(user *User) (*User, error)
UpdateUser(user *User) (*User, error) UpdateUser(user *User) (*User, error)
DeleteUser(id string) error DeleteUser(id string) error
FindTeam(userid string, teamid string) (*Team, error)
ListTeams(userid string) ([]*Team, error)
CreateTeam(userid string, team *Team) (*Team, error)
UpdateTeam(userid string, team *Team) (*Team, error)
DeleteTeam(userid string, teamid string) error
FindUserTeam(userid string, teamid string) (*UserTeam, error)
ListUserTeams(userid string) ([]*UserTeam, error)
CreateUserTeam(userid string, team *UserTeam) (*UserTeam, error)
UpdateUserTeam(userid string, team *UserTeam) (*UserTeam, error)
DeleteUserTeam(userid string, teamid string) error
PopulateUserWithTeams(user *User) (*UserWithTeams, error)
ListUserAddresses(userid string) ([]*UserAddress, error)
CreateUserAddress(userid string, addr *UserAddress) (*UserAddress, error)
UpdateUserAddress(userid string, addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(userid string, addrID string) error
} }
+26 -2
View File
@@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"github.com/labstack/echo/v5" "github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/user" "trankilou.fr/lassistanoque/backend/internal/service/user"
) )
@@ -15,7 +16,7 @@ func NewUserGroup(prefix string, e *echo.Group, service *user.Service, middlewar
_ = userHandler _ = userHandler
auth := e.Group(prefix, middlewares...) auth := e.Group(prefix, middlewares...)
auth.GET("/me", userHandler.Me) auth.GET("/me", userHandler.Me)
// auth.GET("/register", userHandler.Register) auth.PUT("/:id", userHandler.Update)
return auth return auth
} }
@@ -27,7 +28,30 @@ func (h UserHandler) Me(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string) userID := c.Get(auth.ContextUserIDKey).(string)
user, err := h.userService.GetUser(userID) user, err := h.userService.GetUser(userID)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusForbidden, err.Error()) return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
} }
return c.JSON(http.StatusOK, user) return c.JSON(http.StatusOK, user)
} }
func (h UserHandler) Update(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
var updUser domain.User
if err := c.Bind(&updUser); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
updUser.ID = id
isAdmin := false // TODO : check if caller is admin
if !(isAdmin || userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update user")
}
user, err := h.userService.UpdateUser(&updUser, isAdmin)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusOK, user)
}
@@ -33,7 +33,6 @@ type Session struct {
const ContextUserIDKey = "userID" const ContextUserIDKey = "userID"
const ContextEmailKey = "userEmail" const ContextEmailKey = "userEmail"
const ContextNameKey = "userName" const ContextNameKey = "userName"
const ContextAdminKey = "admin"
// Interface définie ici car c'est un port propre au cas d'usage "auth" // Interface définie ici car c'est un port propre au cas d'usage "auth"
type Authenticator interface { type Authenticator interface {
+19
View File
@@ -26,3 +26,22 @@ func (s *Service) GetUser(id string) (*domain.User, error) {
} }
return user, nil return user, nil
} }
func (s *Service) UpdateUser(updUser *domain.User, fromAdmin bool) (*domain.User, error) {
user, err := s.repo.FindUser(updUser.ID)
if err != nil {
return nil, err
}
if fromAdmin {
user.Enabled = updUser.Enabled
}
user.Email = updUser.Email
user.Firstname = updUser.Firstname
user.Lastname = updUser.Lastname
user.Theme = updUser.Theme
user.Lang = updUser.Lang
return s.repo.UpdateUser(user)
}
Binary file not shown.
+47
View File
@@ -0,0 +1,47 @@
# Canaux de commuinication
Ce sont les cananus de communication de l'agent pour l'équipe.
## Paramètre :
- Canal de communication avec l'équipe / public (ça ne peut pas être les deux)
## Réception
Je recois un message sur ce canal
- C'est le
## Emission :
La tâche de l'équipe peut utiliser ce canal.
Si le canal disparait, la tâche échoue.
# Equipe
Modèle -> par équipe ou partagé
Outils -> par équipe ou partagé
Canaux de communication -> par équipe ou partagé
Skill -> par équipe ou partagé
Base de connaissance -> par équipe ou partagé
Agent -> par équipe ou partagé
Mémoire -> par équipe ou partagé
Canaux -> par équipe ou partagé
# ORM
Gestion des transactions ou non
sur table :
- Insert
- Update sur ID ou sur filtre. avec vérification de version et mise à jour de la date_update
- Delete
- Select 1 sur ID ou sur filtre
- Select sur filtres (clause where)
Cas de
user, err := TUser.Get(id)
user, err := TUser.Select("email=$1", email)
users, err := TUser.SelectAll("admin=$1", true)
err := TUser.Delete(id)
err := TUser.DeleteAll("admin=$1", true)
user2, err := TUser.Insert(user2)
user2, err := TUser.Update(user2)
+1 -1
View File
@@ -3,9 +3,9 @@ module trankilou.fr/lassistanoque
go 1.26.5 go 1.26.5
require ( require (
gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-migrate/migrate/v4 v4.19.1 github.com/golang-migrate/migrate/v4 v4.19.1
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v5 v5.3.1 github.com/labstack/echo/v5 v5.3.1
github.com/mattn/go-sqlite3 v1.14.42 github.com/mattn/go-sqlite3 v1.14.42
+20 -7
View File
@@ -1,12 +1,14 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210109-480734046bca h1:83fRxFj6Fb/yLIyz6b3JFNINWfalX7Ia+D3WPct/oIw=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210109-480734046bca/go.mod h1:/Vk2CqBfE7O8jGSQ8H0tB7CI0paO0DAveK2WIgi1FIg=
gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e h1:M73kAt45VlJCVwbyFXrBn9oTZRPcoImlUMm0Qj96Y2U=
gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e/go.mod h1:/Vk2CqBfE7O8jGSQ8H0tB7CI0paO0DAveK2WIgi1FIg=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
@@ -15,19 +17,22 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/labstack/echo/v5 v5.3.1 h1:75maCxkQVGualckLc/5s/ihgpH1a1Dc6AuGWNVNs6bw= github.com/labstack/echo/v5 v5.3.1 h1:75maCxkQVGualckLc/5s/ihgpH1a1Dc6AuGWNVNs6bw=
github.com/labstack/echo/v5 v5.3.1/go.mod h1:4iEGNQiPPZnkfYpNR/L6fINd3NLiGWUD5+eBotFALas= github.com/labstack/echo/v5 v5.3.1/go.mod h1:4iEGNQiPPZnkfYpNR/L6fINd3NLiGWUD5+eBotFALas=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo= github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sixafter/aes-ctr-drbg v1.19.2 h1:+Bd1tS9/y/GapqgyyRjmrce6a5wUAQPusJXlAOh44yM= github.com/sixafter/aes-ctr-drbg v1.19.2 h1:+Bd1tS9/y/GapqgyyRjmrce6a5wUAQPusJXlAOh44yM=
github.com/sixafter/aes-ctr-drbg v1.19.2/go.mod h1:iOBiPPkiy5Z5cEWm2yCoqEtnkfafxNTokJLL0zOPZeQ= github.com/sixafter/aes-ctr-drbg v1.19.2/go.mod h1:iOBiPPkiy5Z5cEWm2yCoqEtnkfafxNTokJLL0zOPZeQ=
@@ -59,5 +64,13 @@ golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/sqlite v1.56.0 h1:/D8e2RfFqoy/Zc6PuC76U28zFwmI/sYx1Kjm4yEn9e0=
modernc.org/sqlite v1.56.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
turso.tech/database/tursogo v0.7.2 h1:WhsLl67w/Kw7ACEsJaTlXIb1parIgRqUcB8AbNZsBqU= turso.tech/database/tursogo v0.7.2 h1:WhsLl67w/Kw7ACEsJaTlXIb1parIgRqUcB8AbNZsBqU=
turso.tech/database/tursogo v0.7.2/go.mod h1:tmC+H2Ot+guA9bs5oOcbbmpj1mHFTa/g7QREXG5QHq0= turso.tech/database/tursogo v0.7.2/go.mod h1:tmC+H2Ot+guA9bs5oOcbbmpj1mHFTa/g7QREXG5QHq0=