Compare commits

..
4 Commits
Author SHA1 Message Date
fabien 4d72952b2c gestion des providers 2026-08-31 21:23:35 +02:00
fabien 357bf9b41c choix de la timezone dans un select 2026-08-23 13:33:40 +02:00
fabien 21e96702a0 add personal address en mieux 2026-08-19 00:08:58 +02:00
fabien d2a153536f add personal address 2026-08-18 23:57:08 +02:00
58 changed files with 2038 additions and 179 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.env
+5
View File
@@ -8,9 +8,11 @@ import (
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password" "trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
"trankilou.fr/lassistanoque/backend/internal/adapter/database" "trankilou.fr/lassistanoque/backend/internal/adapter/database"
"trankilou.fr/lassistanoque/backend/internal/adapter/file" "trankilou.fr/lassistanoque/backend/internal/adapter/file"
"trankilou.fr/lassistanoque/backend/internal/adapter/llm"
"trankilou.fr/lassistanoque/backend/internal/adapter/security" "trankilou.fr/lassistanoque/backend/internal/adapter/security"
"trankilou.fr/lassistanoque/backend/internal/http" "trankilou.fr/lassistanoque/backend/internal/http"
"trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
"trankilou.fr/lassistanoque/backend/internal/service/storage" "trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/user" "trankilou.fr/lassistanoque/backend/internal/service/user"
) )
@@ -56,6 +58,7 @@ func runServe() {
// Adapters // Adapters
tokenManager := security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque") tokenManager := security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
pwdAuth := password.NewPasswordAuthenticator(tokenManager, db.UserRepository()) pwdAuth := password.NewPasswordAuthenticator(tokenManager, db.UserRepository())
llmEngine := llm.NewAnyLLMEngine()
// services // services
authService := auth.NewService( authService := auth.NewService(
@@ -67,6 +70,7 @@ func runServe() {
) )
userService := user.NewService(db.UserRepository()) userService := user.NewService(db.UserRepository())
storageService := storage.NewService(store) storageService := storage.NewService(store)
providerService := provider.NewService(db.ProviderRepository(), db.UserRepository(), llmEngine)
// http server // http server
router := http.NewRouter(http.Dependencies{ router := http.NewRouter(http.Dependencies{
@@ -74,6 +78,7 @@ func runServe() {
AuthService: authService, AuthService: authService,
UserService: userService, UserService: userService,
TokenManager: tokenManager, TokenManager: tokenManager,
ProviderService: providerService,
}) })
if err := router.Start(); err != nil { if err := router.Start(); err != nil {
@@ -12,6 +12,7 @@ type Database interface {
SettingsRepository() domain.SettingsRepository SettingsRepository() domain.SettingsRepository
UserRepository() domain.UserRepository UserRepository() domain.UserRepository
FileRepository() domain.FileRepository FileRepository() domain.FileRepository
ProviderRepository() domain.ProviderRepository
Migrate() error Migrate() error
Close() Close()
} }
@@ -67,3 +67,7 @@ func (db *TursoDB) SettingsRepository() domain.SettingsRepository {
func (db *TursoDB) FileRepository() domain.FileRepository { func (db *TursoDB) FileRepository() domain.FileRepository {
return NewTursoFileRepository(db.DB) return NewTursoFileRepository(db.DB)
} }
func (db *TursoDB) ProviderRepository() domain.ProviderRepository {
return NewTursoModelRepository(db.DB)
}
@@ -59,7 +59,6 @@ func (fr *TursoFileRepository) Upload(reader io.Reader, path string, name string
ContentType: contentType, ContentType: contentType,
VersionId: utility.GenID(), VersionId: utility.GenID(),
DateCreated: time.Now(), DateCreated: time.Now(),
DateUpdated: time.Now(),
Content: bytes, Content: bytes,
} }
@@ -11,4 +11,3 @@ drop table tools;
drop table tasks; drop table tasks;
drop table history; drop table history;
drop table files; drop table files;
drop table share;
@@ -1,25 +1,16 @@
create table providers ( create table providers (
id text not null primary key, id text not null primary key,
team_id text not null, team_id text not null,
type text not null,
name text not null, name text not null,
key text, key text,
url text, url text,
models text,
_date_created numeric not null default current_timestamp, _date_created numeric not null default current_timestamp,
_date_updated numeric, _date_updated numeric,
_version text not null _version text not null
); );
create table models (
id text not null primary key,
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 ( create table settings (
id text not null primary key, id text not null primary key,
@@ -52,7 +43,8 @@ create table users (
picture text not null default '', picture text not null default '',
enabled numeric default true, enabled numeric default true,
theme text default 'auto', theme text default 'auto',
lang text default 'en', lang text default 'fr',
timezone text default 'Europe/Paris',
_date_created numeric not null default current_timestamp, _date_created numeric not null default current_timestamp,
_date_updated numeric, _date_updated numeric,
_version text not null _version text not null
@@ -149,14 +141,3 @@ create table files (
_date_updated numeric , _date_updated numeric ,
_version text not null _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
);
@@ -0,0 +1,61 @@
package turso
import (
"database/sql"
"log"
"gitea.trankilou.fr/fabien/lasebuche"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type TursoModelRepository struct {
db *sql.DB
providerTable lasebuche.Table[domain.Provider]
}
func NewTursoModelRepository(db *sql.DB) *TursoModelRepository {
dialect := lasebuche.NewSqliteDialect()
providerTable, err := lasebuche.NewTable[domain.Provider](db, dialect, "providers")
if err != nil {
log.Fatalf("error creating lasebuche team table")
}
return &TursoModelRepository{
db: db,
providerTable: providerTable,
}
}
func (r *TursoModelRepository) ListProviders(userID string, teamID string) ([]*domain.Provider, error) {
return r.providerTable.SelectWhere(
"team_id=$1 and team_id in (select team_id from user_teams where user_id=$2)",
teamID,
userID,
)
}
func (r *TursoModelRepository) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) {
return r.providerTable.SelectOne(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
func (r *TursoModelRepository) CreateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
return r.providerTable.Insert(provider)
}
func (r *TursoModelRepository) UpdateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
return r.providerTable.Update(provider)
}
func (r *TursoModelRepository) DeleteProvider(userID string, teamID string, id string) error {
return r.providerTable.DeleteWhere(
"id=$1 and team_id=$2 and team_id in (select team_id from user_teams where user_id=$3)",
id,
teamID,
userID,
)
}
@@ -73,7 +73,7 @@ func (ur *TursoUserRepository) FindTeam(userid string, teamid string) (*domain.T
} }
func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) { func (ur *TursoUserRepository) ListTeams(userid string) ([]*domain.Team, error) {
return ur.TeamTable.SelectWhere("user_id=$1", userid) return ur.TeamTable.SelectWhere("id in (select team_id from user_teams where user_id=$1)", userid)
} }
func (ur *TursoUserRepository) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) { func (ur *TursoUserRepository) CreateTeam(userid string, team *domain.Team) (*domain.Team, error) {
@@ -123,21 +123,20 @@ func (ur *TursoUserRepository) DeleteUserTeam(userid string, teamid string) erro
return ur.UserTeamTable.Delete(teamid) 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) { func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddress, error) {
return ur.AddressTable.SelectWhere("user_id=$1", id) return ur.AddressTable.SelectWhere("user_id=$1", id)
} }
func (ur *TursoUserRepository) CreateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) { func (ur *TursoUserRepository) GetUserAddress(addrID string) (*domain.UserAddress, error) {
addr.UserID = userid return ur.AddressTable.Get(addrID)
}
func (ur *TursoUserRepository) CreateUserAddress(addr *domain.UserAddress) (*domain.UserAddress, error) {
return ur.AddressTable.Insert(addr) return ur.AddressTable.Insert(addr)
} }
func (ur *TursoUserRepository) UpdateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) { func (ur *TursoUserRepository) UpdateUserAddress(addr *domain.UserAddress) (*domain.UserAddress, error) {
return ur.AddressTable.Update(addr) return ur.AddressTable.Update(addr)
} }
func (ur *TursoUserRepository) DeleteUserAddress(userid string, addrID string) error { func (ur *TursoUserRepository) DeleteUserAddress(addrID string) error {
return ur.AddressTable.DeleteWhere("id=$1 and user_id=$2", addrID, userid) return ur.AddressTable.DeleteWhere("id=$1", addrID)
} }
+76
View File
@@ -0,0 +1,76 @@
package llm
import (
"context"
"fmt"
anyllm "github.com/mozilla-ai/any-llm-go"
"github.com/mozilla-ai/any-llm-go/providers/anthropic"
"github.com/mozilla-ai/any-llm-go/providers/ollama"
"github.com/mozilla-ai/any-llm-go/providers/openai"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
var providerTypes = []domain.Item{
{ID: "anthropic", Text: "Anthropic"},
{ID: "openai", Text: "OpenAI"},
{ID: "ollama", Text: "Ollama"},
{ID: "openaicomp", Text: "OpenAI compatible"},
{ID: "openrouter", Text: "Openrouter"},
}
type AnyLLMEngine struct {
}
func NewAnyLLMEngine() *AnyLLMEngine {
return &AnyLLMEngine{}
}
func (e *AnyLLMEngine) ListProviderTypes() []domain.Item {
return providerTypes
}
func providerFactory(provider *domain.Provider) (anyllm.Provider, error) {
switch provider.Type {
case "ollama":
return ollama.New(
anyllm.WithBaseURL(provider.URL),
)
case "openai":
return openai.New(
anyllm.WithAPIKey(provider.APIKey),
)
case "openaicomp":
return openai.New(
anyllm.WithBaseURL(provider.URL),
anyllm.WithAPIKey(provider.APIKey),
)
case "anthropic":
return anthropic.New(
anyllm.WithAPIKey(provider.APIKey),
)
}
return nil, fmt.Errorf("unknown provider type: %s", provider.Type)
}
func (e *AnyLLMEngine) ListModelsFromProvider(ctx context.Context, provider *domain.Provider) ([]string, error) {
models := make([]string, 0)
prov, err := providerFactory(provider)
if err != nil {
return nil, err
}
if lister, ok := prov.(anyllm.ModelLister); ok {
response, err := lister.ListModels(ctx)
if err != nil {
return nil, err
}
for _, m := range response.Data {
models = append(models, m.ID)
}
} else {
models = append(models, "Default")
}
return models, nil
}
+2 -2
View File
@@ -117,8 +117,8 @@ func (tm *JwtTokenManager) TokenMiddleware(next echo.HandlerFunc) echo.HandlerFu
// Rend les infos disponibles aux handlers suivants via c.Get(...) // Rend les infos disponibles aux handlers suivants via c.Get(...)
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)
return next(c) return next(c)
} }
+1 -1
View File
@@ -13,7 +13,7 @@ type File struct {
StorageFilename string `db:"storage_filename" json:"-"` StorageFilename string `db:"storage_filename" json:"-"`
Content []byte `db:"content" json:"-"` Content []byte `db:"content" json:"-"`
DateCreated time.Time `db:"date_created" json:"-"` DateCreated time.Time `db:"date_created" json:"-"`
DateUpdated time.Time `db:"date_updated" json:"-"` DateUpdated *time.Time `db:"date_updated" json:"-"`
VersionId string `db:"_version" json:"-"` VersionId string `db:"_version" json:"-"`
} }
+26
View File
@@ -0,0 +1,26 @@
package domain
import "context"
type LLMModelDetail struct {
ParentModel string `json:"parent_model"`
Format string `json:"format"`
Family string `json:"family"`
Families []string `json:"families"`
ParameterSize string `json:"parameter_size"`
QuantizationLevel string `json:"quantization_level"`
}
type LLMModel struct {
Name string `json:"name"`
Model string `json:"model"`
ModifiedAt int64 `json:"modified_at"`
Size int64 `json:"size"`
Digest string `json:"digest"`
Detail *LLMModelDetail `json:"detail"`
}
type LLMEngine interface {
ListProviderTypes() []Item
ListModelsFromProvider(ctx context.Context, provider *Provider) ([]string, error)
}
+24
View File
@@ -0,0 +1,24 @@
package domain
import "time"
type Provider struct {
ID string `db:"id" json:"id"`
TeamID string `db:"team_id" json:"teamId"`
Type string `db:"type" json:"type"`
Name string `db:"name" json:"name"`
APIKey string `db:"key" json:"apiKey"`
URL string `db:"url" json:"baseUrl"`
Models string `db:"models" json:"models"`
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 ProviderRepository interface {
ListProviders(userID string, teamID string) ([]*Provider, error)
GetProvider(userID string, teamID string, id string) (*Provider, error)
CreateProvider(userID string, provider *Provider) (*Provider, error)
UpdateProvider(userID string, provider *Provider) (*Provider, error)
DeleteProvider(userID string, teamID string, id string) error
}
+6
View File
@@ -0,0 +1,6 @@
package domain
type Item struct {
ID string `json:"id"`
Text string `json:"text"`
}
+1 -1
View File
@@ -7,7 +7,7 @@ type Settings struct {
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"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
+28 -18
View File
@@ -14,27 +14,28 @@ type User struct {
Enabled bool `db:"enabled" json:"enabled"` Enabled bool `db:"enabled" json:"enabled"`
Theme string `db:"theme" json:"theme"` Theme string `db:"theme" json:"theme"`
Lang string `db:"lang" json:"lang"` Lang string `db:"lang" json:"lang"`
DateCreated time.Time `db:"_date_created" json:"_date_created"` Timezone string `db:"timezone" json:"timezone"`
DateUpdated time.Time `db:"_date_updated" json:"_date_updated"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
type Team struct { type Team struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"` Label string `db:"label" json:"label"`
DefaultModelID *string `db:"default_model_id" json:"default_model_id"` DefaultModelID *string `db:"default_model_id" json:"defaultModelId"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
type UserTeam struct { type UserTeam struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"` UserID string `db:"user_id" json:"userId"`
TeamID string `db:"team_id" json:"team_id"` TeamID string `db:"team_id" json:"teamId"`
Administrator bool `db:"administrator" json:"administrator"` Administrator bool `db:"administrator" json:"administrator"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
@@ -42,9 +43,9 @@ type UserAddress struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"` UserID string `db:"user_id" json:"user_id"`
Type string `db:"type" json:"type"` Type string `db:"type" json:"type"`
Address string `db:"address_id" json:"address"` Address string `db:"address" json:"address"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
@@ -55,7 +56,17 @@ type TeamRelation struct {
type UserWithTeams struct { type UserWithTeams struct {
User *User `json:"user"` User *User `json:"user"`
Teams []*TeamRelation `json:"teams"` UserTeams []*UserTeam `json:"user_teams"`
Teams []*Team `json:"teams"`
}
func AddressTypes() []string {
return []string{
"email",
"telegram",
"whatsapp",
"signal",
}
} }
type UserRepository interface { type UserRepository interface {
@@ -78,10 +89,9 @@ type UserRepository interface {
UpdateUserTeam(userid string, team *UserTeam) (*UserTeam, error) UpdateUserTeam(userid string, team *UserTeam) (*UserTeam, error)
DeleteUserTeam(userid string, teamid string) error DeleteUserTeam(userid string, teamid string) error
PopulateUserWithTeams(user *User) (*UserWithTeams, error)
ListUserAddresses(userid string) ([]*UserAddress, error) ListUserAddresses(userid string) ([]*UserAddress, error)
CreateUserAddress(userid string, addr *UserAddress) (*UserAddress, error) GetUserAddress(addrID string) (*UserAddress, error)
UpdateUserAddress(userid string, addr *UserAddress) (*UserAddress, error) CreateUserAddress(addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(userid string, addrID string) error UpdateUserAddress(addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(addrID string) error
} }
+623
View File
@@ -0,0 +1,623 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
)
type MiscHandler struct {
}
func NewMiscGroup(prefix string, e *echo.Group, middlewares ...echo.MiddlewareFunc) *echo.Group {
miscHandler := &MiscHandler{}
misc := e.Group(prefix, middlewares...)
misc.GET("/timezones", miscHandler.Timezones)
return misc
}
func (h MiscHandler) Timezones(c *echo.Context) error {
tz := []string{
"Africa/Abidjan",
"Africa/Accra",
"Africa/Addis_Ababa",
"Africa/Algiers",
"Africa/Asmara",
"Africa/Asmera",
"Africa/Bamako",
"Africa/Bangui",
"Africa/Banjul",
"Africa/Bissau",
"Africa/Blantyre",
"Africa/Brazzaville",
"Africa/Bujumbura",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/Conakry",
"Africa/Dakar",
"Africa/Dar_es_Salaam",
"Africa/Djibouti",
"Africa/Douala",
"Africa/El_Aaiun",
"Africa/Freetown",
"Africa/Gaborone",
"Africa/Harare",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Kampala",
"Africa/Khartoum",
"Africa/Kigali",
"Africa/Kinshasa",
"Africa/Lagos",
"Africa/Libreville",
"Africa/Lome",
"Africa/Luanda",
"Africa/Lubumbashi",
"Africa/Lusaka",
"Africa/Malabo",
"Africa/Maputo",
"Africa/Maseru",
"Africa/Mbabane",
"Africa/Mogadishu",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Niamey",
"Africa/Nouakchott",
"Africa/Ouagadougou",
"Africa/Porto-Novo",
"Africa/Sao_Tome",
"Africa/Timbuktu",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Anguilla",
"America/Antigua",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/ComodRivadavia",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Aruba",
"America/Asuncion",
"America/Atikokan",
"America/Atka",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Blanc-Sablon",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Buenos_Aires",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Catamarca",
"America/Cayenne",
"America/Cayman",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Coral_Harbour",
"America/Cordoba",
"America/Costa_Rica",
"America/Coyhaique",
"America/Creston",
"America/Cuiaba",
"America/Curacao",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Dominica",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Ensenada",
"America/Fort_Nelson",
"America/Fort_Wayne",
"America/Fortaleza",
"America/Glace_Bay",
"America/Godthab",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Grenada",
"America/Guadeloupe",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Indianapolis",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Jujuy",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/Knox_IN",
"America/Kralendijk",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Louisville",
"America/Lower_Princes",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Marigot",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Mendoza",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/Montreal",
"America/Montserrat",
"America/Nassau",
"America/New_York",
"America/Nipigon",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Pangnirtung",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Port_of_Spain",
"America/Porto_Acre",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rainy_River",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Rosario",
"America/Santa_Isabel",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Shiprock",
"America/Sitka",
"America/St_Barthelemy",
"America/St_Johns",
"America/St_Kitts",
"America/St_Lucia",
"America/St_Thomas",
"America/St_Vincent",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Thunder_Bay",
"America/Tijuana",
"America/Toronto",
"America/Tortola",
"America/Vancouver",
"America/Virgin",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"America/Yellowknife",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/DumontDUrville",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/McMurdo",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/South_Pole",
"Antarctica/Syowa",
"Antarctica/Troll",
"Antarctica/Vostok",
"Arctic/Longyearbyen",
"Asia/Aden",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Ashkhabad",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Bahrain",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Brunei",
"Asia/Calcutta",
"Asia/Chita",
"Asia/Choibalsan",
"Asia/Chongqing",
"Asia/Chungking",
"Asia/Colombo",
"Asia/Dacca",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Harbin",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Istanbul",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kashgar",
"Asia/Kathmandu",
"Asia/Katmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuala_Lumpur",
"Asia/Kuching",
"Asia/Kuwait",
"Asia/Macao",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Muscat",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Phnom_Penh",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Rangoon",
"Asia/Riyadh",
"Asia/Saigon",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Tel_Aviv",
"Asia/Thimbu",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ujung_Pandang",
"Asia/Ulaanbaatar",
"Asia/Ulan_Bator",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vientiane",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faeroe",
"Atlantic/Faroe",
"Atlantic/Jan_Mayen",
"Atlantic/Madeira",
"Atlantic/Reykjavik",
"Atlantic/South_Georgia",
"Atlantic/St_Helena",
"Atlantic/Stanley",
"Australia/ACT",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Canberra",
"Australia/Currie",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/LHI",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/NSW",
"Australia/North",
"Australia/Perth",
"Australia/Queensland",
"Australia/South",
"Australia/Sydney",
"Australia/Tasmania",
"Australia/Victoria",
"Australia/West",
"Australia/Yancowinna",
"Brazil/Acre",
"Brazil/DeNoronha",
"Brazil/East",
"Brazil/West",
"CET",
"CST6CDT",
"Canada/Atlantic",
"Canada/Central",
"Canada/Eastern",
"Canada/Mountain",
"Canada/Newfoundland",
"Canada/Pacific",
"Canada/Saskatchewan",
"Canada/Yukon",
"Chile/Continental",
"Chile/EasterIsland",
"Cuba",
"EET",
"EST",
"EST5EDT",
"Egypt",
"Eire",
"Etc/GMT",
"Etc/GMT+0",
"Etc/GMT+1",
"Etc/GMT+10",
"Etc/GMT+11",
"Etc/GMT+12",
"Etc/GMT+2",
"Etc/GMT+3",
"Etc/GMT+4",
"Etc/GMT+5",
"Etc/GMT+6",
"Etc/GMT+7",
"Etc/GMT+8",
"Etc/GMT+9",
"Etc/GMT-0",
"Etc/GMT-1",
"Etc/GMT-10",
"Etc/GMT-11",
"Etc/GMT-12",
"Etc/GMT-13",
"Etc/GMT-14",
"Etc/GMT-2",
"Etc/GMT-3",
"Etc/GMT-4",
"Etc/GMT-5",
"Etc/GMT-6",
"Etc/GMT-7",
"Etc/GMT-8",
"Etc/GMT-9",
"Etc/GMT0",
"Etc/Greenwich",
"Etc/UCT",
"Etc/UTC",
"Etc/Universal",
"Etc/Zulu",
"Europe/Amsterdam",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belfast",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Bratislava",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Busingen",
"Europe/Chisinau",
"Europe/Copenhagen",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Guernsey",
"Europe/Helsinki",
"Europe/Isle_of_Man",
"Europe/Istanbul",
"Europe/Jersey",
"Europe/Kaliningrad",
"Europe/Kiev",
"Europe/Kirov",
"Europe/Kyiv",
"Europe/Lisbon",
"Europe/Ljubljana",
"Europe/London",
"Europe/Luxembourg",
"Europe/Madrid",
"Europe/Malta",
"Europe/Mariehamn",
"Europe/Minsk",
"Europe/Monaco",
"Europe/Moscow",
"Europe/Nicosia",
"Europe/Oslo",
"Europe/Paris",
"Europe/Podgorica",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/San_Marino",
"Europe/Sarajevo",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Skopje",
"Europe/Sofia",
"Europe/Stockholm",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Tiraspol",
"Europe/Ulyanovsk",
"Europe/Uzhgorod",
"Europe/Vaduz",
"Europe/Vatican",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zagreb",
"Europe/Zaporozhye",
"Europe/Zurich",
"Factory",
"GB",
"GB-Eire",
"GMT",
"GMT+0",
"GMT-0",
"GMT0",
"Greenwich",
"HST",
"Hongkong",
"Iceland",
"Indian/Antananarivo",
"Indian/Chagos",
"Indian/Christmas",
"Indian/Cocos",
"Indian/Comoro",
"Indian/Kerguelen",
"Indian/Mahe",
"Indian/Maldives",
"Indian/Mauritius",
"Indian/Mayotte",
"Indian/Reunion",
"Iran",
"Israel",
"Jamaica",
"Japan",
"Kwajalein",
"Libya",
"MET",
"MST",
"MST7MDT",
"Mexico/BajaNorte",
"Mexico/BajaSur",
"Mexico/General",
"NZ",
"NZ-CHAT",
"Navajo",
"PRC",
"PST8PDT",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Chuuk",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Enderbury",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Funafuti",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Johnston",
"Pacific/Kanton",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Majuro",
"Pacific/Marquesas",
"Pacific/Midway",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Pohnpei",
"Pacific/Ponape",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Saipan",
"Pacific/Samoa",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu",
"Pacific/Truk",
"Pacific/Wake",
"Pacific/Wallis",
"Pacific/Yap",
"Poland",
"Portugal",
"ROC",
"ROK",
"Singapore",
"Turkey",
"UCT",
"US/Alaska",
"US/Aleutian",
"US/Arizona",
"US/Central",
"US/East-Indiana",
"US/Eastern",
"US/Hawaii",
"US/Indiana-Starke",
"US/Michigan",
"US/Mountain",
"US/Pacific",
"US/Samoa",
"UTC",
"Universal",
"W-SU",
"WET",
"Zulu",
}
return c.JSON(http.StatusOK, tz)
}
+123
View File
@@ -0,0 +1,123 @@
package handlers
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/provider"
)
func NewModelGroup(prefix string, e *echo.Group, service *provider.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
modelHandler := &ModelHandler{
providerService: service,
}
model := e.Group(prefix, middlewares...)
model.GET("/providerTypes", modelHandler.ListProviderTypes)
model.GET("/:space", modelHandler.ListProviders)
model.POST("/:space/avalable-models", modelHandler.ListAvailableModels)
model.GET("/:space/:provider", modelHandler.GetProvider)
model.PUT("/:space", modelHandler.UpdateProvider)
model.POST("/:space", modelHandler.CreateProvider)
model.DELETE("/:space/:provider", modelHandler.DeleteProvider)
return model
}
type ModelHandler struct {
providerService *provider.Service
}
func (h *ModelHandler) ListProviderTypes(c *echo.Context) error {
return c.JSON(http.StatusOK, h.providerService.ListProviderTypes())
}
func (h *ModelHandler) ListProviders(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
providers, err := h.providerService.ListProviders(userID, teamID)
if err != nil {
c.Logger().Error("error listing providers: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error listing providers")
}
return c.JSON(http.StatusOK, providers)
}
func (h *ModelHandler) GetProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
providerID := c.Param("provider")
provider, err := h.providerService.GetProvider(userID, teamID, providerID)
if err != nil {
c.Logger().Error("error getting provider: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error getting provider")
}
return c.JSON(http.StatusOK, provider)
}
func (h *ModelHandler) CreateProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
updprovider, err := h.providerService.CreateProvider(userID, &provider)
if err != nil {
c.Logger().Error("error creating provider: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, updprovider)
}
func (h *ModelHandler) UpdateProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
updprovider, err := h.providerService.UpdateProvider(userID, &provider)
if err != nil {
c.Logger().Error("error updating provider: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, updprovider)
}
func (h *ModelHandler) DeleteProvider(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
teamID := c.Param("space")
providerID := c.Param("provider")
err := h.providerService.DeleteProvider(userID, teamID, providerID)
if err != nil {
c.Logger().Error("error creating provider: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, providerID)
}
func (h *ModelHandler) ListAvailableModels(c *echo.Context) error {
teamID := c.Param("space")
var provider domain.Provider
err := c.Bind(&provider)
if err != nil {
c.Logger().Error("error binding provider: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error binding provider")
}
provider.TeamID = teamID
list, err := h.providerService.ListAvailableModels(c.Request().Context(), &provider)
if err != nil {
c.Logger().Error("error listing models: %s", err)
return echo.NewHTTPError(http.StatusBadRequest, "error creating provider")
}
return c.JSON(http.StatusOK, list)
}
+79 -5
View File
@@ -13,10 +13,13 @@ func NewUserGroup(prefix string, e *echo.Group, service *user.Service, middlewar
userHandler := &UserHandler{ userHandler := &UserHandler{
userService: service, userService: service,
} }
_ = userHandler
auth := e.Group(prefix, middlewares...) auth := e.Group(prefix, middlewares...)
auth.GET("/me", userHandler.Me) auth.GET("/me", userHandler.Me)
auth.PUT("/:id", userHandler.Update) auth.PUT("/:id", userHandler.Update)
auth.GET("/:id/address", userHandler.Addresses)
auth.POST("/:id/address", userHandler.CreateAddress)
auth.PUT("/:id/address", userHandler.UpdateAddress)
auth.DELETE("/:id/address/:address", userHandler.DeleteAddress)
return auth return auth
} }
@@ -26,7 +29,7 @@ type UserHandler struct {
func (h UserHandler) Me(c *echo.Context) error { 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.GetUserWithTeams(userID)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, err.Error()) return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
} }
@@ -43,15 +46,86 @@ func (h UserHandler) Update(c *echo.Context) error {
} }
updUser.ID = id updUser.ID = id
isAdmin := false // TODO : check if caller is admin if !(userID == id) {
if !(isAdmin || userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update user") return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update user")
} }
user, err := h.userService.UpdateUser(&updUser, isAdmin) user, err := h.userService.UpdateUser(&updUser, false)
if err != nil { if err != nil {
return c.String(http.StatusBadRequest, err.Error()) return c.String(http.StatusBadRequest, err.Error())
} }
return c.JSON(http.StatusOK, user) return c.JSON(http.StatusOK, user)
} }
func (h UserHandler) Addresses(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
addresses, err := h.userService.ListAdresses(userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, addresses)
}
func (h UserHandler) CreateAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
var address domain.UserAddress
if err := c.Bind(&address); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
address.UserID = userID
newaddress, err := h.userService.AddAddress(userID, address.Type, address.Address)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, newaddress)
}
func (h UserHandler) UpdateAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
var address domain.UserAddress
if err := c.Bind(&address); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
address.UserID = userID
newaddress, err := h.userService.UpdateAddress(userID, &address)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, newaddress)
}
func (h UserHandler) DeleteAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
addressID := c.Param("address")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
err := h.userService.DeleteAddress(userID, addressID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, "deleted")
}
+4
View File
@@ -11,6 +11,7 @@ import (
"trankilou.fr/lassistanoque/backend/internal/config" "trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/http/handlers" "trankilou.fr/lassistanoque/backend/internal/http/handlers"
"trankilou.fr/lassistanoque/backend/internal/service/auth" "trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/provider"
"trankilou.fr/lassistanoque/backend/internal/service/storage" "trankilou.fr/lassistanoque/backend/internal/service/storage"
"trankilou.fr/lassistanoque/backend/internal/service/user" "trankilou.fr/lassistanoque/backend/internal/service/user"
) )
@@ -35,6 +36,7 @@ type Dependencies struct {
StorageService *storage.Service StorageService *storage.Service
AuthService *auth.Service AuthService *auth.Service
UserService *user.Service UserService *user.Service
ProviderService *provider.Service
TokenManager auth.TokenManager TokenManager auth.TokenManager
} }
@@ -70,6 +72,8 @@ func NewRouter(deps Dependencies) *Router {
api := e.Group("/api") api := e.Group("/api")
_ = handlers.NewAuthGroup("/auth", api, deps.AuthService) _ = handlers.NewAuthGroup("/auth", api, deps.AuthService)
_ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware) _ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware)
_ = handlers.NewMiscGroup("/misc", api, deps.TokenManager.TokenMiddleware)
_ = handlers.NewModelGroup("/provider", api, deps.ProviderService, deps.TokenManager.TokenMiddleware)
return &Router{ return &Router{
echo: e, echo: e,
@@ -31,8 +31,10 @@ type Session struct {
} }
const ContextUserIDKey = "userID" const ContextUserIDKey = "userID"
const ContextEmailKey = "userEmail" const ContextUserAdminKey = "userAdmin"
const ContextNameKey = "userName"
//const ContextEmailKey = "userEmail"
//const ContextNameKey = "userName"
// 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 {
+3
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
) )
@@ -53,8 +54,10 @@ func (s *Service) Register(registration Registration) (*Session, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
now := time.Now()
_, err = s.userRepository.CreateTeam(session.User.ID, &domain.Team{ _, err = s.userRepository.CreateTeam(session.User.ID, &domain.Team{
Label: "Espace personnel", Label: "Espace personnel",
DateCreated: now,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@@ -0,0 +1,76 @@
package provider
import (
"context"
"fmt"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type Service struct {
repo domain.ProviderRepository
repoUser domain.UserRepository
llmengine domain.LLMEngine
}
func NewService(
repo domain.ProviderRepository,
repoUser domain.UserRepository,
llmengine domain.LLMEngine,
) *Service {
return &Service{
repo,
repoUser,
llmengine,
}
}
func (s *Service) ListProviderTypes() []domain.Item {
return s.llmengine.ListProviderTypes()
}
func (s *Service) ListProviders(userID string, teamID string) ([]*domain.Provider, error) {
return s.repo.ListProviders(userID, teamID)
}
func (s *Service) GetProvider(userID string, teamID string, id string) (*domain.Provider, error) {
return s.repo.GetProvider(userID, teamID, id)
}
func (s *Service) CreateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
if _, err := s.repoUser.FindUserTeam(userID, provider.TeamID); err != nil {
return nil, fmt.Errorf("error finding user in team: %s", err)
}
return s.repo.CreateProvider(userID, provider)
}
func (s *Service) UpdateProvider(userID string, provider *domain.Provider) (*domain.Provider, error) {
if _, err := s.repoUser.FindUserTeam(userID, provider.TeamID); err != nil {
return nil, fmt.Errorf("error finding user in team: %s", err)
}
if _, err := s.repo.GetProvider(userID, provider.TeamID, provider.ID); err != nil {
return nil, fmt.Errorf("error finding provider: %s", err)
}
return s.repo.UpdateProvider(userID, provider)
}
func (s *Service) DeleteProvider(userID string, teamID string, id string) error {
if _, err := s.repoUser.FindUserTeam(userID, teamID); err != nil {
return fmt.Errorf("error finding user in team: %s", err)
}
if _, err := s.repo.GetProvider(userID, teamID, id); err != nil {
return fmt.Errorf("error finding provider: %s", err)
}
return s.repo.DeleteProvider(userID, teamID, id)
}
func (s *Service) ListAvailableModels(ctx context.Context, provider *domain.Provider) ([]string, error) {
return s.llmengine.ListModelsFromProvider(ctx, provider)
}
+60
View File
@@ -27,6 +27,30 @@ func (s *Service) GetUser(id string) (*domain.User, error) {
return user, nil return user, nil
} }
func (s *Service) GetUserWithTeams(id string) (*domain.UserWithTeams, error) {
u, err := s.GetUser(id)
if err != nil {
return nil, err
}
teams, err := s.repo.ListTeams(u.ID)
if err != nil {
return nil, err
}
userTeams, err := s.repo.ListUserTeams(u.ID)
if err != nil {
return nil, err
}
ut := &domain.UserWithTeams{
User: u,
UserTeams: userTeams,
Teams: teams,
}
return ut, nil
}
func (s *Service) UpdateUser(updUser *domain.User, fromAdmin bool) (*domain.User, error) { func (s *Service) UpdateUser(updUser *domain.User, fromAdmin bool) (*domain.User, error) {
user, err := s.repo.FindUser(updUser.ID) user, err := s.repo.FindUser(updUser.ID)
if err != nil { if err != nil {
@@ -41,7 +65,43 @@ func (s *Service) UpdateUser(updUser *domain.User, fromAdmin bool) (*domain.User
user.Lastname = updUser.Lastname user.Lastname = updUser.Lastname
user.Theme = updUser.Theme user.Theme = updUser.Theme
user.Lang = updUser.Lang user.Lang = updUser.Lang
user.Timezone = updUser.Timezone
return s.repo.UpdateUser(user) return s.repo.UpdateUser(user)
} }
func (s *Service) ListAdresses(userId string) ([]*domain.UserAddress, error) {
return s.repo.ListUserAddresses(userId)
}
func (s *Service) AddAddress(userId string, typ string, address string) (*domain.UserAddress, error) {
addr := &domain.UserAddress{
UserID: userId,
Type: typ,
Address: address,
}
return s.repo.CreateUserAddress(addr)
}
func (s *Service) UpdateAddress(userId string, address *domain.UserAddress) (*domain.UserAddress, error) {
old, err := s.repo.GetUserAddress(address.ID)
if err != nil {
return nil, err
}
if old.UserID != userId {
return nil, fmt.Errorf("unauthorized")
}
return s.repo.UpdateUserAddress(address)
}
func (s *Service) DeleteAddress(userId string, addressId string) error {
old, err := s.repo.GetUserAddress(addressId)
if err != nil {
return err
}
if old.UserID != userId {
return fmt.Errorf("unauthorized")
}
return s.repo.DeleteUserAddress(addressId)
}
Binary file not shown.
+53 -25
View File
@@ -8,7 +8,8 @@
"name": "web", "name": "web",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"oidc-client-ts": "^3.5.0" "oidc-client-ts": "^3.5.0",
"svelte-select": "^6.0.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
@@ -198,6 +199,31 @@
"node": "^20.19.0 || ^22.13.0 || >=24" "node": "^20.19.0 || ^22.13.0 || >=24"
} }
}, },
"node_modules/@floating-ui/core": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.8.0.tgz",
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.12"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.8.0.tgz",
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.8.0",
"@floating-ui/utils": "^0.2.12"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.12.tgz",
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
"license": "MIT"
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -268,7 +294,6 @@
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/sourcemap-codec": "^1.5.0",
@@ -279,7 +304,6 @@
"version": "2.3.5", "version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
@@ -290,7 +314,6 @@
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
@@ -300,14 +323,12 @@
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31", "version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
@@ -822,7 +843,6 @@
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==", "integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
"dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"acorn": "^8.9.0" "acorn": "^8.9.0"
@@ -1259,7 +1279,6 @@
"version": "1.0.9", "version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
@@ -1283,7 +1302,6 @@
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ws": { "node_modules/@types/ws": {
@@ -1446,7 +1464,7 @@
"version": "8.65.0", "version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==", "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1530,7 +1548,6 @@
"version": "8.18.0", "version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
@@ -1570,7 +1587,6 @@
"version": "5.3.1", "version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -1580,7 +1596,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
@@ -1613,7 +1628,6 @@
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -1706,7 +1720,6 @@
"version": "5.9.0", "version": "5.9.0",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz",
"integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==", "integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/enhanced-resolve": { "node_modules/enhanced-resolve": {
@@ -1894,7 +1907,6 @@
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/espree": { "node_modules/espree": {
@@ -1932,7 +1944,6 @@
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz",
"integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==", "integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15" "@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2164,7 +2175,6 @@
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/estree": "^1.0.6" "@types/estree": "^1.0.6"
@@ -2595,7 +2605,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/locate-path": { "node_modules/locate-path": {
@@ -2618,7 +2627,6 @@
"version": "0.30.21", "version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
@@ -2678,9 +2686,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.16", "version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3211,7 +3219,6 @@
"version": "5.56.8", "version": "5.56.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz",
"integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==", "integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@jridgewell/remapping": "^2.3.4", "@jridgewell/remapping": "^2.3.4",
@@ -3383,6 +3390,28 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/svelte-floating-ui": {
"version": "1.5.8",
"resolved": "https://registry.npmjs.org/svelte-floating-ui/-/svelte-floating-ui-1.5.8.tgz",
"integrity": "sha512-dVvJhZ2bT+kQDHlE4Lep8t+sgEc0XD96fXLzAi2DDI2bsaegBbClxXVNMma0C2WsG+n9GJSYx292dTvA8CYRtw==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.5.0",
"@floating-ui/dom": "^1.5.3"
}
},
"node_modules/svelte-select": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/svelte-select/-/svelte-select-6.0.0.tgz",
"integrity": "sha512-WJ1UDw0a1Z3I7oVFgaCS7G9D2rCSmYAPpufNEj/DNuk5Oyb2a5dju6F36o0urBVw9bTE3WkM9cZkI0x+Sfo8Bg==",
"license": "ISC",
"dependencies": {
"svelte-floating-ui": "1.5.8"
},
"peerDependencies": {
"svelte": ">=5"
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
@@ -3973,7 +4002,6 @@
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
} }
} }
+2 -1
View File
@@ -48,6 +48,7 @@
"esbuild": "^0.25.0" "esbuild": "^0.25.0"
}, },
"dependencies": { "dependencies": {
"oidc-client-ts": "^3.5.0" "oidc-client-ts": "^3.5.0",
"svelte-select": "^6.0.0"
} }
} }
@@ -0,0 +1,5 @@
import { api } from '../client';
export const miscApi = {
listTimezones: () => api.get<string[]>(`/misc/timezones`),
};
@@ -0,0 +1,13 @@
import { api } from '../client';
import type { Provider, Item } from "$lib/types/api"
import { space } from "$lib/state/space.svelte"
export const providerApi = {
providerTypes: () => api.get<Item[]>('/provider/providerTypes'),
getProviders: () => api.get<Provider[]>(`/provider/${space.id}`),
getProvider: (id: string) => api.get<Provider>(`/provider/${space.id}/${id}`),
createProvider: (data: Partial<Provider>) => api.post<Provider>(`/provider/${space.id}`, data),
updateProvider: (data: Partial<Provider>) => api.put<Provider>(`/provider/${space.id}`, data),
deleteProvider: (id: string) => api.delete(`/provider/${space.id}/${id}`),
getAvailableModels: (data: Partial<Provider>) => api.post<string[]>(`/provider/${space.id}/avalable-models`, data),
};
+7 -2
View File
@@ -1,7 +1,12 @@
import { api } from '../client'; import { api } from '../client';
import { type User } from "$lib/types/api" import type { User, Address, UserWithTeams } from "$lib/types/api"
export const userApi = { export const userApi = {
me: () => api.get<User>('/user/me'), me: () => api.get<UserWithTeams>('/user/me'),
update: (id: string, data: Partial<User>) => api.put<User>(`/user/${id}`, data), update: (id: string, data: Partial<User>) => api.put<User>(`/user/${id}`, data),
listAddresses: (id: string) => api.get<Address[]>(`/user/${id}/address`),
createAddress: (id: string, data: Partial<Address>) => api.post<Address>(`/user/${id}/address`, data),
updateAddress: (id: string, data: Partial<Address>) => api.put<Address>(`/user/${id}/address`, data),
deleteAddress: (id:string, addrid: string) => api.delete<Address[]>(`/user/${id}/address/${addrid}`),
}; };
@@ -8,6 +8,4 @@
{#if errors.has(key)} {#if errors.has(key)}
<span class="text-red-700 dark:text-red-400 text-xs">{errors.get(key)}</span> <span class="text-red-700 dark:text-red-400 text-xs">{errors.get(key)}</span>
{:else}
<span class="text-xs">&nbsp;</span>
{/if} {/if}
+27 -15
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { uiState } from "$lib/state/ui.svelte" import { uiState } from "$lib/state/ui.svelte"
import { auth } from "$lib/state/auth.svelte"
import Menu from "$lib/components/Menu.svelte" import Menu from "$lib/components/Menu.svelte"
import { logout } from "$lib/state/auth.svelte" import { logout } from "$lib/state/auth.svelte"
import logo from "$lib/assets/logo-txt.png" import logo from "$lib/assets/logo-txt.png"
import { space } from '$lib/state/space.svelte'
let {handleMenuDesktop, handleMenuMobile} = $props(); let {handleMenuDesktop, handleMenuMobile} = $props();
@@ -13,7 +13,7 @@
</script> </script>
<div class="flex flex-row italic mb-4"> <div class="flex flex-row italic mb-2">
{#if uiState.menuContentVisible} {#if uiState.menuContentVisible}
<div class="flex-1 p-3 font openonly"> <div class="flex-1 p-3 font openonly">
<img src={logo} class="h-5" alt="logo"/> <img src={logo} class="h-5" alt="logo"/>
@@ -26,26 +26,38 @@
<i class="icon-menu pointer block p-2"></i> <i class="icon-menu pointer block p-2"></i>
</button> </button>
</div> </div>
{#if uiState.menuContentVisible}
<div class="m-2">
<div class="formelement">
<select class="textinput">
{#each space.spaces as s (s.id)}
<option value={s.id}>{s.label}</option>
{/each}
</select>
</div>
</div>
{/if}
<div id="menu" class="flex-1 overflow-y-scroll"> <div id="menu" class="flex-1 overflow-y-scroll">
<Menu label="Conversation" icon="icon-chat" link="/"/> <Menu label="Nouveau chat" icon="icon-plus" link="/"/>
<Menu label="Tâches" icon="icon-tasks" link="/tasks"/> <Menu label="Conversations" icon="icon-chat" link="/history"/>
<Menu label="Planification" icon="icon-tasks" link="/tasks"/>
<Menu label="Connaissances" icon="icon-graduation-cap" link="/kb"/> <Menu label="Connaissances" icon="icon-graduation-cap" link="/kb"/>
<Menu label="Historique" icon="icon-history" link="/history"/> <Menu label="Réglages" icon="icon-cog" link="/settings"/>
{#if auth.user?.administrator}
<Menu label="Paramétrage" icon="icon-cog" link="/settings"/>
{/if}
</div> </div>
<div id="profile" class="w-full flex flex-row items-center border-t border-stone-200 dark:border-stone-800"> <div id="profile" class="w-full items-center border-t border-stone-200 dark:border-stone-800">
{#if uiState.menuContentVisible} {#if uiState.menuContentVisible}
<div class="p-4 flex-1 profile"> <div class="flex flex-row gap-2 p-2 ">
<div>Fabien Masson</div> <div class="py-1 flex-1 flex flex-row text-xs">
<div class="flex"> <div class="w-6 h-6 rounded-full bg-cyan-600 text-white flex items-center justify-center font-medium">
<a href="/profile" class="text-xs flex-1">Modifier</a> FM
</div> </div>
<div class="flex-1 ml-2 inline-flex items-center">Fabien Masson</div>
</div> </div>
<div class="p-2"> <a href="/profile" class="bt small inline-flex items-center justify-center" title="Edition profil">
<button class="w-full bt" onclick={handleLogout} title="Déconecter"> <i class="icon-edit"></i>
</a>
<button class="bt small" onclick={handleLogout} title="Déconnecter">
<i class="icon-logout"></i> <i class="icon-logout"></i>
</button> </button>
</div> </div>
+3 -5
View File
@@ -1,15 +1,13 @@
import { type User } from "$lib/types/api" import type { User } from "$lib/types/api"
type AuthState = { type AuthState = {
user: User | null; user: User | null
status: 'loading' | 'authenticated' | 'unauthenticated'; status: 'loading' | 'authenticated' | 'unauthenticated'
//accessToken: string | null
} }
export const auth: AuthState = $state({ export const auth: AuthState = $state({
user: null, user: null,
status: 'loading', status: 'loading',
//accessToken: null
}) })
export const logout = () => { export const logout = () => {
@@ -0,0 +1,6 @@
import type { Team } from "$lib/types/api"
export const space = $state({
spaces: [] as Team[],
id: "",
});
+58 -1
View File
@@ -1,10 +1,67 @@
export type Item = {
id: string
text: string
}
export interface User { export interface User {
id: string id: string
firstname: string firstname: string
lastname: string lastname: string
email: string email: string
administrator: boolean
theme: string theme: string
lang: string lang: string
timezone: string
_version: string
}
export interface UserTeam {
id: string
userId: string
teamId: string
administrator: boolean
_version: string
}
export interface Team {
id: string
label: string
defaultModelId: string
_version: string
}
export interface UserWithTeams {
user: User
userTeams: UserTeam[]
teams: Team[]
}
export interface Address {
id: string
type: string
address: string
_version: string
}
export interface Provider {
id: string
teamId: string
type: string
name: string
apiKey: string
baseUrl: string
models: string
_date_created: string
_date_updated: string
_version: string
}
export interface Model {
id: string
teamId: string
providerId: string
modelName: string
configuration: object
_date_created: string
_date_updated: string
_version: string _version: string
} }
@@ -1,5 +1,10 @@
const emailRegex: RegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; const emailRegex: RegExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
const urlRegex: RegExp = /^https?:\/\/([a-zA-Z0-9.-]+|\[[0-9a-fA-F:.]+\])(:\d+)?(\/[^\s]*)?$/;
export function validateEmail(input: string): boolean { export function validateEmail(input: string): boolean {
return emailRegex.test(input); return emailRegex.test(input);
} }
export function validateUrl(input: string): boolean {
return urlRegex.test(input);
}
@@ -1,7 +1,8 @@
<div class="container">
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur? Lorem ipsum dolor sit amet consectetur, adipisicing elit. Asperiores, reiciendis quo. Quos animi nisi modi cumque hic illo doloremque totam! Distinctio molestias sint magnam illum nostrum eos, fugit aliquid consequuntur?
</div>
@@ -3,6 +3,7 @@
import { page } from '$app/state'; import { page } from '$app/state';
import {uiState} from "$lib/state/ui.svelte" import {uiState} from "$lib/state/ui.svelte"
import { auth } from '$lib/state/auth.svelte' import { auth } from '$lib/state/auth.svelte'
import { space } from '$lib/state/space.svelte'
import Sidebar from "$lib/components/Sidebar.svelte" import Sidebar from "$lib/components/Sidebar.svelte"
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
import { onMount } from "svelte"; import { onMount } from "svelte";
@@ -39,7 +40,9 @@
onMount(async () => { onMount(async () => {
handleResize() handleResize()
if (data.me) { if (data.me) {
auth.user = data.me auth.user = data.me.user
space.spaces = data.me.teams
space.id = data.me.teams[0].id
themeToLocalStorage(auth.user.theme) themeToLocalStorage(auth.user.theme)
applyTheme() applyTheme()
auth.status = 'authenticated' auth.status = 'authenticated'
@@ -64,7 +67,7 @@
</div> </div>
<div id="main"> <div id="main">
<div class="flex flex-col h-screen "> <div class="flex flex-col h-screen ">
<div class={["header", page.data.theme]}> <div class={["header", "container" , page.data.theme]}>
<h1 class="flex"> <h1 class="flex">
<span class="flex-1">{page.data.title}</span> <span class="flex-1">{page.data.title}</span>
{#if page.data.back} {#if page.data.back}
@@ -2,17 +2,28 @@
import { SvelteMap } from 'svelte/reactivity'; import { SvelteMap } from 'svelte/reactivity';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import { type User } from '$lib/types/api' import type { User, Address, Item } from '$lib/types/api'
import { userApi } from '$lib/api/endpoints/user' import { userApi } from '$lib/api/endpoints/user'
import { miscApi } from '$lib/api/endpoints/misc'
import FormError from '$lib/components/FormError.svelte'; import FormError from '$lib/components/FormError.svelte';
import { applyTheme, themeToLocalStorage } from '$lib/ui/themeswitcher'; import { applyTheme, themeToLocalStorage } from '$lib/ui/themeswitcher';
import { validateEmail } from '$lib/utilities/validators' import { validateEmail } from '$lib/utilities/validators'
import { onMount } from 'svelte';
import Select from 'svelte-select/no-styles';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
let form : User = $state(data.me) let form : User = $state(data.me.user)
let addresses : Address[] = $state([] as Address[])
let newAddress : Address = $state({} as Address)
let timezones = $state([] as Item[])
let errors = new SvelteMap<string,string>() let errors = new SvelteMap<string,string>()
onMount(async ()=> {
addresses = await userApi.listAddresses(form.id)
timezones = (await miscApi.listTimezones()).map((it)=>{return {id:it, text:it}})
})
$effect(()=>{ $effect(()=>{
themeToLocalStorage(form.theme) themeToLocalStorage(form.theme)
applyTheme() applyTheme()
@@ -36,14 +47,30 @@
} }
} }
const handleAddAddress = () => {
userApi.createAddress(form.id, newAddress)
.then((data)=>{
addresses.push(data)
newAddress = {} as Address
})
.catch((error)=>{ console.log(error) })
}
const handleDeleteAddress = (id: string) => {
userApi.deleteAddress(form.id,id).then(()=>{
addresses = addresses.filter((a)=> a.id!=id)
}).catch((error)=>{console.log(error)})
}
</script> </script>
<div class="mb-4 formgroup"> <div class="mb-4 formgroup container">
<h2>Mon compte</h2> <h2>Mon compte</h2>
<form onsubmit={submitUser} onchange={()=>{errors.clear()}}> <form onsubmit={submitUser} onchange={()=>{errors.clear()}}>
<label for="email" class="block my-1 flex-1">Email</label> <div class="formelement">
<label for="email" class="my-1 flex-1">Email</label>
<input <input
id="email" id="email"
type="text" type="text"
@@ -52,10 +79,11 @@
autofocus autofocus
/> />
<FormError key="email" errors={errors} /> <FormError key="email" errors={errors} />
</div>
<div class="flex"> <div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="flex-1 mr-2"> <div class="formelement">
<label for="firstname" class="block my-1 flex-1">Prénom</label> <label for="firstname" class=" my-1 flex-1">Prénom</label>
<input <input
id="firstname" id="firstname"
type="text" type="text"
@@ -65,8 +93,8 @@
<FormError key="firstname" errors={errors} /> <FormError key="firstname" errors={errors} />
</div> </div>
<div class="flex-1 ml-2"> <div class="formelement">
<label for="lastname" class="block my-1 flex-1">Nom</label> <label for="lastname" class="my-1 flex-1">Nom</label>
<input <input
id="lastname" id="lastname"
type="text" type="text"
@@ -77,18 +105,9 @@
</div> </div>
</div> </div>
<div class="flex"> <div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="flex-1 mr-2">
<label for="theme" class="block my-1 flex-1">Thème</label>
<select bind:value={form.theme} class="textinput">
<option value="auto">Automatic</option>
<option value="dark">Sombre</option>
<option value="light">Clair</option>
</select>
<FormError key="theme" errors={errors} />
</div>
<div class="flex-1 ml-2"> <div class="formelement">
<label for="lang" class="block my-1 flex-1">Langue</label> <label for="lang" class="block my-1 flex-1">Langue</label>
<select bind:value={form.lang} class="textinput"> <select bind:value={form.lang} class="textinput">
<option value="en">English</option> <option value="en">English</option>
@@ -96,6 +115,22 @@
</select> </select>
<FormError key="lang" errors={errors} /> <FormError key="lang" errors={errors} />
</div> </div>
<div class="formelement">
<label for="timezone" class="block my-1 flex-1">Timezone</label>
<Select items={timezones} bind:value={form.timezone}/>
<FormError key="timezone" errors={errors} />
</div>
</div>
<div class="formelement">
<label for="theme" class="block my-1 flex-1">Thème</label>
<select bind:value={form.theme} class="textinput">
<option value="auto">Automatique</option>
<option value="dark">Sombre</option>
<option value="light">Clair</option>
</select>
<FormError key="theme" errors={errors} />
</div> </div>
<button class="bt w-full">Enregistrer</button> <button class="bt w-full">Enregistrer</button>
@@ -104,6 +139,35 @@
</div> </div>
<div class="mb-4 formgroup"> <div class="mb-4 formgroup container">
<h2>Canaux de communication</h2> <h2>Canaux de communication</h2>
{#each addresses as address (address.id) }
<div class="m-2">
<div class="dark:bg-stone-700 bg-stone-300 rounded inline px-2 py-1 text-sm">
<span class="inline-block w-20">{address.type} : </span>
<span class="">{address.address}</span>
<button class="ml-4 pointer" onclick={()=>{handleDeleteAddress(address.id)}}>
<i class="icon-cancel"></i>
</button>
</div>
</div>
{/each}
<div class="flex gap-2 mt-6">
<div>
<select bind:value={newAddress.type} class="textinput">
<option value="email">Email</option>
<option value="signal">Signal</option>
<option value="telegram">Telegram</option>
<option value="whatsapp">Whatsapp</option>
</select>
</div>
<div class="flex-1">
<input type="text" bind:value={newAddress.address} class="textinput" />
</div>
<div>
<button class="bt" onclick={handleAddAddress}>Ajouter</button>
</div>
</div>
</div> </div>
@@ -1,4 +1,4 @@
<div class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4"> <div class="container grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
<a class="bt p-5 text-center" <a class="bt p-5 text-center"
title="Modèles" title="Modèles"
@@ -1 +1,157 @@
<div>Modèles</div> <script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import type { Provider} from '$lib/types/api'
import type { PageProps } from './$types';
import { providerApi } from "$lib/api/endpoints/provider"
import { onMount } from 'svelte';
import { validateUrl } from '$lib/utilities/validators'
import ProviderForm from './ProviderForm.svelte'
let { data }: PageProps = $props();
let providers = $state([] as Provider[])
let addingProvider = $state(false)
let editingProvider = $state(null as string|null)
let providerForm = $state({} as Provider)
let errors = new SvelteMap<string,string>()
onMount(async ()=>{
providers = await providerApi.getProviders()
})
const enterAddProviderHandler = () => {
if (editingProvider) return
errors.clear()
providerForm = {} as Provider
providerForm.models = ""
addingProvider= true
}
const cancelAddProviderHandler = () => {
addingProvider= false
}
const submitAddProvider = (p: Provider) => {
if (!p.name) {
errors.set("name","Le nom est obligatoire !")
}
if (!p.type) {
errors.set("type","Le type est obligatoire !")
}
if (p.baseUrl && !validateUrl(p.baseUrl)) {
errors.set("baseUrl","Mauvaise URL !")
}
if (errors.size === 0) {
providerApi.createProvider(p)
.then((p)=>{
providers.push(p)
addingProvider= false
providerForm = {} as Provider
})
.catch((err)=>{
console.log(err)
})
}
}
const cancelEditProviderHandler = () => {
editingProvider = null
providerForm = {} as Provider
}
const submitEditProvider = (p: Provider) => {
if (!p.name) {
errors.set("name","Le nom est obligatoire !")
}
if (!p.type) {
errors.set("type","Le type est obligatoire !")
}
if (p.baseUrl && !validateUrl(p.baseUrl)) {
errors.set("baseUrl","Mauvaise URL !")
}
if (errors.size === 0) {
providerApi.updateProvider(p)
.then((p)=>{
providers = providers.map((it)=>{return p.id===it.id ? p : it})
editingProvider = null
providerForm = {} as Provider
})
.catch((err)=>{
console.log(err)
})
}
}
const handleEditProvider = async (id: string) => {
errors.clear()
if (addingProvider) return
providerApi.getProvider(id)
.then((data)=>{
providers = providers.map((it)=>it.id===id ? data : it)
editingProvider = id
})
}
const handleDeleteProvider = (id: string) => {
providerApi.deleteProvider(id)
.then(() => {
providers = providers.filter((it)=>it.id!==id)
})
.catch((err) => {console.log(err)})
}
</script>
<div class="container">
{#if addingProvider}
{#key "new"}
<ProviderForm
item={providerForm}
errors={errors}
providerTypes={data.providerTypes}
onSave={submitAddProvider}
onCancel={cancelAddProviderHandler}
/>
{/key}
{:else}
<div class="rounded border border-dashed p-2 border-stone-500 cursor-pointer" onclick={enterAddProviderHandler} tabindex="0">
<i class="icon-plus"></i>
Ajouter un fournisseur
</div>
{/if}
{#each providers as p (p.id) }
{#if editingProvider===p.id}
{#key p.id}
<ProviderForm
item={p}
errors={errors}
providerTypes={data.providerTypes}
onSave={submitEditProvider}
onCancel={cancelEditProviderHandler}
/>
{/key}
{:else}
<div class="rounded border p-2 my-2 border-stone-500 flex">
<div class="flex-1">
<span>{ p.name }</span>
<span class="text-stone-500">({ p.type })</span>
</div>
<button class="bt small mx-1" onclick={()=>handleEditProvider(p.id)}>
<i class="icon-edit"></i>
</button>
<button class="bt small mx-1" onclick={()=>handleDeleteProvider(p.id)}>
<i class="icon-trash-empty"></i>
</button>
</div>
{/if}
{/each}
</div>
@@ -1,7 +1,11 @@
export function load() { import { providerApi } from "$lib/api/endpoints/provider"
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return { return {
title: 'Paramétrage : Modèles', title: 'Paramétrage : Modèles',
back: "/settings", back: "/settings",
theme: "settings" theme: "settings",
providerTypes: await providerApi.providerTypes(),
}; };
} }
@@ -0,0 +1,142 @@
<script lang="ts">
import { SvelteMap } from 'svelte/reactivity';
import FormError from '$lib/components/FormError.svelte';
import Select from 'svelte-select/no-styles';
import type { Provider, Item } from "$lib/types/api"
import { providerApi } from "$lib/api/endpoints/provider"
type Props = {
item: Provider
errors: Map<string,string>
providerTypes: Item[]
onSave: (item: Provider) => void
onCancel: () => void
}
let {
item,
errors,
providerTypes,
onSave,
onCancel,
}: Props = $props();
type Selectable = {
value: string
selected: boolean
}
let selectedModels = $state(item.models.split(";"))
let availableModels : Selectable[]= $state(item.models.split(";").map((it)=> {return {value:it, selected:true}}))
let modelsErrors = new SvelteMap<string,string>()
const autofocus = (node: HTMLElement) => {
node.focus();
}
const handleSave = (e: Event) => {
e.preventDefault()
item.models = selectedModels.join(";")
onSave(item)
}
const handleCancel = (e: Event) => {
e.preventDefault()
onCancel()
}
const fetchModels = (e: Event) => {
e.preventDefault()
modelsErrors.clear()
providerApi.getAvailableModels(item)
.then((data) => {
availableModels = data.map((it)=>{
return { value: it, selected: selectedModels.includes(it)}
});
})
.catch((err) => { modelsErrors.set("models",err) })
}
const handleModelChange = () => {
selectedModels = availableModels.filter((it)=>it.selected).map((it)=>it.value)
}
</script>
<div>
<div class="formgroup my-2">
<form onsubmit={handleSave} onchange={()=>{errors.clear()}}>
<div class="formelement">
<label for="name">Nom</label>
<input
id="name"
type="text"
bind:value={item.name}
class="textinput"
placeholder="nom affiché"
use:autofocus
/>
<FormError key="name" errors={errors} />
</div>
<div class="formelement">
<label for="type" class="block my-1 flex-1">Type</label>
<Select items={providerTypes}
valueMode="id" itemId="id" label="text"
bind:value={item.type}
/>
<FormError key="type" errors={errors} />
</div>
<div class="formelement">
<label for="baseUrl">URL</label>
<input
id="baseUrl"
type="text"
bind:value={item.baseUrl}
class="textinput"
placeholder="http://127.0.0.1:8080/v1"
autocomplete="off"
/>
<FormError key="baseUrl" errors={errors} />
</div>
<div class="formelement">
<label for="apiKey">Clé API</label>
<input
id="apiKey"
type="password"
bind:value={item.apiKey}
class="textinput"
placeholder="clé API (optionnel)"
autocomplete="new-password"
/>
<FormError key="apiKey" errors={errors} />
</div>
<div class="formelement">
<label for="apiKey">Models</label>
<button class="bt small" onclick={fetchModels}>
Afficher les modèles disponibles
</button>
<ol class="ml-2">
{#each availableModels as model (model.value)}
{#if model.value}
<li>
<label>
<input type="checkbox" bind:checked={model.selected} onchange={handleModelChange}/>
{model.value}
</label>
</li>
{/if}
{/each}
</ol>
<div><FormError key="models" errors={modelsErrors} /></div>
</div>
<div class="text-right">
<button type="button" class="bt" onclick={handleCancel}>Annuler</button>
<button type="submit" class="bt mx-4">Enregistrer</button>
</div>
</form>
</div>
</div>
@@ -31,6 +31,12 @@
if (!validateEmail(form.email)) { if (!validateEmail(form.email)) {
errors.set("email","Adresse email invalide !") errors.set("email","Adresse email invalide !")
} }
if (!form.firstname) {
errors.set("firstname","Le prénom est obligatoire !")
}
if (!form.lastname) {
errors.set("lastname","Le nom est obligatoire !")
}
if (form.password!==form.verifPassword) { if (form.password!==form.verifPassword) {
errors.set("verifPassword","Les mots de pass ne correspondent pas.") errors.set("verifPassword","Les mots de pass ne correspondent pas.")
} }
+162 -14
View File
@@ -8,38 +8,45 @@ html {
@apply h-full; @apply h-full;
} }
body { body {
@apply h-full bg-stone-100 text-stone-900 dark:bg-stone-900 dark:text-stone-200; @apply h-full bg-stone-100 text-stone-950 dark:bg-stone-950 dark:text-stone-100;
} }
label { .formelement > label {
@apply text-xs text-stone-600 uppercase dark:text-stone-400; @apply text-xs text-stone-600 uppercase dark:text-stone-400;
} }
hr { hr {
@apply border-stone-200 dark:border-stone-800; @apply border-stone-200 dark:border-stone-800;
} }
h2 { h2 {
@apply mb-4 font-bold; @apply mb-4 text-base font-bold;
} }
.textinput { .container {
@apply w-full rounded bg-white p-1 text-stone-900 dark:bg-stone-700 dark:text-stone-100; @apply mx-auto max-w-3xl;
} }
.pointer { .pointer {
@apply cursor-pointer; @apply cursor-pointer;
} }
.bt { .bt {
@apply cursor-pointer rounded bg-stone-200 px-4 py-2 hover:bg-stone-300 dark:bg-stone-800 dark:hover:bg-stone-700; appearance: button;
@apply cursor-pointer rounded bg-stone-300 px-4 py-1 font-bold hover:bg-stone-200 dark:bg-stone-800 dark:hover:bg-stone-700;
&.small {
@apply px-2 py-1;
}
} }
.menuitem { .menuitem {
@apply text-stone-800 text-stone-950 dark:text-stone-300 hover:dark:text-stone-50; @apply text-stone-950 dark:text-stone-300 hover:dark:text-stone-50;
} }
.header { .header {
@apply p-2 text-lg; @apply py-2 text-lg font-bold;
/*
h1 { h1 {
@apply rounded bg-stone-200 px-2 font-bold text-sky-600 dark:bg-stone-800 dark:text-sky-200; @apply rounded bg-stone-200 px-2 font-bold uppercase dark:bg-stone-800;
} }
*/
&.settings { &.settings {
h1 { h1 {
@@ -56,10 +63,6 @@ h2 {
@apply text-sky-500; @apply text-sky-500;
} }
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 dark:border-stone-700 dark:bg-stone-900;
}
#outer { #outer {
@apply flex h-full flex-row; @apply flex h-full flex-row;
} }
@@ -97,7 +100,7 @@ h2 {
@apply ml-10; @apply ml-10;
} }
#sidebar { #sidebar {
@apply fixed left-0 h-full w-full bg-stone-100 dark:bg-stone-900; @apply fixed left-0 h-full w-full bg-stone-100 dark:bg-stone-950;
} }
.mobileMenuClosed { .mobileMenuClosed {
#sidebar { #sidebar {
@@ -105,3 +108,148 @@ h2 {
} }
} }
} }
/* form */
.formgroup {
@apply rounded border border-stone-300 bg-stone-100 p-2 text-sm dark:border-stone-700 dark:bg-stone-900;
}
.formelement {
@apply mb-3;
}
.textinput {
@apply h-8 w-full rounded border-stone-700 bg-stone-50 px-2 py-1 text-sm text-stone-900 dark:bg-stone-950 dark:text-stone-100;
}
/* svelte select */
.svelte-select {
@apply relative flex h-8 w-full border-none text-stone-900 dark:bg-stone-950 dark:text-stone-100;
}
.svelte-select .value-container {
@apply relative flex-1;
}
.svelte-select input {
@apply absolute top-0 left-0 m-0 h-8 w-full cursor-default rounded border-stone-700 bg-transparent px-2 py-0 text-sm leading-10 text-stone-100;
}
.svelte-select.focused {
/* @apply border-blue-600; */
}
.svelte-select.disabled {
@apply border-gray-200 bg-gray-200 text-gray-600;
}
.svelte-select.disabled input {
@apply placeholder:text-gray-400 placeholder:opacity-100;
}
.svelte-select > .value-container > .selected-item {
@apply h-8 overflow-x-hidden px-4 leading-8 focus:outline-none;
}
.svelte-select .hide-selected-item {
@apply hidden;
}
.svelte-select .indicators {
@apply pointer-events-none absolute top-0 right-0 bottom-0 flex h-8 translate-y-0 items-center text-gray-200;
}
.svelte-select .icons > * {
@apply transition-colors duration-200 ease-in-out focus:outline-none;
}
.svelte-select .clear-select {
@apply pointer-events-auto;
}
.svelte-select.focused .icons,
.svelte-select .chevron:hover,
.svelte-select .clear-select:hover {
@apply text-gray-600;
}
.svelte-select .clear-select {
@apply h-5 w-9 flex-none px-2 text-gray-300;
}
.svelte-select .chevron {
@apply flex h-5 w-9 border-l-2 pt-0 pr-2 pl-2 text-gray-300;
}
.svelte-select.multi {
@apply h-auto flex-wrap items-stretch pr-9 pl-4;
}
.svelte-select.multi input {
@apply relative m-0 p-0;
}
.svelte-select.error {
@apply border-red-500 bg-white;
}
.a11y-text {
@apply sr-only;
}
.svelte-select-list {
@apply absolute right-0 left-0 z-10 max-h-64 w-full overflow-y-auto rounded-sm border-none bg-stone-50 px-4 shadow-md dark:bg-stone-800;
}
.svelte-select-list .list-group-title {
@apply h-8 cursor-default px-5 text-sm leading-10 font-medium text-ellipsis whitespace-nowrap text-gray-400 uppercase;
}
.svelte-select-list .empty {
@apply py-5 text-center text-gray-500;
}
.svelte-select-item {
@apply h-8 cursor-default overflow-hidden px-5 leading-10 text-ellipsis whitespace-nowrap text-gray-800;
}
.item.group-item {
@apply px-10;
}
.item.active {
@apply bg-blue-600 text-white dark:bg-blue-900;
}
.item.not-selectable {
@apply text-gray-300;
}
.item.first {
@apply rounded-t-sm;
}
.item.hover:not(.active) {
@apply bg-blue-100 dark:bg-stone-700;
}
.multi input {
flex: 1 1 40px;
}
.multi-item {
@apply mt-1 mr-1 flex h-8 max-w-full cursor-default items-center overflow-hidden rounded-sm border border-gray-200 bg-gray-100 pr-1 pl-1 leading-8 text-ellipsis whitespace-nowrap;
}
.multi-item.disabled {
@apply hover:bg-gray-300 hover:text-gray-500;
}
.multi-item-clear {
@apply flex w-5 items-center justify-center;
}
.list-item {
@apply list-none;
}
@@ -113,6 +113,24 @@
"css": "left-big", "css": "left-big",
"code": 59400, "code": 59400,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "5211af474d3a9848f67f945e2ccaf143",
"css": "cancel",
"code": 59401,
"src": "fontawesome"
},
{
"uid": "44e04715aecbca7f266a17d5a7863c68",
"css": "plus",
"code": 59402,
"src": "fontawesome"
},
{
"uid": "f48ae54adfb27d8ada53d0fd9e34ee10",
"css": "trash-empty",
"code": 59403,
"src": "fontawesome"
} }
] ]
} }
@@ -8,6 +8,9 @@
.icon-right-big:before { content: '\e806'; } /* '' */ .icon-right-big:before { content: '\e806'; } /* '' */
.icon-logout:before { content: '\e807'; } /* '' */ .icon-logout:before { content: '\e807'; } /* '' */
.icon-left-big:before { content: '\e808'; } /* '' */ .icon-left-big:before { content: '\e808'; } /* '' */
.icon-cancel:before { content: '\e809'; } /* '' */
.icon-plus:before { content: '\e80a'; } /* '' */
.icon-trash-empty:before { content: '\e80b'; } /* '' */
.icon-facebook:before { content: '\f09a'; } /* '' */ .icon-facebook:before { content: '\f09a'; } /* '' */
.icon-tasks:before { content: '\f0ae'; } /* '' */ .icon-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-menu:before { content: '\f0c9'; } /* '' */
@@ -1,15 +1,15 @@
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.eot?40016728'); src: url('../font/fontello.eot?20532310');
src: url('../font/fontello.eot?40016728#iefix') format('embedded-opentype'), src: url('../font/fontello.eot?20532310#iefix') format('embedded-opentype'),
url('../font/fontello.svg?40016728#fontello') format('svg'); url('../font/fontello.svg?20532310#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('data:application/octet-stream;base64,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 truncated src: url('data:application/octet-stream;base64,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 truncated
url('data:application/octet-stream;base64,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 truncated url('data:application/octet-stream;base64,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 truncated
} }
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
@@ -17,7 +17,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.svg?40016728#fontello') format('svg'); src: url('../font/fontello.svg?20532310#fontello') format('svg');
} }
} }
*/ */
@@ -66,6 +66,9 @@
.icon-right-big:before { content: '\e806'; } /* '' */ .icon-right-big:before { content: '\e806'; } /* '' */
.icon-logout:before { content: '\e807'; } /* '' */ .icon-logout:before { content: '\e807'; } /* '' */
.icon-left-big:before { content: '\e808'; } /* '' */ .icon-left-big:before { content: '\e808'; } /* '' */
.icon-cancel:before { content: '\e809'; } /* '' */
.icon-plus:before { content: '\e80a'; } /* '' */
.icon-trash-empty:before { content: '\e80b'; } /* '' */
.icon-facebook:before { content: '\f09a'; } /* '' */ .icon-facebook:before { content: '\f09a'; } /* '' */
.icon-tasks:before { content: '\f0ae'; } /* '' */ .icon-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-menu:before { content: '\f0c9'; } /* '' */
@@ -8,6 +8,9 @@
.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); } .icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); } .icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); } .icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-trash-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.icon-facebook { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09a;&nbsp;'); } .icon-facebook { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09a;&nbsp;'); }
.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ae;&nbsp;'); } .icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ae;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); } .icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
@@ -19,6 +19,9 @@
.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); } .icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe806;&nbsp;'); }
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); } .icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe807;&nbsp;'); }
.icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); } .icon-left-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe808;&nbsp;'); }
.icon-cancel { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe809;&nbsp;'); }
.icon-plus { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80a;&nbsp;'); }
.icon-trash-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe80b;&nbsp;'); }
.icon-facebook { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09a;&nbsp;'); } .icon-facebook { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf09a;&nbsp;'); }
.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ae;&nbsp;'); } .icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ae;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); } .icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
+10 -7
View File
@@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.eot?64785462'); src: url('../font/fontello.eot?1806785');
src: url('../font/fontello.eot?64785462#iefix') format('embedded-opentype'), src: url('../font/fontello.eot?1806785#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?64785462') format('woff2'), url('../font/fontello.woff2?1806785') format('woff2'),
url('../font/fontello.woff?64785462') format('woff'), url('../font/fontello.woff?1806785') format('woff'),
url('../font/fontello.ttf?64785462') format('truetype'), url('../font/fontello.ttf?1806785') format('truetype'),
url('../font/fontello.svg?64785462#fontello') format('svg'); url('../font/fontello.svg?1806785#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.svg?64785462#fontello') format('svg'); src: url('../font/fontello.svg?1806785#fontello') format('svg');
} }
} }
*/ */
@@ -63,6 +63,9 @@
.icon-right-big:before { content: '\e806'; } /* '' */ .icon-right-big:before { content: '\e806'; } /* '' */
.icon-logout:before { content: '\e807'; } /* '' */ .icon-logout:before { content: '\e807'; } /* '' */
.icon-left-big:before { content: '\e808'; } /* '' */ .icon-left-big:before { content: '\e808'; } /* '' */
.icon-cancel:before { content: '\e809'; } /* '' */
.icon-plus:before { content: '\e80a'; } /* '' */
.icon-trash-empty:before { content: '\e80b'; } /* '' */
.icon-facebook:before { content: '\f09a'; } /* '' */ .icon-facebook:before { content: '\f09a'; } /* '' */
.icon-tasks:before { content: '\f0ae'; } /* '' */ .icon-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-menu:before { content: '\f0c9'; } /* '' */
+20 -9
View File
@@ -146,11 +146,11 @@
} }
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('./font/fontello.eot?72761720'); src: url('./font/fontello.eot?13627011');
src: url('./font/fontello.eot?72761720#iefix') format('embedded-opentype'), src: url('./font/fontello.eot?13627011#iefix') format('embedded-opentype'),
url('./font/fontello.woff?72761720') format('woff'), url('./font/fontello.woff?13627011') format('woff'),
url('./font/fontello.ttf?72761720') format('truetype'), url('./font/fontello.ttf?13627011') format('truetype'),
url('./font/fontello.svg?72761720#fontello') format('svg'); url('./font/fontello.svg?13627011#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@@ -242,6 +242,17 @@
<div class="span3" title="Code: 0xe808"> <div class="span3" title="Code: 0xe808">
<i class="demo-icon icon-left-big">&#xe808;</i> <span class="i-name">icon-left-big</span><span class="i-code">0xe808</span> <i class="demo-icon icon-left-big">&#xe808;</i> <span class="i-name">icon-left-big</span><span class="i-code">0xe808</span>
</div> </div>
<div class="span3" title="Code: 0xe809">
<i class="demo-icon icon-cancel">&#xe809;</i> <span class="i-name">icon-cancel</span><span class="i-code">0xe809</span>
</div>
<div class="span3" title="Code: 0xe80a">
<i class="demo-icon icon-plus">&#xe80a;</i> <span class="i-name">icon-plus</span><span class="i-code">0xe80a</span>
</div>
<div class="span3" title="Code: 0xe80b">
<i class="demo-icon icon-trash-empty">&#xe80b;</i> <span class="i-name">icon-trash-empty</span><span class="i-code">0xe80b</span>
</div>
</div>
<div class="row">
<div class="span3" title="Code: 0xf09a"> <div class="span3" title="Code: 0xf09a">
<i class="demo-icon icon-facebook">&#xf09a;</i> <span class="i-name">icon-facebook</span><span class="i-code">0xf09a</span> <i class="demo-icon icon-facebook">&#xf09a;</i> <span class="i-name">icon-facebook</span><span class="i-code">0xf09a</span>
</div> </div>
@@ -251,11 +262,11 @@
<div class="span3" title="Code: 0xf0c9"> <div class="span3" title="Code: 0xf0c9">
<i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span> <i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span>
</div> </div>
</div>
<div class="row">
<div class="span3" title="Code: 0xf113"> <div class="span3" title="Code: 0xf113">
<i class="demo-icon icon-github">&#xf113;</i> <span class="i-name">icon-github</span><span class="i-code">0xf113</span> <i class="demo-icon icon-github">&#xf113;</i> <span class="i-name">icon-github</span><span class="i-code">0xf113</span>
</div> </div>
</div>
<div class="row">
<div class="span3" title="Code: 0xf179"> <div class="span3" title="Code: 0xf179">
<i class="demo-icon icon-apple">&#xf179;</i> <span class="i-name">icon-apple</span><span class="i-code">0xf179</span> <i class="demo-icon icon-apple">&#xf179;</i> <span class="i-name">icon-apple</span><span class="i-code">0xf179</span>
</div> </div>
@@ -265,11 +276,11 @@
<div class="span3" title="Code: 0xf1a0"> <div class="span3" title="Code: 0xf1a0">
<i class="demo-icon icon-google">&#xf1a0;</i> <span class="i-name">icon-google</span><span class="i-code">0xf1a0</span> <i class="demo-icon icon-google">&#xf1a0;</i> <span class="i-name">icon-google</span><span class="i-code">0xf1a0</span>
</div> </div>
</div>
<div class="row">
<div class="span3" title="Code: 0xf1d8"> <div class="span3" title="Code: 0xf1d8">
<i class="demo-icon icon-paper-plane">&#xf1d8;</i> <span class="i-name">icon-paper-plane</span><span class="i-code">0xf1d8</span> <i class="demo-icon icon-paper-plane">&#xf1d8;</i> <span class="i-name">icon-paper-plane</span><span class="i-code">0xf1d8</span>
</div> </div>
</div>
<div class="row">
<div class="span3" title="Code: 0xf1da"> <div class="span3" title="Code: 0xf1da">
<i class="demo-icon icon-history">&#xf1da;</i> <span class="i-name">icon-history</span><span class="i-code">0xf1da</span> <i class="demo-icon icon-history">&#xf1da;</i> <span class="i-name">icon-history</span><span class="i-code">0xf1da</span>
</div> </div>
Binary file not shown.
@@ -24,6 +24,12 @@
<glyph glyph-name="left-big" unicode="&#xe808;" d="M857 350v-71q0-30-18-51t-47-21h-393l164-164q21-20 21-50t-21-50l-42-43q-21-20-51-20-29 0-50 20l-364 364q-20 21-20 50 0 29 20 51l364 363q21 21 50 21 29 0 51-21l42-41q21-22 21-51t-21-51l-164-164h393q29 0 47-20t18-51z" horiz-adv-x="857.1" /> <glyph glyph-name="left-big" unicode="&#xe808;" d="M857 350v-71q0-30-18-51t-47-21h-393l164-164q21-20 21-50t-21-50l-42-43q-21-20-51-20-29 0-50 20l-364 364q-20 21-20 50 0 29 20 51l364 363q21 21 50 21 29 0 51-21l42-41q21-22 21-51t-21-51l-164-164h393q29 0 47-20t18-51z" horiz-adv-x="857.1" />
<glyph glyph-name="cancel" unicode="&#xe809;" d="M724 112q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
<glyph glyph-name="plus" unicode="&#xe80a;" d="M786 439v-107q0-22-16-38t-38-15h-232v-233q0-22-16-37t-38-16h-107q-22 0-38 16t-15 37v233h-232q-23 0-38 15t-16 38v107q0 23 16 38t38 16h232v232q0 22 15 38t38 16h107q23 0 38-16t16-38v-232h232q23 0 38-16t16-38z" horiz-adv-x="785.7" />
<glyph glyph-name="trash-empty" unicode="&#xe80b;" d="M286 439v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m143 0v-321q0-8-5-13t-13-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q8 0 13-5t5-13z m142 0v-321q0-8-5-13t-12-5h-36q-8 0-13 5t-5 13v321q0 8 5 13t13 5h36q7 0 12-5t5-13z m72-404v529h-500v-529q0-12 4-22t8-15 6-5h464q2 0 6 5t8 15 4 22z m-375 601h250l-27 65q-4 5-9 6h-177q-6-1-10-6z m518-18v-36q0-8-5-13t-13-5h-54v-529q0-46-26-80t-63-34h-464q-37 0-63 33t-27 79v531h-53q-8 0-13 5t-5 13v36q0 8 5 13t13 5h172l39 93q9 21 31 35t44 15h178q23 0 44-15t30-35l39-93h173q8 0 13-5t5-13z" horiz-adv-x="785.7" />
<glyph glyph-name="facebook" unicode="&#xf09a;" d="M535 843v-147h-87q-48 0-65-20t-17-60v-106h164l-22-165h-142v-424h-171v424h-142v165h142v122q0 104 58 161t155 57q82 0 127-7z" horiz-adv-x="571.4" /> <glyph glyph-name="facebook" unicode="&#xf09a;" d="M535 843v-147h-87q-48 0-65-20t-17-60v-106h164l-22-165h-142v-424h-171v424h-142v165h142v122q0 104 58 161t155 57q82 0 127-7z" horiz-adv-x="571.4" />
<glyph glyph-name="tasks" unicode="&#xf0ae;" d="M571 64h358v72h-358v-72z m-214 286h572v71h-572v-71z m357 286h215v71h-215v-71z m286-465v-142q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v142q0 15 11 26t25 10h928q15 0 25-10t11-26z m0 286v-143q0-14-11-25t-25-10h-928q-15 0-25 10t-11 25v143q0 15 11 25t25 11h928q15 0 25-11t11-25z m0 286v-143q0-14-11-25t-25-11h-928q-15 0-25 11t-11 25v143q0 14 11 25t25 11h928q15 0 25-11t11-25z" horiz-adv-x="1000" /> <glyph glyph-name="tasks" unicode="&#xf0ae;" d="M571 64h358v72h-358v-72z m-214 286h572v71h-572v-71z m357 286h215v71h-215v-71z m286-465v-142q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v142q0 15 11 26t25 10h928q15 0 25-10t11-26z m0 286v-143q0-14-11-25t-25-10h-928q-15 0-25 10t-11 25v143q0 15 11 25t25 11h928q15 0 25-11t11-25z m0 286v-143q0-14-11-25t-25-11h-928q-15 0-25 11t-11 25v143q0 14 11 25t25 11h928q15 0 25-11t11-25z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.