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 2075 additions and 216 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
.env
+9 -4
View File
@@ -8,9 +8,11 @@ import (
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
"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/http"
"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/user"
)
@@ -56,6 +58,7 @@ func runServe() {
// Adapters
tokenManager := security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
pwdAuth := password.NewPasswordAuthenticator(tokenManager, db.UserRepository())
llmEngine := llm.NewAnyLLMEngine()
// services
authService := auth.NewService(
@@ -67,13 +70,15 @@ func runServe() {
)
userService := user.NewService(db.UserRepository())
storageService := storage.NewService(store)
providerService := provider.NewService(db.ProviderRepository(), db.UserRepository(), llmEngine)
// http server
router := http.NewRouter(http.Dependencies{
StorageService: storageService,
AuthService: authService,
UserService: userService,
TokenManager: tokenManager,
StorageService: storageService,
AuthService: authService,
UserService: userService,
TokenManager: tokenManager,
ProviderService: providerService,
})
if err := router.Start(); err != nil {
@@ -12,6 +12,7 @@ type Database interface {
SettingsRepository() domain.SettingsRepository
UserRepository() domain.UserRepository
FileRepository() domain.FileRepository
ProviderRepository() domain.ProviderRepository
Migrate() error
Close()
}
@@ -67,3 +67,7 @@ func (db *TursoDB) SettingsRepository() domain.SettingsRepository {
func (db *TursoDB) FileRepository() domain.FileRepository {
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,
VersionId: utility.GenID(),
DateCreated: time.Now(),
DateUpdated: time.Now(),
Content: bytes,
}
@@ -11,4 +11,3 @@ drop table tools;
drop table tasks;
drop table history;
drop table files;
drop table share;
@@ -1,25 +1,16 @@
create table providers (
id text not null primary key,
team_id text not null,
type text not null,
name text not null,
key text,
url text,
models text,
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
);
create table models (
id text not null primary key,
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 (
id text not null primary key,
@@ -52,7 +43,8 @@ create table users (
picture text not null default '',
enabled numeric default true,
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_updated numeric,
_version text not null
@@ -149,14 +141,3 @@ create table files (
_date_updated numeric ,
_version text not null
);
create table share (
id text not null primary key,
object_id text not null,
object_type text not null,
team_id text not null,
enabled numeric not null default 0,
_date_created numeric not null default current_timestamp,
_date_updated numeric,
_version text not null
);
@@ -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) {
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) {
@@ -123,21 +123,20 @@ func (ur *TursoUserRepository) DeleteUserTeam(userid string, teamid string) erro
return ur.UserTeamTable.Delete(teamid)
}
func (ur *TursoUserRepository) PopulateUserWithTeams(user *domain.User) (*domain.UserWithTeams, error) {
return nil, nil
}
func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddress, error) {
return ur.AddressTable.SelectWhere("user_id=$1", id)
}
func (ur *TursoUserRepository) CreateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) {
addr.UserID = userid
func (ur *TursoUserRepository) GetUserAddress(addrID string) (*domain.UserAddress, error) {
return ur.AddressTable.Get(addrID)
}
func (ur *TursoUserRepository) CreateUserAddress(addr *domain.UserAddress) (*domain.UserAddress, error) {
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)
}
func (ur *TursoUserRepository) DeleteUserAddress(userid string, addrID string) error {
return ur.AddressTable.DeleteWhere("id=$1 and user_id=$2", addrID, userid)
func (ur *TursoUserRepository) DeleteUserAddress(addrID string) error {
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(...)
c.Set(auth.ContextUserIDKey, claims.ID)
c.Set(auth.ContextEmailKey, claims.Email)
c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname)
//c.Set(auth.ContextEmailKey, claims.Email)
//c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname)
return next(c)
}
+9 -9
View File
@@ -6,15 +6,15 @@ import (
)
type File struct {
ID string `db:"id" json:"id"`
Name string `db:"name" json:"name"`
ContentType string `db:"content_type" json:"contentType"`
StoragePath string `db:"storage_path" json:"-"`
StorageFilename string `db:"storage_filename" json:"-"`
Content []byte `db:"content" json:"-"`
DateCreated time.Time `db:"date_created" json:"-"`
DateUpdated time.Time `db:"date_updated" json:"-"`
VersionId string `db:"_version" json:"-"`
ID string `db:"id" json:"id"`
Name string `db:"name" json:"name"`
ContentType string `db:"content_type" json:"contentType"`
StoragePath string `db:"storage_path" json:"-"`
StorageFilename string `db:"storage_filename" json:"-"`
Content []byte `db:"content" json:"-"`
DateCreated time.Time `db:"date_created" json:"-"`
DateUpdated *time.Time `db:"date_updated" json:"-"`
VersionId string `db:"_version" json:"-"`
}
type FileRepository interface {
+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"`
RegisterEnabled bool `db:"register_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"`
VersionId string `db:"_version" json:"_version"`
}
+39 -29
View File
@@ -5,36 +5,37 @@ import (
)
type User struct {
ID string `db:"id" json:"id"`
Firstname string `db:"firstname" json:"firstname,omitempty"`
Lastname string `db:"lastname" json:"lastname,omitempty"`
Password string `db:"password" json:"-"`
Email string `db:"email" json:"email,omitempty"`
PictureID string `db:"picture" json:"picture,omitempty"`
Enabled bool `db:"enabled" json:"enabled"`
Theme string `db:"theme" json:"theme"`
Lang string `db:"lang" json:"lang"`
DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"`
ID string `db:"id" json:"id"`
Firstname string `db:"firstname" json:"firstname,omitempty"`
Lastname string `db:"lastname" json:"lastname,omitempty"`
Password string `db:"password" json:"-"`
Email string `db:"email" json:"email,omitempty"`
PictureID string `db:"picture" json:"picture,omitempty"`
Enabled bool `db:"enabled" json:"enabled"`
Theme string `db:"theme" json:"theme"`
Lang string `db:"lang" json:"lang"`
Timezone string `db:"timezone" json:"timezone"`
DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"`
}
type Team struct {
ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"`
DefaultModelID *string `db:"default_model_id" json:"default_model_id"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
DefaultModelID *string `db:"default_model_id" json:"defaultModelId"`
DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"`
}
type UserTeam struct {
ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"`
TeamID string `db:"team_id" json:"team_id"`
UserID string `db:"user_id" json:"userId"`
TeamID string `db:"team_id" json:"teamId"`
Administrator bool `db:"administrator" json:"administrator"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"`
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"`
}
@@ -42,9 +43,9 @@ type UserAddress struct {
ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"`
Type string `db:"type" json:"type"`
Address string `db:"address_id" json:"address"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
Address string `db:"address" json:"address"`
DateCreated time.Time `db:"_date_created" json:"_dateCreated"`
DateUpdated *time.Time `db:"_date_updated" json:"_dateUpdated"`
VersionId string `db:"_version" json:"_version"`
}
@@ -54,8 +55,18 @@ type TeamRelation struct {
}
type UserWithTeams struct {
User *User `json:"user"`
Teams []*TeamRelation `json:"teams"`
User *User `json:"user"`
UserTeams []*UserTeam `json:"user_teams"`
Teams []*Team `json:"teams"`
}
func AddressTypes() []string {
return []string{
"email",
"telegram",
"whatsapp",
"signal",
}
}
type UserRepository interface {
@@ -78,10 +89,9 @@ type UserRepository interface {
UpdateUserTeam(userid string, team *UserTeam) (*UserTeam, error)
DeleteUserTeam(userid string, teamid string) error
PopulateUserWithTeams(user *User) (*UserWithTeams, error)
ListUserAddresses(userid string) ([]*UserAddress, error)
CreateUserAddress(userid string, addr *UserAddress) (*UserAddress, error)
UpdateUserAddress(userid string, addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(userid string, addrID string) error
GetUserAddress(addrID string) (*UserAddress, error)
CreateUserAddress(addr *UserAddress) (*UserAddress, 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{
userService: service,
}
_ = userHandler
auth := e.Group(prefix, middlewares...)
auth.GET("/me", userHandler.Me)
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
}
@@ -26,7 +29,7 @@ type UserHandler struct {
func (h UserHandler) Me(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
user, err := h.userService.GetUser(userID)
user, err := h.userService.GetUserWithTeams(userID)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
}
@@ -43,15 +46,86 @@ func (h UserHandler) Update(c *echo.Context) error {
}
updUser.ID = id
isAdmin := false // TODO : check if caller is admin
if !(isAdmin || userID == id) {
if !(userID == id) {
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 {
return c.String(http.StatusBadRequest, err.Error())
}
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")
}
+8 -4
View File
@@ -11,6 +11,7 @@ import (
"trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/http/handlers"
"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/user"
)
@@ -32,10 +33,11 @@ type Router struct {
}
type Dependencies struct {
StorageService *storage.Service
AuthService *auth.Service
UserService *user.Service
TokenManager auth.TokenManager
StorageService *storage.Service
AuthService *auth.Service
UserService *user.Service
ProviderService *provider.Service
TokenManager auth.TokenManager
}
func NewRouter(deps Dependencies) *Router {
@@ -70,6 +72,8 @@ func NewRouter(deps Dependencies) *Router {
api := e.Group("/api")
_ = handlers.NewAuthGroup("/auth", api, deps.AuthService)
_ = 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{
echo: e,
@@ -31,8 +31,10 @@ type Session struct {
}
const ContextUserIDKey = "userID"
const ContextEmailKey = "userEmail"
const ContextNameKey = "userName"
const ContextUserAdminKey = "userAdmin"
//const ContextEmailKey = "userEmail"
//const ContextNameKey = "userName"
// Interface définie ici car c'est un port propre au cas d'usage "auth"
type Authenticator interface {
+4 -1
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"time"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
@@ -53,8 +54,10 @@ func (s *Service) Register(registration Registration) (*Session, error) {
if err != nil {
return nil, err
}
now := time.Now()
_, err = s.userRepository.CreateTeam(session.User.ID, &domain.Team{
Label: "Espace personnel",
Label: "Espace personnel",
DateCreated: now,
})
if err != nil {
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
}
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) {
user, err := s.repo.FindUser(updUser.ID)
if err != nil {
@@ -41,7 +65,43 @@ func (s *Service) UpdateUser(updUser *domain.User, fromAdmin bool) (*domain.User
user.Lastname = updUser.Lastname
user.Theme = updUser.Theme
user.Lang = updUser.Lang
user.Timezone = updUser.Timezone
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",
"version": "0.0.1",
"dependencies": {
"oidc-client-ts": "^3.5.0"
"oidc-client-ts": "^3.5.0",
"svelte-select": "^6.0.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -198,6 +199,31 @@
"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": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -268,7 +294,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -279,7 +304,6 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -290,7 +314,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -300,14 +323,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -822,7 +843,6 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.11.tgz",
"integrity": "sha512-LFuZUkjJ9iF7JZye/aG5XM0SFcQ5VyL0oVX4WJ9dc0Va3R3s0OauX1BESVCb+YN/ol8TAfqGDDAQsTG627Y5kw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
@@ -1259,7 +1279,6 @@
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
@@ -1283,7 +1302,6 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/ws": {
@@ -1446,7 +1464,7 @@
"version": "8.65.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
"dev": true,
"devOptional": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1530,7 +1548,6 @@
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -1570,7 +1587,6 @@
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
"integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1580,7 +1596,6 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
@@ -1613,7 +1628,6 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -1706,7 +1720,6 @@
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.0.tgz",
"integrity": "sha512-RWrqdArjvPbsATEhOPUo6Wndc/iWnkWKlhIrdlF3zMMYo/c3CVtoaVAyLtWxz5h8nSlkHzxnzV2uLydPXmtF+A==",
"dev": true,
"license": "MIT"
},
"node_modules/enhanced-resolve": {
@@ -1894,7 +1907,6 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"dev": true,
"license": "MIT"
},
"node_modules/espree": {
@@ -1932,7 +1944,6 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.3.0.tgz",
"integrity": "sha512-GQ/7RN8uOtEfNpzZzBMTzW9JBcX42oaSVtPzdF+6cEL8pqIL094iUpr9jzYGn4O4P/1S60dJ6izyT8F4LYARng==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
@@ -2164,7 +2175,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.6"
@@ -2595,7 +2605,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
"dev": true,
"license": "MIT"
},
"node_modules/locate-path": {
@@ -2618,7 +2627,6 @@
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -2678,9 +2686,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"dev": true,
"funding": [
{
@@ -3211,7 +3219,6 @@
"version": "5.56.8",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.8.tgz",
"integrity": "sha512-PY8LOw7xP6c8IOiVqdo0sbbZVYhXRSfklOQLAUyGBKqjTX0wx/z4l/9J+PmBpmlLnxzEb1NqltxQ5/wZme/Cmg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
@@ -3383,6 +3390,28 @@
"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": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
@@ -3973,7 +4002,6 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
"integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
"dev": true,
"license": "MIT"
}
}
+2 -1
View File
@@ -48,6 +48,7 @@
"esbuild": "^0.25.0"
},
"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 { type User } from "$lib/types/api"
import type { User, Address, UserWithTeams } from "$lib/types/api"
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),
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)}
<span class="text-red-700 dark:text-red-400 text-xs">{errors.get(key)}</span>
{:else}
<span class="text-xs">&nbsp;</span>
{/if}
+28 -16
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { uiState } from "$lib/state/ui.svelte"
import { auth } from "$lib/state/auth.svelte"
import Menu from "$lib/components/Menu.svelte"
import { logout } from "$lib/state/auth.svelte"
import logo from "$lib/assets/logo-txt.png"
import { space } from '$lib/state/space.svelte'
let {handleMenuDesktop, handleMenuMobile} = $props();
@@ -13,7 +13,7 @@
</script>
<div class="flex flex-row italic mb-4">
<div class="flex flex-row italic mb-2">
{#if uiState.menuContentVisible}
<div class="flex-1 p-3 font openonly">
<img src={logo} class="h-5" alt="logo"/>
@@ -26,26 +26,38 @@
<i class="icon-menu pointer block p-2"></i>
</button>
</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">
<Menu label="Conversation" icon="icon-chat" link="/"/>
<Menu label="Tâches" icon="icon-tasks" link="/tasks"/>
<Menu label="Nouveau chat" icon="icon-plus" link="/"/>
<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="Historique" icon="icon-history" link="/history"/>
{#if auth.user?.administrator}
<Menu label="Paramétrage" icon="icon-cog" link="/settings"/>
{/if}
<Menu label="Réglages" icon="icon-cog" link="/settings"/>
</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}
<div class="p-4 flex-1 profile">
<div>Fabien Masson</div>
<div class="flex">
<a href="/profile" class="text-xs flex-1">Modifier</a>
<div class="flex flex-row gap-2 p-2 ">
<div class="py-1 flex-1 flex flex-row text-xs">
<div class="w-6 h-6 rounded-full bg-cyan-600 text-white flex items-center justify-center font-medium">
FM
</div>
<div class="flex-1 ml-2 inline-flex items-center">Fabien Masson</div>
</div>
</div>
<div class="p-2">
<button class="w-full bt" onclick={handleLogout} title="Déconecter">
<a href="/profile" class="bt small inline-flex items-center justify-center" title="Edition profil">
<i class="icon-edit"></i>
</a>
<button class="bt small" onclick={handleLogout} title="Déconnecter">
<i class="icon-logout"></i>
</button>
</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 = {
user: User | null;
status: 'loading' | 'authenticated' | 'unauthenticated';
//accessToken: string | null
user: User | null
status: 'loading' | 'authenticated' | 'unauthenticated'
}
export const auth: AuthState = $state({
user: null,
status: 'loading',
//accessToken: null
})
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 {
id: string
firstname: string
lastname: string
email: string
administrator: boolean
theme: 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
}
@@ -1,5 +1,10 @@
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 {
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?
</div>
@@ -3,6 +3,7 @@
import { page } from '$app/state';
import {uiState} from "$lib/state/ui.svelte"
import { auth } from '$lib/state/auth.svelte'
import { space } from '$lib/state/space.svelte'
import Sidebar from "$lib/components/Sidebar.svelte"
import { goto } from '$app/navigation'
import { onMount } from "svelte";
@@ -39,7 +40,9 @@
onMount(async () => {
handleResize()
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)
applyTheme()
auth.status = 'authenticated'
@@ -64,7 +67,7 @@
</div>
<div id="main">
<div class="flex flex-col h-screen ">
<div class={["header", page.data.theme]}>
<div class={["header", "container" , page.data.theme]}>
<h1 class="flex">
<span class="flex-1">{page.data.title}</span>
{#if page.data.back}
@@ -2,17 +2,28 @@
import { SvelteMap } from 'svelte/reactivity';
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 { miscApi } from '$lib/api/endpoints/misc'
import FormError from '$lib/components/FormError.svelte';
import { applyTheme, themeToLocalStorage } from '$lib/ui/themeswitcher';
import { validateEmail } from '$lib/utilities/validators'
import { onMount } from 'svelte';
import Select from 'svelte-select/no-styles';
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>()
onMount(async ()=> {
addresses = await userApi.listAddresses(form.id)
timezones = (await miscApi.listTimezones()).map((it)=>{return {id:it, text:it}})
})
$effect(()=>{
themeToLocalStorage(form.theme)
applyTheme()
@@ -36,26 +47,43 @@
}
}
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>
<div class="mb-4 formgroup">
<div class="mb-4 formgroup container">
<h2>Mon compte</h2>
<form onsubmit={submitUser} onchange={()=>{errors.clear()}}>
<label for="email" class="block my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
<div class="formelement">
<label for="email" class="my-1 flex-1">Email</label>
<input
id="email"
type="text"
bind:value={form.email}
class="textinput"
autofocus
/>
<FormError key="email" errors={errors} />
</div>
<div class="flex">
<div class="flex-1 mr-2">
<label for="firstname" class="block my-1 flex-1">Prénom</label>
<div class="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="formelement">
<label for="firstname" class=" my-1 flex-1">Prénom</label>
<input
id="firstname"
type="text"
@@ -65,8 +93,8 @@
<FormError key="firstname" errors={errors} />
</div>
<div class="flex-1 ml-2">
<label for="lastname" class="block my-1 flex-1">Nom</label>
<div class="formelement">
<label for="lastname" class="my-1 flex-1">Nom</label>
<input
id="lastname"
type="text"
@@ -77,18 +105,9 @@
</div>
</div>
<div class="flex">
<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="grid grid-cols-1 gap-x-4 md:grid-cols-2">
<div class="flex-1 ml-2">
<div class="formelement">
<label for="lang" class="block my-1 flex-1">Langue</label>
<select bind:value={form.lang} class="textinput">
<option value="en">English</option>
@@ -96,6 +115,22 @@
</select>
<FormError key="lang" errors={errors} />
</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>
<button class="bt w-full">Enregistrer</button>
@@ -104,6 +139,35 @@
</div>
<div class="mb-4 formgroup">
<div class="mb-4 formgroup container">
<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>
@@ -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"
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 {
title: 'Paramétrage : Modèles',
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)) {
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) {
errors.set("verifPassword","Les mots de pass ne correspondent pas.")
}
+162 -14
View File
@@ -8,38 +8,45 @@ html {
@apply h-full;
}
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;
}
hr {
@apply border-stone-200 dark:border-stone-800;
}
h2 {
@apply mb-4 font-bold;
@apply mb-4 text-base font-bold;
}
.textinput {
@apply w-full rounded bg-white p-1 text-stone-900 dark:bg-stone-700 dark:text-stone-100;
.container {
@apply mx-auto max-w-3xl;
}
.pointer {
@apply cursor-pointer;
}
.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 {
@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 {
@apply p-2 text-lg;
@apply py-2 text-lg font-bold;
/*
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 {
h1 {
@@ -56,10 +63,6 @@ h2 {
@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 {
@apply flex h-full flex-row;
}
@@ -97,7 +100,7 @@ h2 {
@apply ml-10;
}
#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 {
#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",
"code": 59400,
"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-logout:before { content: '\e807'; } /* '' */
.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-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
@@ -1,15 +1,15 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?40016728');
src: url('../font/fontello.eot?40016728#iefix') format('embedded-opentype'),
url('../font/fontello.svg?40016728#fontello') format('svg');
src: url('../font/fontello.eot?20532310');
src: url('../font/fontello.eot?20532310#iefix') format('embedded-opentype'),
url('../font/fontello.svg?20532310#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'fontello';
src: url('data:application/octet-stream;base64,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 truncated
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
}
/* 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 */
@@ -17,7 +17,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
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-logout:before { content: '\e807'; } /* '' */
.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-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
@@ -8,6 +8,9 @@
.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-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-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ae;&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-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-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-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0ae;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
+10 -7
View File
@@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?64785462');
src: url('../font/fontello.eot?64785462#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?64785462') format('woff2'),
url('../font/fontello.woff?64785462') format('woff'),
url('../font/fontello.ttf?64785462') format('truetype'),
url('../font/fontello.svg?64785462#fontello') format('svg');
src: url('../font/fontello.eot?1806785');
src: url('../font/fontello.eot?1806785#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?1806785') format('woff2'),
url('../font/fontello.woff?1806785') format('woff'),
url('../font/fontello.ttf?1806785') format('truetype'),
url('../font/fontello.svg?1806785#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
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-logout:before { content: '\e807'; } /* '' */
.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-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
+20 -9
View File
@@ -146,11 +146,11 @@
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?72761720');
src: url('./font/fontello.eot?72761720#iefix') format('embedded-opentype'),
url('./font/fontello.woff?72761720') format('woff'),
url('./font/fontello.ttf?72761720') format('truetype'),
url('./font/fontello.svg?72761720#fontello') format('svg');
src: url('./font/fontello.eot?13627011');
src: url('./font/fontello.eot?13627011#iefix') format('embedded-opentype'),
url('./font/fontello.woff?13627011') format('woff'),
url('./font/fontello.ttf?13627011') format('truetype'),
url('./font/fontello.svg?13627011#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -242,6 +242,17 @@
<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>
</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">
<i class="demo-icon icon-facebook">&#xf09a;</i> <span class="i-name">icon-facebook</span><span class="i-code">0xf09a</span>
</div>
@@ -251,11 +262,11 @@
<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>
</div>
</div>
<div class="row">
<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>
</div>
</div>
<div class="row">
<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>
</div>
@@ -265,11 +276,11 @@
<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>
</div>
</div>
<div class="row">
<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>
</div>
</div>
<div class="row">
<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>
</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="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="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.