From e3fd942c47d31227fd5674275f2695d9ee1cea66 Mon Sep 17 00:00:00 2001 From: Fabien Masson Date: Sat, 8 Aug 2026 23:12:37 +0200 Subject: [PATCH] first commit --- .../adapter/auth/password/password.go | 8 +- backend/internal/adapter/database/turso/db.go | 14 +- .../adapter/database/turso/file_repository.go | 82 +++----- .../migrations/000001_create_tables.down.sql | 4 + .../migrations/000001_create_tables.up.sql | 159 ++++++++++----- .../migrations/000002_insert_system.up.sql | 4 +- .../turso/migrations/000003_files.down.sql | 1 - .../turso/migrations/000003_files.up.sql | 11 -- .../database/turso/settings_repository.go | 51 ++--- .../adapter/database/turso/user_repository.go | 181 +++++++++++------- backend/internal/adapter/security/jwt.go | 28 ++- backend/internal/domain/settings.go | 14 +- backend/internal/domain/user.go | 84 +++++++- backend/internal/http/handlers/user.go | 28 ++- .../internal/service/auth/authenticator.go | 1 - backend/internal/service/user/service.go | 19 ++ backend/lassistanoque.db-wal | Bin 37112 -> 0 bytes conception.md | 47 +++++ go.mod | 2 +- go.sum | 27 ++- 20 files changed, 494 insertions(+), 271 deletions(-) delete mode 100644 backend/internal/adapter/database/turso/migrations/000003_files.down.sql delete mode 100644 backend/internal/adapter/database/turso/migrations/000003_files.up.sql create mode 100644 conception.md diff --git a/backend/internal/adapter/auth/password/password.go b/backend/internal/adapter/auth/password/password.go index ded3a69..850e18b 100644 --- a/backend/internal/adapter/auth/password/password.go +++ b/backend/internal/adapter/auth/password/password.go @@ -70,13 +70,7 @@ func (s *PasswordAuthenticator) Register(ctx context.Context, registration auth. Enabled: true, } - count, err := s.userRepository.CountUsers() - if err != nil { - return nil, err - } - if count == 0 { - user.Administrator = true - } + // TODO: create team "Espace personnel" and put user in it user, err = s.userRepository.CreateUser(user) diff --git a/backend/internal/adapter/database/turso/db.go b/backend/internal/adapter/database/turso/db.go index 8e2027c..4d6303e 100644 --- a/backend/internal/adapter/database/turso/db.go +++ b/backend/internal/adapter/database/turso/db.go @@ -1,13 +1,13 @@ package turso import ( + "database/sql" "embed" "fmt" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/sqlite3" "github.com/golang-migrate/migrate/v4/source/iofs" - "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" "trankilou.fr/lassistanoque/backend/internal/config" "trankilou.fr/lassistanoque/backend/internal/domain" @@ -18,11 +18,11 @@ import ( var FS embed.FS type TursoDB struct { - DB *sqlx.DB + DB *sql.DB } func NewTursoDB(cfg *config.Config) (*TursoDB, error) { - db, err := sqlx.Connect("turso", cfg.DatabaseURL) + db, err := sql.Open("turso", cfg.DatabaseURL) if err != nil { 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 { - sqldb := db.DB.DB + sqldb := db.DB dbDriver, err := sqlite3.WithInstance(sqldb, &sqlite3.Config{}) if err != nil { return err @@ -57,13 +57,13 @@ func (db *TursoDB) Migrate() error { } func (db *TursoDB) UserRepository() domain.UserRepository { - return &TursoUserRepository{DB: db.DB} + return NewTursoUserRepository(db.DB) } func (db *TursoDB) SettingsRepository() domain.SettingsRepository { - return &TursoSettingsRepository{DB: db.DB} + return NewTursoSettingsRepository(db.DB) } func (db *TursoDB) FileRepository() domain.FileRepository { - return &TursoFileRepository{DB: db.DB} + return NewTursoFileRepository(db.DB) } diff --git a/backend/internal/adapter/database/turso/file_repository.go b/backend/internal/adapter/database/turso/file_repository.go index 427fe7c..443ce08 100644 --- a/backend/internal/adapter/database/turso/file_repository.go +++ b/backend/internal/adapter/database/turso/file_repository.go @@ -1,74 +1,47 @@ package turso import ( + "database/sql" "io" + "log" "time" - "github.com/jmoiron/sqlx" - "trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors" + "gitea.trankilou.fr/fabien/lasebuche" "trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/utility" ) 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) { - var file domain.File - 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 + return fr.FileTable.Get(id) } func (fr *TursoFileRepository) Create(file *domain.File) (*domain.File, error) { - file.ID = utility.GenID() - 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 + return fr.FileTable.Insert(file) } func (fr *TursoFileRepository) Update(file *domain.File) (*domain.File, error) { - newVersion := utility.GenID() - 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 + return fr.FileTable.Update(file) } func (fr *TursoFileRepository) Delete(id string) error { - res, err := fr.DB.Exec("delete from files where id=$1", id) - if count, _ := res.RowsAffected(); count == 0 { - return dberrors.ErrNoRowUpdated - } - return err + return fr.FileTable.Delete(id) } 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, } - _, err = fr.DB.NamedExec( - `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 + return fr.FileTable.Insert(file) } func (fr *TursoFileRepository) Download(id string) (*domain.File, error) { - var file domain.File - err := fr.DB.Get(&file, "select * from files where id=$1", id) - return &file, err + return fr.FileTable.Get(id) } diff --git a/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql b/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql index e0659d8..6c17ca8 100644 --- a/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql +++ b/backend/internal/adapter/database/turso/migrations/000001_create_tables.down.sql @@ -4,7 +4,11 @@ drop table system; drop table oidc; drop table users; drop table user_addresses; +drop table teams; +drop table user_teams; drop table channels; drop table tools; drop table tasks; drop table history; +drop table files; +drop table share; diff --git a/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql b/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql index fe76ff6..2980d0b 100644 --- a/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql +++ b/backend/internal/adapter/database/turso/migrations/000001_create_tables.up.sql @@ -1,36 +1,46 @@ create table providers ( id text not null primary key, - name text, + team_id text not null, + name text not null, key text, url text, - _version text + _date_created numeric not null default current_timestamp, + _date_updated numeric, + _version text not null ); create table models ( id text not null primary key, - provider_id text, - name text, - modelname text, - configuration text, - _version text + team_id text not null, + provider_id text not null, + name text not null default '', + modelname text not null default '', + configuration text not null default '{}', + _date_created numeric not null default current_timestamp, + _date_updated numeric, + _version text not null ); create table settings ( - chat_model_id text, - default_lang text, - register_enabled numeric, - password_enabled numeric, - _version text + id text not null primary key, + default_lang text not null default 'en', + register_enabled numeric not null default 1, + password_enabled numeric not null default 1, + _date_created numeric not null default current_timestamp, + _date_updated numeric, + _version text not null ); create table oidc ( id text not null primary key, - label text, - domain text, - client_id text, - client_secret text, - wellknown_url text, - _version text + label text not null, + domain text not null, + client_id text not null, + client_secret text not null, + wellknown_url text not null, + _date_created numeric not null default current_timestamp, + _date_updated numeric, + _version text not null ); create table users ( @@ -41,55 +51,112 @@ create table users ( email text not null unique, picture text not null default '', 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 ); create table user_addresses ( id text not null primary key, - user_id text, - type text, - address text, - _version text + user_id text not null, + type text not null, + address text not null, + _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 ( id text not null primary key, - name text, - type text, - enabled numeric, - configuration text, - _version text + team_id text not null, + name text not null, + type text not null, + enabled numeric not null default 1, + configuration text not null default '{}', + _date_created numeric not null default current_timestamp, + _date_updated numeric, + _version text not null ); create table tools ( id text not null primary key, - name text, - type text, - enabled numeric, - configuration text, - _version text + team_id text not null, + name text not null, + type text not null, + enabled numeric not null default 1, + configuration text not null default '{}', + _date_created numeric not null default current_timestamp, + _date_updated numeric, + _version text not null ); create table tasks ( id text not null primary key, - owner_id text, - model_id text, - label text, - prompt text, - cron text, - status text, + team_id text not null, + model_id text not null, + label text not null, + prompt text not null, + cron text not null, + status text not null, next_datetime numeric, - _version text + _date_created numeric not null default current_timestamp, + _date_updated numeric, + _version text not null ); create table history ( id text not null primary key, - task_id text, - start_datetimle numeric, + task_id text not null, + start_datetime numeric, end_datetime numeric, - prompt text, - log text, - response text, - _version text + prompt text not null, + log text not null, + response text not null, + _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 ); diff --git a/backend/internal/adapter/database/turso/migrations/000002_insert_system.up.sql b/backend/internal/adapter/database/turso/migrations/000002_insert_system.up.sql index 79fc8c7..6828282 100644 --- a/backend/internal/adapter/database/turso/migrations/000002_insert_system.up.sql +++ b/backend/internal/adapter/database/turso/migrations/000002_insert_system.up.sql @@ -1,2 +1,2 @@ -insert into settings (default_lang, register_enabled, password_enabled, _version) -values ('en', true, true, 'init'); +insert into settings (id, default_lang, register_enabled, password_enabled, _version) +values ('settings', 'en', true, true, 'init'); diff --git a/backend/internal/adapter/database/turso/migrations/000003_files.down.sql b/backend/internal/adapter/database/turso/migrations/000003_files.down.sql deleted file mode 100644 index cdf230b..0000000 --- a/backend/internal/adapter/database/turso/migrations/000003_files.down.sql +++ /dev/null @@ -1 +0,0 @@ -drop table files; diff --git a/backend/internal/adapter/database/turso/migrations/000003_files.up.sql b/backend/internal/adapter/database/turso/migrations/000003_files.up.sql deleted file mode 100644 index 7ef16fb..0000000 --- a/backend/internal/adapter/database/turso/migrations/000003_files.up.sql +++ /dev/null @@ -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 -); diff --git a/backend/internal/adapter/database/turso/settings_repository.go b/backend/internal/adapter/database/turso/settings_repository.go index 41aadcb..39e4918 100644 --- a/backend/internal/adapter/database/turso/settings_repository.go +++ b/backend/internal/adapter/database/turso/settings_repository.go @@ -1,45 +1,34 @@ package turso import ( - "github.com/jmoiron/sqlx" - "trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors" + "database/sql" + "log" + + "gitea.trankilou.fr/fabien/lasebuche" "trankilou.fr/lassistanoque/backend/internal/domain" - "trankilou.fr/lassistanoque/backend/internal/utility" ) 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) { - var settings domain.Settings - err := sr.DB.Get(&settings, "select * from settings limit 1") - return &settings, err + return sr.SettingsTable.Get("settings") } func (sr *TursoSettingsRepository) UpdateSettings(setting *domain.Settings) (*domain.Settings, error) { - newVersion := utility.GenID() - - 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 + return sr.SettingsTable.Update(setting) } diff --git a/backend/internal/adapter/database/turso/user_repository.go b/backend/internal/adapter/database/turso/user_repository.go index 665f44c..baa8832 100644 --- a/backend/internal/adapter/database/turso/user_repository.go +++ b/backend/internal/adapter/database/turso/user_repository.go @@ -1,90 +1,143 @@ package turso import ( - "github.com/jmoiron/sqlx" - "trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors" + "database/sql" + "log" + "trankilou.fr/lassistanoque/backend/internal/domain" - "trankilou.fr/lassistanoque/backend/internal/utility" + + "gitea.trankilou.fr/fabien/lasebuche" ) 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) { - var user domain.User - err := ur.DB.Get(&user, "select * from users where id=$1", id) - return &user, err + return ur.UserTable.Get(id) } func (ur *TursoUserRepository) FindUserByEmail(email string) (*domain.User, error) { - var user domain.User - 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 + return ur.UserTable.SelectOne("email=$1", email) } func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) { - var users []*domain.User - err := ur.DB.Select(&users, "select * from users order by lastname, firstname") - return users, err + return ur.UserTable.SelectWhere("") } func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) { - - 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 + return ur.UserTable.Insert(user) } func (ur *TursoUserRepository) UpdateUser(user *domain.User) (*domain.User, error) { - - 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 + return ur.UserTable.Update(user) } func (ur *TursoUserRepository) DeleteUser(id string) error { - res, err := ur.DB.Exec("delete from users where id=$1", id) - if count, _ := res.RowsAffected(); count == 0 { - return dberrors.ErrNoRowUpdated - } - return err + return ur.UserTable.Delete(id) +} + +func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.Team, error) { + 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) } diff --git a/backend/internal/adapter/security/jwt.go b/backend/internal/adapter/security/jwt.go index dca8e78..16d2d89 100644 --- a/backend/internal/adapter/security/jwt.go +++ b/backend/internal/adapter/security/jwt.go @@ -15,11 +15,10 @@ import ( // Claims personnalisées embarquées dans le JWT. type Claims struct { - UserID string `json:"uid"` - Email string `json:"email"` - Firstname string `json:"firstname"` - Lastname string `json:"lastname"` - Administrator bool `json:"administrator"` + UserID string `json:"uid"` + Email string `json:"email"` + Firstname string `json:"firstname"` + Lastname string `json:"lastname"` 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) { expiresAt := time.Now().Add(tm.accessTTL) claims := Claims{ - UserID: user.ID, - Email: user.Email, - Firstname: user.Firstname, - Lastname: user.Lastname, - Administrator: user.Administrator, + UserID: user.ID, + Email: user.Email, + Firstname: user.Firstname, + Lastname: user.Lastname, RegisteredClaims: jwt.RegisteredClaims{ Issuer: tm.issuer, Subject: user.ID, @@ -93,11 +91,10 @@ func (tm *JwtTokenManager) ParseAndValidate(tokenString string) (*domain.User, e return nil, errors.New("token invalide") } return &domain.User{ - ID: claims.UserID, - Email: claims.Email, - Firstname: claims.Firstname, - Lastname: claims.Lastname, - Administrator: claims.Administrator, + ID: claims.UserID, + Email: claims.Email, + Firstname: claims.Firstname, + Lastname: claims.Lastname, }, nil } @@ -122,7 +119,6 @@ func (tm *JwtTokenManager) TokenMiddleware(next echo.HandlerFunc) echo.HandlerFu c.Set(auth.ContextUserIDKey, claims.ID) c.Set(auth.ContextEmailKey, claims.Email) c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname) - c.Set(auth.ContextAdminKey, claims.Administrator) return next(c) } diff --git a/backend/internal/domain/settings.go b/backend/internal/domain/settings.go index 253a99e..ecfc611 100644 --- a/backend/internal/domain/settings.go +++ b/backend/internal/domain/settings.go @@ -1,13 +1,15 @@ package domain -import "database/sql" +import "time" type Settings struct { - ChatModelID sql.NullString `db:"chat_model_id"` - DefaultLang string `db:"default_lang"` - RegisterEnabled bool `db:"register_enabled"` - PasswordEnabled bool `db:"password_enabled"` - VersionId string `db:"_version"` + ID string `db:"id" json:"id"` + DefaultLang string `db:"default_lang"` + RegisterEnabled bool `db:"register_enabled"` + PasswordEnabled bool `db:"password_enabled"` + 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 { diff --git a/backend/internal/domain/user.go b/backend/internal/domain/user.go index 549b265..4abeb58 100644 --- a/backend/internal/domain/user.go +++ b/backend/internal/domain/user.go @@ -1,23 +1,87 @@ package domain +import ( + "time" +) + type User struct { - ID string `db:"id" json:"id"` - Firstname string `db:"firstname" json:"firstname,omitempty"` - Lastname string `db:"lastname" json:"lastname,omitempty"` - Password string `db:"password" json:"-"` - Email string `db:"email" json:"email,omitempty"` - PictureID string `db:"picture" json:"picture,omitempty"` - Enabled bool `db:"enabled" json:"enabled"` - Administrator bool `db:"administrator" json:"administrator"` - VersionId string `db:"_version" json:"-"` + ID string `db:"id" json:"id"` + Firstname string `db:"firstname" json:"firstname,omitempty"` + Lastname string `db:"lastname" json:"lastname,omitempty"` + Password string `db:"password" json:"-"` + Email string `db:"email" json:"email,omitempty"` + PictureID string `db:"picture" json:"picture,omitempty"` + Enabled bool `db:"enabled" json:"enabled"` + Theme string `db:"theme" json:"theme"` + 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 { - CountUsers() (int, error) FindUser(id string) (*User, error) FindUserByEmail(email string) (*User, error) ListUsers() ([]*User, error) CreateUser(user *User) (*User, error) UpdateUser(user *User) (*User, 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 } diff --git a/backend/internal/http/handlers/user.go b/backend/internal/http/handlers/user.go index ef507e4..f4f21d9 100644 --- a/backend/internal/http/handlers/user.go +++ b/backend/internal/http/handlers/user.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/labstack/echo/v5" + "trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/user" ) @@ -15,7 +16,7 @@ func NewUserGroup(prefix string, e *echo.Group, service *user.Service, middlewar _ = userHandler auth := e.Group(prefix, middlewares...) auth.GET("/me", userHandler.Me) - // auth.GET("/register", userHandler.Register) + auth.PUT("/:id", userHandler.Update) return auth } @@ -27,7 +28,30 @@ func (h UserHandler) Me(c *echo.Context) error { userID := c.Get(auth.ContextUserIDKey).(string) user, err := h.userService.GetUser(userID) if err != nil { - return echo.NewHTTPError(http.StatusForbidden, err.Error()) + return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) } 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) +} diff --git a/backend/internal/service/auth/authenticator.go b/backend/internal/service/auth/authenticator.go index 2c49837..844150d 100644 --- a/backend/internal/service/auth/authenticator.go +++ b/backend/internal/service/auth/authenticator.go @@ -33,7 +33,6 @@ type Session struct { const ContextUserIDKey = "userID" const ContextEmailKey = "userEmail" const ContextNameKey = "userName" -const ContextAdminKey = "admin" // Interface définie ici car c'est un port propre au cas d'usage "auth" type Authenticator interface { diff --git a/backend/internal/service/user/service.go b/backend/internal/service/user/service.go index aa64eab..707e1ce 100644 --- a/backend/internal/service/user/service.go +++ b/backend/internal/service/user/service.go @@ -26,3 +26,22 @@ func (s *Service) GetUser(id string) (*domain.User, error) { } 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) + +} diff --git a/backend/lassistanoque.db-wal b/backend/lassistanoque.db-wal index bd6e9d6e871aa410547ee0b89c7ce920929a7a64..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 GIT binary patch literal 0 HcmV?d00001 literal 37112 zcmeI4UrgIZ9LEy~2?0W&l$wkpZY>f-O-uO?Lv&3cY_t#vBrR0xEGM}n2FDJz1Eu>@ zC0eIFR#RW5ZO@aY?P)KY`cUn?uhrfrwbI^JshTux+9q}9bL_vAfQq?fkP2o2|vBZQoAb>vcbNKKNI-^yuB2&$7f+Q)wT(`}B{ueh5AA z5To7uw~hS41_Xcr5C8%|00;m9AOHk_01yBIKmZ85b_5*uTD!ZJ$kJXuW4++S(=VP( zhX!UFh|%u-$435O0|Gz*2mk>f00e*l5C8%|00;m9AOHkjKLU=@x`17iW~>WLd~)m0 zpI6)0RwDDficw0Evz&^C$gd4*YHXJgB1N_GKJqd)FWGUteEHYKl7ZG|WbT_@_-NHe zUfORGIvh7`-UnW$`F`^)&m+%gO}{r?YHDwI*l@oAyMJne`cl9lFs<{a(My z#RW`_^sQrhITVbBP&9aDK7^8@t(c-pGMC0|j#D#LYBP#yEj#4INsj1G$e1KaB<8bN z<`ZQJMiZxF%gdo~l#R|UgjS-#g{1(ZAI#^pq{;^`n$l2Qkm3QvZedyBCDGb?|M^o+ zCfIq9S2Db&Xonpr?J$L_?JlOX(_Y-5o++72$tjC>N=E7*MU{CmU6yC@kjg0=EODY< zC_qw9R#{D-%4Y>^zzHBMCJl3iH785ioN9hrkkaP9jFp@uD%dnwAo%?!t3{;NEE174 zX)j!BqY+umQ{B`^-%eUIQ`6y!rEL+*m6Eek5(||Efs13oZ1ygR37NRDf|ikB)AoA$h01N3vYT{jNflBd`Kl)bFD|s7P;2E!$s5b72Ve2&Qkjh z@SDG>TKs7hE9@XmJpRVRi&6wiQ}ITFK5KYP%Y&COHfz&| zz#XAbj#HFvNj65f{cFNmjyHOIxkM~H7m0-eD90z%ylf0)<^0OF?CL_u$crv(HE_u+ zFOuP$bSOzS##FP+Y;b;s6tZ+L^DO>ex09J{KPVt2KAB(#wD*R>K)s7;Z?_ji)CHPN zd$}zR&@6|Cmvsb8N(4`N7)S`bHfdKxU&*3iGNqO$gKaDb8zLEQ*}QDP-KElB=yK^} zf*v*sR#h@rk-nv;^_I39R@6sE2bDF)ZS4YtOwBmO_vwT{gtd!bP zll0Du=E5_fRWvtC`%`FjZY8>ctTmv;FxpcK>MIrUuiITrKRJ0VMX;yf9z%@<$)0Sp zVwC4ab#$1-EibD(C{E^ZKlyS&?lCB@3q0#%JwHBwOz$Ho-Fk3LzkfipfdCKy0zd!= z00AHX1b_e#00KY&2mpa25YYbjZ{9~R^`!lL)^SA$J?pUdLRGeNsmH#OKZ8%j? 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) diff --git a/go.mod b/go.mod index b7becbe..62871fa 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module trankilou.fr/lassistanoque go 1.26.5 require ( + gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210632-98bd52d7c29e github.com/golang-jwt/jwt/v5 v5.3.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/labstack/echo/v5 v5.3.1 github.com/mattn/go-sqlite3 v1.14.42 diff --git a/go.sum b/go.sum index 9507bf1..95ec3e8 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,14 @@ -filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= -filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gitea.trankilou.fr/fabien/lasebuche v0.0.0-20260808210109-480734046bca h1:83fRxFj6Fb/yLIyz6b3JFNINWfalX7Ia+D3WPct/oIw= +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/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/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/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/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= 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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 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/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/go.mod h1:4iEGNQiPPZnkfYpNR/L6fINd3NLiGWUD5+eBotFALas= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 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/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/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/sixafter/aes-ctr-drbg v1.19.2 h1:+Bd1tS9/y/GapqgyyRjmrce6a5wUAQPusJXlAOh44yM= 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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 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/go.mod h1:tmC+H2Ot+guA9bs5oOcbbmpj1mHFTa/g7QREXG5QHq0=