renommage lassistanoque

This commit is contained in:
2026-08-07 00:03:15 +02:00
commit 5810c4c94f
73 changed files with 2601 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
lassistanoque.db
+8
View File
@@ -0,0 +1,8 @@
{
"languages": {
"CSS": {
"format_on_save": "on",
"language_servers": ["tailwindcss-language-server"]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

+73
View File
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="80mm"
height="80mm"
viewBox="0 0 80 80"
version="1.1"
id="svg1"
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
sodipodi:docname="dessin.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.5"
inkscape:cx="397"
inkscape:cy="561"
inkscape:window-width="2560"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
inkscape:export-bgcolor="#ffffff00" />
<defs
id="defs1" />
<g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1">
<circle
style="fill:#164450;stroke-width:0.265;stroke-dasharray:none"
id="path1"
cy="45.644962"
cx="23.643471"
r="20" />
<circle
style="fill:#216778;stroke-width:0.264583"
id="path1-3-1-6"
cx="34.202797"
cy="41.673168"
r="17.5" />
<circle
style="fill:#2c89a0;stroke-width:0.264583"
id="path1-3"
cx="45.844456"
cy="38.072063"
r="15" />
<circle
style="fill:#37abc8;stroke-width:0.264583"
id="path1-3-1-3"
cx="56.873432"
cy="34.070667"
r="12.5" />
<circle
style="fill:#5fbcd3;stroke-width:0.264583"
id="path1-3-1"
cx="65.639908"
cy="30.484972"
r="10" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+1
View File
@@ -0,0 +1 @@
LASSISTANOQUE_JWT_SECRET=bCrRlJVsCmMhvKGbZFUbUPeYngvIDqJ6
+25
View File
@@ -0,0 +1,25 @@
MODULE := trankilou.fr/agence
BINARY := agence
CMD := .
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "v0.0.0")
COMMIT := $(shell git rev-parse --short HEAD)
BUILD_DATE := $(shell date -u '+%Y-%m-%dT%H:%M:%SZ')
PKG := trankilou.fr/agence/commands
LDFLAGS := -X '$(PKG).Version=$(VERSION)' \
-X '$(PKG).CommitSHA=$(COMMIT)' \
-X '$(PKG).BuildDate=$(BUILD_DATE)'
.PHONY: run run_%
build:
(cd web; npm run build);
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ../build/lassistanoque_linux_amd64
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ../build/lassistanoque_linux_arm64
GOOS=windows GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ../build/lassistanoque_windows_amd64
GOOS=windows GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ../build/lassistanoque_windows_arm64
GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o ../build/lassistanoque_darwin_amd64
GOOS=darwin GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o ../build/lassistanoque_darwin_arm64
+33
View File
@@ -0,0 +1,33 @@
package cmd
import (
"github.com/spf13/cobra"
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
)
func init() {
rootCmd.AddCommand(
newLigrateCmd(),
)
}
// newVersionCmd creates the version command for displaying version information.
func newLigrateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "migrate",
Short: "migrate database",
Run: func(cmd *cobra.Command, args []string) {
db, err := database.GetDatabase()
if err != nil {
panic(err)
}
defer db.Close()
err = db.Migrate()
if err != nil {
panic(err)
}
},
}
return cmd
}
+24
View File
@@ -0,0 +1,24 @@
package cmd
import (
"os"
"github.com/spf13/cobra"
)
// var (
// cfgFile string
// )
// rootCmd is the root command for the agence CLI.
var rootCmd = &cobra.Command{
Use: "lassistanoque",
Short: "lassistanoque: autonome agent",
}
// Execute is the main entry point for the CLI application.
func Execute() {
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}
}
+78
View File
@@ -0,0 +1,78 @@
package cmd
import (
"log/slog"
"time"
"github.com/spf13/cobra"
"trankilou.fr/lassistanoque/backend/internal/adapter/auth/password"
"trankilou.fr/lassistanoque/backend/internal/adapter/database"
"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/user"
)
func init() {
rootCmd.AddCommand(
newServeCmd(),
)
}
func newServeCmd() *cobra.Command {
var short bool
cmd := &cobra.Command{
Use: "serve",
Short: "Run server",
Run: func(cmd *cobra.Command, args []string) {
runServe()
},
}
cmd.Flags().BoolVarP(&short, "short", "s", false, "show only version number")
return cmd
}
func runServe() {
// database
db, err := database.GetDatabase()
if err != nil {
panic(err)
}
defer db.Close()
err = db.Migrate()
if err != nil {
panic(err)
}
settings, err := db.SettingsReporitory().GetSettings()
if err != nil {
panic(err)
}
// Adapters
tokenManager := security.NewJwtTokenManager(12*time.Hour, 7*24*time.Hour, "lassistanoque")
pwdAuth := password.NewPasswordAuthenticator(tokenManager, db.UserReporitory())
// services
authService := auth.NewService(
db.SettingsReporitory(),
db.UserReporitory(),
map[string]auth.Authenticator{
"password": pwdAuth,
},
)
userService := user.NewService(db.UserReporitory())
// http server
router := http.NewRouter(http.Dependencies{
Settings: settings,
AuthService: authService,
UserService: userService,
TokenManager: tokenManager,
})
if err := router.Start(); err != nil {
slog.Error("failed to start server", "error", err)
}
}
+39
View File
@@ -0,0 +1,39 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(
newVersionCmd(),
)
}
// Version, CommitSHA, and BuildDate are set at build time using ldflags.
var (
Version = "dev"
CommitSHA = "none"
BuildDate = "unknown"
)
// newVersionCmd creates the version command for displaying version information.
func newVersionCmd() *cobra.Command {
var short bool
cmd := &cobra.Command{
Use: "version",
Short: "Show version",
Long: "Display version information for the agence application.",
Run: func(cmd *cobra.Command, args []string) {
if short {
fmt.Println(Version)
return
}
fmt.Printf("lassistanoque %s (commit: %s, built: %s)\n", Version, CommitSHA, BuildDate)
},
}
cmd.Flags().BoolVarP(&short, "short", "s", false, "show only version number")
return cmd
}
@@ -0,0 +1,130 @@
package password
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
type params struct {
memory uint32 // en KiB
iterations uint32
parallelism uint8
saltLength uint32
keyLength uint32
}
var defaultParams = &params{
memory: 64 * 1024, // 64 MB
iterations: 3,
parallelism: 2,
saltLength: 16,
keyLength: 32,
}
// generateSalt crée un salt aléatoire cryptographiquement sûr
func generateSalt(length uint32) ([]byte, error) {
salt := make([]byte, length)
if _, err := rand.Read(salt); err != nil {
return nil, err
}
return salt, nil
}
// HashPassword génère un hash Argon2id encodé (salt + hash + paramètres inclus)
func HashPassword(password string) (string, error) {
salt, err := generateSalt(defaultParams.saltLength)
if err != nil {
return "", err
}
hash := argon2.IDKey(
[]byte(password),
salt,
defaultParams.iterations,
defaultParams.memory,
defaultParams.parallelism,
defaultParams.keyLength,
)
// Format standard: $argon2id$v=19$m=65536,t=3,p=2$salt$hash
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encoded := fmt.Sprintf(
"$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version,
defaultParams.memory,
defaultParams.iterations,
defaultParams.parallelism,
b64Salt,
b64Hash,
)
return encoded, nil
}
// VerifyPassword compare un mot de passe en clair avec un hash stocké
func VerifyPassword(password, encodedHash string) (bool, error) {
p, salt, hash, err := decodeHash(encodedHash)
if err != nil {
return false, err
}
otherHash := argon2.IDKey(
[]byte(password),
salt,
p.iterations,
p.memory,
p.parallelism,
p.keyLength,
)
// Comparaison en temps constant pour éviter les timing attacks
if subtle.ConstantTimeCompare(hash, otherHash) == 1 {
return true, nil
}
return false, nil
}
// decodeHash parse la chaîne encodée pour en extraire params, salt et hash
func decodeHash(encodedHash string) (*params, []byte, []byte, error) {
parts := strings.Split(encodedHash, "$")
if len(parts) != 6 {
return nil, nil, nil, errors.New("format de hash invalide")
}
var version int
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
if err != nil {
return nil, nil, nil, err
}
if version != argon2.Version {
return nil, nil, nil, errors.New("version argon2 incompatible")
}
p := &params{}
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.memory, &p.iterations, &p.parallelism)
if err != nil {
return nil, nil, nil, err
}
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil {
return nil, nil, nil, err
}
p.saltLength = uint32(len(salt))
hash, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil {
return nil, nil, nil, err
}
p.keyLength = uint32(len(hash))
return p, salt, hash, nil
}
@@ -0,0 +1,75 @@
package password
import (
"context"
"fmt"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
)
type PasswordAuthenticator struct {
tokenManager auth.TokenManager
userRepository domain.UserRepository
}
func NewPasswordAuthenticator(
tokenManager auth.TokenManager,
userRepository domain.UserRepository,
) *PasswordAuthenticator {
return &PasswordAuthenticator{
tokenManager,
userRepository,
}
}
func (s *PasswordAuthenticator) Authenticate(ctx context.Context, creds auth.Credentials) (*auth.Session, error) {
user, err := s.userRepository.FindUserByEmail(creds.Email)
if err != nil {
return nil, fmt.Errorf("Unauthorized user %s : %s", creds.Email, err)
}
ok, err := VerifyPassword(creds.Password, user.Password)
if !ok {
return nil, fmt.Errorf("Password don't match")
}
if err != nil {
return nil, fmt.Errorf("Error verifying password: %s", err)
}
accessToken, expiresAt, err := s.tokenManager.GenerateAccessToken(user)
if err != nil {
return nil, fmt.Errorf("Unable to generate access token: %s", err)
}
refreshToken, _, err := s.tokenManager.GenerateRefreshToken(user.ID)
if err != nil {
return nil, fmt.Errorf("Unable to generate refresh token: %s", err)
}
return &auth.Session{
User: user,
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresAt: expiresAt,
}, nil
}
func (s *PasswordAuthenticator) Register(ctx context.Context, registration auth.Registration) (*auth.Session, error) {
hashedPassord, err := HashPassword(registration.Password)
if err != nil {
return nil, err
}
user, err := s.userRepository.CreateUser(&domain.User{
Email: registration.Email,
Firstname: registration.Firstname,
Lastname: registration.Lastname,
Password: hashedPassord,
})
if err != nil {
return nil, err
}
_ = user
return nil, nil
}
@@ -0,0 +1,7 @@
package dberrors
import "errors"
var (
ErrNoRowUpdated = errors.New("No row was updated")
)
@@ -0,0 +1,26 @@
package database
import (
"fmt"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/turso"
"trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type Database interface {
SettingsReporitory() domain.SettingsRepository
UserReporitory() domain.UserRepository
Migrate() error
Close()
}
func GetDatabase() (Database, error) {
cfg := config.GetConfig()
switch cfg.DatabaseType {
case "turso":
return turso.NewTursoDB(cfg)
default:
}
return nil, fmt.Errorf("Database %s not implemented", cfg.DatabaseType)
}
@@ -0,0 +1,69 @@
package turso
import (
"embed"
"fmt"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/domain"
_ "turso.tech/database/tursogo"
)
//go:embed migrations/*
var FS embed.FS
type TursoDB struct {
DB *sqlx.DB
}
func NewTursoDB(cfg *config.Config) (*TursoDB, error) {
db, err := sqlx.Connect("turso", cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("error connecting to db %: %s", cfg.DatabaseURL, err)
}
return &TursoDB{
DB: db,
}, nil
}
func (db *TursoDB) UserReporitory() domain.UserRepository {
return &TursoUserRepository{
DB: db.DB,
}
}
func (db *TursoDB) SettingsReporitory() domain.SettingsRepository {
return &TursoSettingsRepository{
DB: db.DB,
}
}
func (db *TursoDB) Close() {
db.DB.Close()
}
func (db *TursoDB) Migrate() error {
sqldb := db.DB.DB
dbDriver, err := sqlite3.WithInstance(sqldb, &sqlite3.Config{})
if err != nil {
return err
}
sourceInstance, err := iofs.New(FS, "migrations")
if err != nil {
return err
}
m, err := migrate.NewWithInstance("iofs", sourceInstance, "sqlite3", dbDriver)
if err != nil {
return err
}
m.Up()
return nil
}
@@ -0,0 +1,10 @@
drop table providers;
drop table models;
drop table system;
drop table oidc;
drop table users;
drop table user_addresses;
drop table channels;
drop table tools;
drop table tasks;
drop table history;
@@ -0,0 +1,95 @@
create table providers (
id text not null primary key,
name text,
key text,
url text,
_version text
);
create table models (
id text not null primary key,
provider_id text,
name text,
modelname text,
configuration text,
_version text
);
create table settings (
chat_model_id text,
default_lang text,
register_enabled numeric,
password_enabled numeric,
_version text
);
create table oidc (
id string not null primary key,
label string,
domain string,
client_id string,
client_secret string,
wellknown_url string,
_version text
);
create table users (
id string not null primary key,
firstname string not null default '',
lastname string not null default '',
password string not null default '',
email string not null unique,
picture string not null default '',
enabled numeric default true,
administrator numeric default false,
_version text not null
);
create table user_addresses (
id string not null primary key,
user_id string,
type string,
address string,
_version text
);
create table channels (
id string not null primary key,
name string,
type string,
enabled numeric,
configuration string,
_version text
);
create table tools (
id string not null primary key,
name string,
type string,
enabled numeric,
configuration string,
_version text
);
create table tasks (
id string not null primary key,
owner_id string,
model_id string,
label string,
prompt string,
cron string,
status string,
next_datetime numeric,
_version text
);
create table history (
id string not null primary key,
task_id string,
start_datetimle numeric,
end_datetime numeric,
prompt string,
log string,
response string,
_version text
);
@@ -0,0 +1 @@
delete from sytem;
@@ -0,0 +1,2 @@
insert into settings (default_lang, register_enabled, password_enabled, _version)
values ('en', true, true, 'init');
@@ -0,0 +1,45 @@
package turso
import (
"github.com/jmoiron/sqlx"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility"
)
type TursoSettingsRepository struct {
DB *sqlx.DB
}
func (sr *TursoSettingsRepository) GetSettings() (*domain.Settings, error) {
var settings domain.Settings
err := sr.DB.Get(&settings, "select * from settings limit 1")
return &settings, err
}
func (sr *TursoSettingsRepository) UpdateSettings(setting *domain.Settings) (*domain.Settings, error) {
newVersion := utility.GenID()
res, err := sr.DB.Exec(
`update settings
set chat_model_id=$1,
default_lang=$2,
register_enabled=$3,
password_enabled=$4,
_version=:$5
where _version=$6`,
setting.ChatModelID, setting.DefaultLang, setting.RegisterEnabled,
setting.PasswordEnabled, newVersion, setting.VersionId,
)
if err != nil {
return nil, err
}
if count, _ := res.RowsAffected(); count == 0 {
return nil, dberrors.ErrNoRowUpdated
}
setting.VersionId = newVersion
return setting, nil
}
@@ -0,0 +1,93 @@
package turso
import (
"github.com/jmoiron/sqlx"
"trankilou.fr/lassistanoque/backend/internal/adapter/database/dberrors"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/utility"
)
type TursoUserRepository struct {
DB *sqlx.DB
}
func (ur *TursoUserRepository) FindUser(id string) (*domain.User, error) {
var user domain.User
err := ur.DB.Get(&user, "select * from users where id=$1", id)
return &user, err
}
func (ur *TursoUserRepository) FindUserByEmail(email string) (*domain.User, error) {
var user domain.User
err := ur.DB.Get(&user, "select * from users where email=$1", email)
return &user, err
}
func (ur *TursoUserRepository) CountUsers() (int, error) {
var count int
err := ur.DB.Get(&count, "select count(*) from users")
return count, err
}
func (ur *TursoUserRepository) ListUsers() ([]*domain.User, error) {
var users []*domain.User
err := ur.DB.Select(&users, "select * from users order by lastname, firstname")
return users, err
}
func (ur *TursoUserRepository) CreateUser(user *domain.User) (*domain.User, error) {
count, err := ur.CountUsers()
if err != nil {
return nil, err
}
if count == 0 {
user.Administrator = true
}
user.ID = utility.GenID()
user.VersionId = utility.GenID()
ur.DB.NamedExec(
`insert into users (id, email, firstname, lastname, enabled, password, administrator, _version)
values (:id, :email, :firstname, :lastname, :enabled, :password, :administrator, :_version)`,
user)
return nil, nil
}
func (ur *TursoUserRepository) UpdateUser(user *domain.User) (*domain.User, error) {
newVersion := utility.GenID()
res, err := ur.DB.Exec(
`update users
set email=$1,
firstname=$2,
lastname=$3,
enabled=$4,
administrator=$5
_version=:$6
where id=$7 and _version=$8`,
user.Email, user.Firstname, user.Lastname,
user.Enabled, user.Administrator,
newVersion, user.ID, user.VersionId,
)
if err != nil {
return nil, err
}
if count, _ := res.RowsAffected(); count == 0 {
return nil, dberrors.ErrNoRowUpdated
}
user.VersionId = newVersion
return user, nil
}
func (ur *TursoUserRepository) DeleteUser(id string) error {
res, err := ur.DB.Exec("delete from users where id=$1", id)
if count, _ := res.RowsAffected(); count == 0 {
return dberrors.ErrNoRowUpdated
}
return err
}
+129
View File
@@ -0,0 +1,129 @@
package security
import (
"errors"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
)
// Claims personnalisées embarquées dans le JWT.
type Claims struct {
UserID string `json:"uid"`
Email string `json:"email"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Administrator bool `json:"administrator"`
jwt.RegisteredClaims
}
type JwtTokenManager struct {
secret []byte
accessTTL time.Duration
refreshTTL time.Duration
issuer string
}
func NewJwtTokenManager(accessTTL, refreshTTL time.Duration, issuer string) *JwtTokenManager {
cfg := config.GetConfig()
return &JwtTokenManager{
secret: []byte(cfg.JWTSecret),
accessTTL: accessTTL,
refreshTTL: refreshTTL,
issuer: issuer,
}
}
// GenerateAccessToken crée un JWT de courte durée (ex: 15 min) utilisé
// pour authentifier les requêtes API.
func (tm *JwtTokenManager) GenerateAccessToken(user *domain.User) (string, time.Time, error) {
expiresAt := time.Now().Add(tm.accessTTL)
claims := Claims{
UserID: user.ID,
Email: user.Email,
Firstname: user.Firstname,
Lastname: user.Lastname,
Administrator: user.Administrator,
RegisteredClaims: jwt.RegisteredClaims{
Issuer: tm.issuer,
Subject: user.ID,
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(tm.secret)
return signed, expiresAt, err
}
// GenerateRefreshToken crée un token de longue durée (ex: 7 jours) utilisé
// uniquement pour obtenir un nouvel access token, jamais pour appeler l'API directement.
func (tm *JwtTokenManager) GenerateRefreshToken(userID string) (string, time.Time, error) {
expiresAt := time.Now().Add(tm.refreshTTL)
claims := jwt.RegisteredClaims{
Issuer: tm.issuer,
Subject: userID,
ExpiresAt: jwt.NewNumericDate(expiresAt),
IssuedAt: jwt.NewNumericDate(time.Now()),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString(tm.secret)
return signed, expiresAt, err
}
// ParseAndValidate décode et vérifie la signature + l'expiration d'un JWT.
func (tm *JwtTokenManager) ParseAndValidate(tokenString string) (*domain.User, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("méthode de signature inattendue")
}
return tm.secret, nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, errors.New("token invalide")
}
return &domain.User{
ID: claims.UserID,
Email: claims.Email,
Firstname: claims.Firstname,
Lastname: claims.Lastname,
Administrator: claims.Administrator,
}, nil
}
func (tm *JwtTokenManager) TokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c *echo.Context) error {
header := c.Request().Header.Get("Authorization")
if header == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "en-tête Authorization manquant")
}
parts := strings.SplitN(header, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
return echo.NewHTTPError(http.StatusUnauthorized, "format attendu: Bearer <token>")
}
claims, err := tm.ParseAndValidate(parts[1])
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "token invalide ou expiré")
}
// 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.ContextAdminKey, claims.Administrator)
return next(c)
}
}
+56
View File
@@ -0,0 +1,56 @@
package config
import (
"log"
"os"
"strconv"
"github.com/joho/godotenv"
)
const (
DEFAULT_DB_TYPE = "turso"
DEFAULT_DB_URL = "lassistanoque.db"
DEFAULT_HTTP_PORT = 3000
)
type Config struct {
DatabaseType string
DatabaseURL string
HttpPort int
JWTSecret string
}
var config *Config
func GetConfig() *Config {
if config == nil {
godotenv.Load()
databaseType := os.Getenv("LASSISTANOQUE_DB_TYPE")
if databaseType == "" {
databaseType = DEFAULT_DB_TYPE
}
databaseURL := os.Getenv("LASSISTANOQUE_DB_URL")
if databaseURL == "" {
databaseURL = DEFAULT_DB_URL
}
httpPort, _ := strconv.Atoi(os.Getenv("LASSISTANOQUE_HTTP_PORT"))
if httpPort == 0 {
httpPort = DEFAULT_HTTP_PORT
}
jwtsecret := os.Getenv("LASSISTANOQUE_JWT_SECRET")
if jwtsecret == "" {
log.Fatal("LASSISTANOQUE_JWT_SECRET must be set")
}
config = &Config{
DatabaseType: databaseType,
DatabaseURL: databaseURL,
HttpPort: httpPort,
}
}
return config
}
+16
View File
@@ -0,0 +1,16 @@
package domain
import "database/sql"
type Settings struct {
ChatModelID sql.NullString `db:"chat_model_id"`
DefaultLang string `db:"default_lang"`
RegisterEnabled bool `db:"register_enabled"`
PasswordEnabled bool `db:"password_enabled"`
VersionId string `db:"_version"`
}
type SettingsRepository interface {
GetSettings() (*Settings, error)
UpdateSettings(config *Settings) (*Settings, error)
}
+23
View File
@@ -0,0 +1,23 @@
package domain
type User struct {
ID string `db:"id" json:"id"`
Firstname string `db:"firstname" json:"firstname,omitempty"`
Lastname string `db:"lastname" json:"lastname,omitempty"`
Password string `db:"password" json:"-"`
Email string `db:"email" json:"email,omitempty"`
PictureID string `db:"picture" json:"picture,omitempty"`
Enabled bool `db:"enabled" json:"enabled"`
Administrator bool `db:"administrator" json:"administrator"`
VersionId string `db:"_version" json:"-"`
}
type UserRepository interface {
CountUsers() (int, error)
FindUser(id string) (*User, error)
FindUserByEmail(email string) (*User, error)
ListUsers() ([]*User, error)
CreateUser(user *User) (*User, error)
UpdateUser(user *User) (*User, error)
DeleteUser(id string) error
}
+91
View File
@@ -0,0 +1,91 @@
package handlers
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
)
func NewAuthGroup(prefix string, e *echo.Group, service *auth.Service) *echo.Group {
authHandler := &AuthHandler{
authService: service,
}
auth := e.Group(prefix)
auth.POST("/login", authHandler.Login)
auth.GET("/oidc/callback", authHandler.OIDCCallback)
auth.POST("/register", authHandler.Register)
return auth
}
type AuthHandler struct {
authService *auth.Service
}
type loginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
type loginResponse struct {
AccessToken string `json:"accessToken"`
RefreshToken string `json:"refreshToken"`
ExpiresAt string `json:"expiresAt"`
User *domain.User `json:"user"`
}
func (h AuthHandler) Login(c *echo.Context) error {
var req loginRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "invalid request")
}
if req.Email == "" || req.Password == "" {
return echo.NewHTTPError(http.StatusBadRequest, "email and password are required")
}
creds := auth.Credentials{
Method: "password",
Email: req.Email,
Password: req.Password,
}
session, err := h.authService.Login(context.Background(), creds)
if err != nil {
c.Logger().Error(fmt.Sprintf("error while login: %s", err))
return echo.NewHTTPError(http.StatusUnauthorized, "login error")
}
resp := loginResponse{
AccessToken: session.AccessToken,
RefreshToken: session.RefreshToken,
ExpiresAt: session.ExpiresAt.Format("2006-01-02T15:04:05Z07:00"),
}
resp.User = session.User
return c.JSON(http.StatusOK, resp)
}
func (h AuthHandler) OIDCCallback(c *echo.Context) error {
return nil
}
func (h AuthHandler) Register(c *echo.Context) error {
reader := c.Request().Body
var registration auth.Registration
decoder := json.NewDecoder(reader)
err := decoder.Decode(&registration)
if err != nil {
return c.String(http.StatusBadRequest, "malformatted registration")
}
user, err := h.authService.Register(registration)
if err != nil {
return c.String(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, user)
}
+33
View File
@@ -0,0 +1,33 @@
package handlers
import (
"net/http"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
func NewUserGroup(prefix string, e *echo.Group, service *user.Service, middlewares ...echo.MiddlewareFunc) *echo.Group {
userHandler := &UserHandler{
userService: service,
}
_ = userHandler
auth := e.Group(prefix, middlewares...)
auth.GET("/me", userHandler.Me)
// auth.GET("/register", userHandler.Register)
return auth
}
type UserHandler struct {
userService *user.Service
}
func (h UserHandler) Me(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
user, err := h.userService.GetUser(userID)
if err != nil {
return echo.NewHTTPError(http.StatusForbidden, err.Error())
}
return c.JSON(http.StatusOK, user)
}
+85
View File
@@ -0,0 +1,85 @@
package http
import (
"embed"
"fmt"
"io/fs"
"net/http"
"github.com/labstack/echo/v5"
"github.com/labstack/echo/v5/middleware"
"trankilou.fr/lassistanoque/backend/internal/config"
"trankilou.fr/lassistanoque/backend/internal/domain"
"trankilou.fr/lassistanoque/backend/internal/http/handlers"
"trankilou.fr/lassistanoque/backend/internal/service/auth"
"trankilou.fr/lassistanoque/backend/internal/service/user"
)
//go:embed web/_app/*
var appFiles embed.FS
//go:embed web/assets/*
var assetsFiles embed.FS
//go:embed web/index.html
var indexhtml []byte
//go:embed web/robots.txt
var robotstxt []byte
type Router struct {
settings *domain.Settings
echo *echo.Echo
}
type Dependencies struct {
Settings *domain.Settings
AuthService *auth.Service
UserService *user.Service
TokenManager auth.TokenManager
}
func NewRouter(deps Dependencies) *Router {
e := echo.New()
e.Use(middleware.RequestLogger())
e.Use(middleware.CORS("http://localhost:5173"))
// SPA
fsapp, err := fs.Sub(appFiles, "web/_app")
if err != nil {
panic(err)
}
fsassets, err := fs.Sub(assetsFiles, "web/assets")
if err != nil {
panic(err)
}
appHandler := http.FileServer(http.FS(fsapp))
assetsHandler := http.FileServer(http.FS(fsassets))
e.GET("/_app/*", echo.WrapHandler(http.StripPrefix("/_app/", appHandler)))
e.GET("/assets/*", echo.WrapHandler(http.StripPrefix("/assets/", assetsHandler)))
e.GET("/robots.txt", func(c *echo.Context) error {
return c.Blob(http.StatusOK, "plain/text", robotstxt)
})
e.GET("/*", func(c *echo.Context) error {
fmt.Println("match /*")
fmt.Println(string(indexhtml))
return c.Blob(http.StatusOK, "text/html", indexhtml)
})
// API
api := e.Group("/api")
_ = handlers.NewAuthGroup("/auth", api, deps.AuthService)
_ = handlers.NewUserGroup("/user", api, deps.UserService, deps.TokenManager.TokenMiddleware)
//_ = handlers.NewUserGroup("/user", e, deps.UserService)
return &Router{
settings: deps.Settings,
echo: e,
}
}
func (r *Router) Start() error {
cfg := config.GetConfig()
return r.echo.Start(fmt.Sprintf(":%d", cfg.HttpPort))
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,81 @@
var e=({status:e,message:t})=>`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>`+t+`</title>
<style>
body {
--bg: white;
--fg: #222;
--divider: #ccc;
background: var(--bg);
color: var(--fg);
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
}
.error {
display: flex;
align-items: center;
max-width: 32rem;
margin: 0 1rem;
}
.status {
font-weight: 200;
font-size: 3rem;
line-height: 1;
position: relative;
top: -0.05rem;
}
.message {
border-left: 1px solid var(--divider);
padding: 0 0 0 1rem;
margin: 0 0 0 1rem;
min-height: 2.5rem;
display: flex;
align-items: center;
}
.message h1 {
font-weight: 400;
font-size: 1em;
margin: 0;
}
@media (prefers-color-scheme: dark) {
body {
--bg: #222;
--fg: #ddd;
--divider: #666;
}
}
</style>
</head>
<body>
<div class="error">
<span class="status">`+e+`</span>
<div class="message">
<h1>`+t+`</h1>
</div>
</div>
</body>
</html>
`;export{e as default};
@@ -0,0 +1 @@
import"./tTEyPFub.js";
@@ -0,0 +1 @@
var e=`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAANQAAAAmCAYAAAC8hLUKAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAABaBJREFUeJztnV1oHFUUx//n7ibt7mxChVRtfRKKDxZjtVpEKa0ULba7WysoVfyoKNT6JEoRRVDxAwRbXwqCffGDIGgpdXdR0KqtilK1tlFRY0GpVRttjJrsbJsmc48PSXdmtpuZvTuzZFPP7+2ePff8z8zOP9z52hAzQxCEeFAz3YAgnE0k6a3yxSDaGLnSBL3AN6WPRW9JmCmoUFkI8AM14a2ct/6YkYZaABUqVwB8ixuAwznrkbjqJ0FqEYi3RK7UyX0AxFCzm/NA8B8Lil8FcNYYCkBvzTY6AGIzlCz5BCFGxFCCECPJaeKHAWijShoLqDQ6Vh2P0zCvz/xpUoLeGe6G07HQF8x2DTAglyKFWUF9Qznjy/jGef+YFKKi3Q9WvdVAAtsAPGTUjTMnC0afL1aChSwqRnUEYYaQJZ8gxIgYShBiRAwlCDEy3UWJmWGc96AD1/piB3AS2RnqRxAMaStDTV0V9F8ZFDMJswhZ8glCjIihBCFG6i/5kp0bqGQb3vuhTwDud4f4MnRGyV4F4ILApBNWH98Mx6yXGp2C3QuFJVFqgMGcs14L1NlVWYBOvs6sMI1wNr3baMZeJFG2bzPTqVeIv+C1me+NphTLi0G0NDhLvc/Z1G9mdU9cBdIXmcyBwte8xjoUWLdkXw/gfDeAq2tTqGTfGaijcYzz1nuNtFTfUMwvNjLZ3xYv4azVH57oQWMLCKsDc1LYCUS8satoPZifiFRjkkBDoZMvAeMVs5J8GICRoTAylAKlDHXqSdODAIwMBaa1AJ4LScoBMDIU4NwDpnvNpvCzAAINBcajAFYEZKjQ70zhQwANGUqWfIIQI2IoQYgRMZQgxEj9c6gJ3YNzuv6ddla50gvmA5HVD1prsMJj6hF7AyjkPKUBqGj/AGBRNcD8NLozHZGKHj/ziXcq2S+Dcburgz3otsx0yqOLqGhP+GJaXcPrUvtdncp9YN7uCqdG0WWoU4+VDVzs0XSIirY7VtiKTIj2qL2LirZ7Xkh4k7PWrd4UKto2gDnVAKvN6E5vbrDzKZ3KwzX77ijnrAt9OSetVZgPqo7LlY1g3uHJcNBlzQ3U2Qfd6P3Q+oYix+GVmKj7GQAqcaSrbqfhx6HheU2EStCxvKhBlARzwh2DgranaRgJAF4dZapDb0PD8dQAAOU5AACAtQLIk0PJlmxPffy96fB9SSWoqX1zOpKok5asrW2874o1/fGZx3PtFWIqQNfs3XDdlY33JEs+QYgRMZQgxEhzz/J1OEdxKrnJH+S7qVBOV4cK73I2szNKc0IbwPwkSP1eHSsKvu/TvNAdVChfaTSF6LLW9NI8TRmKV3cPA3jJG6Oi3Q8i941djVEAYqjZTgI7eW3625brEJYDtLzlOi1GlnyCECNiKEGIkbZ6H0r4X3MUjL8iVSAMxtRL04ihhPaA6SnOp3eEJ7Y3suQThBgRQwlCjIihBCFG2v8ciu1PqeB5wk/xM6E3jB2dByn3wUtFeSrYByP1QWDOWZf7YonEY5jQ2zzNLjbWITUIJn9dlRrw5/Ab0Ooz93OaG3l7AEDhec5afeGJLYCwDJrcP+iK74rhOxrknHVDYM6cid0YS7o6CahQXeLPOZfZFJgzRfsbCrjU/zCj6gmbwOsy33nHVKrkQRFfga+ns2buEQBHXB17PthY5zDn04FfKGe7hgAMVXUKQ12gVPTtYZwbuUaz0jVvd1OxfD+Iov5Mwa+hKZMPJQxXdfciiVE7WJfo70ZbkCWfIMSIGEoQYiSJhP4JrLb7ouPzxqbJnx7C6wB95Bl/bF6DBgBsD8zRzjfGdTXvh6LgumFwI29q6V9ACTMdDaN/+QMA6Og5BacSbXsmxf1LzQ46Dqdm/4+R+c1WpiIIP3sCX4VPog9AdNJYy1dCN7w0q7IPGktDjg3WPzZa7j+UbbXRpuZZaAAAAABJRU5ErkJggg==`;export{e as t};
@@ -0,0 +1 @@
import{P as e}from"./BXe04aPf.js";var t=e({user:null,accessToken:null,status:`loading`}),n=()=>{t.status=`unauthenticated`};export{n,t};
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
typeof window<`u`&&((window.__svelte??={}).v??=new Set).add(`5`);
@@ -0,0 +1,2 @@
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.DUhcgpvL.js","../chunks/BXe04aPf.js","../chunks/xihTtKlq.js","../assets/0.CHStj0oN.css","../nodes/1.Dh_qEDnK.js","../chunks/tTEyPFub.js","../nodes/2.C55SYr5i.js","../chunks/kaCwo2dy.js","../chunks/Bthvo3p_.js","../nodes/3.BZSqsgG4.js","../chunks/bT040zHf.js","../nodes/4.5uyrhTtI.js","../nodes/5.z-y4HE0j.js"])))=>i.map(i=>d[i]);
import{C as e,D as t,F as n,G as r,H as i,I as a,L as o,M as s,N as c,O as l,T as u,V as d,_ as f,a as p,f as m,g as h,h as g,i as _,j as v,k as y,m as b,n as x,r as S,v as C,y as w}from"../chunks/BXe04aPf.js";import"../chunks/xihTtKlq.js";var T=`modulepreload`,E=function(e,t){return new URL(e,t).href},D={},O=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}function s(e){return import.meta.resolve?import.meta.resolve(e):new URL(e,import.meta.url).href}r=o(t.map(t=>{if(t=E(t,n),t=s(t),t in D)return;D[t]=!0;let r=t.endsWith(`.css`);for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}let i=document.createElement(`link`);if(i.rel=r?`stylesheet`:T,r||(i.as=`script`),i.crossOrigin=``,i.href=t,a&&i.setAttribute(`nonce`,a),document.head.appendChild(i),r)return new Promise((e,n)=>{i.addEventListener(`load`,e),i.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},k={},A=C(`<div id="svelte-announcer" aria-live="assertive" aria-atomic="true" style="position: absolute; left: 0; top: 0; clip: rect(0 0 0 0); clip-path: inset(50%); overflow: hidden; white-space: nowrap; width: 1px; height: 1px"><!></div>`),j=C(`<!> <!>`,1);function M(S,C){i(C,!0);let T=_(C,`components`,23,()=>[]),E=_(C,`data_0`,3,null),D=_(C,`data_1`,3,null),O=_(C,`data_2`,3,null);y(()=>C.stores.page.set(C.page)),l(()=>{C.stores,C.page,C.constructors,T(),C.form,E(),D(),O(),C.stores.page.notify()});let k=a(!1),M=a(!1),N=a(null);x(()=>{let t=C.stores.page.subscribe(()=>{e(k)&&(n(M,!0),u().then(()=>{n(N,document.title||`untitled page`,!0)}))});return n(k,!0),t});let P=o(()=>C.constructors[2]);var F=j(),I=s(F),L=t=>{let n=o(()=>C.constructors[0]);var r=f(),i=s(r);m(i,()=>e(n),(t,n)=>{p(n(t,{get data(){return E()},get form(){return C.form},get params(){return C.page.params},children:(t,n)=>{var r=f(),i=s(r),a=t=>{let n=o(()=>C.constructors[1]);var r=f(),i=s(r);m(i,()=>e(n),(t,n)=>{p(n(t,{get data(){return D()},get form(){return C.form},get params(){return C.page.params},children:(t,n)=>{var r=f(),i=s(r);m(i,()=>e(P),(e,t)=>{p(t(e,{get data(){return O()},get form(){return C.form},get params(){return C.page.params}}),e=>T()[2]=e,()=>T()?.[2])}),h(t,r)},$$slots:{default:!0}}),e=>T()[1]=e,()=>T()?.[1])}),h(t,r)},c=t=>{let n=o(()=>C.constructors[1]);var r=f(),i=s(r);m(i,()=>e(n),(e,t)=>{p(t(e,{get data(){return D()},get form(){return C.form},get params(){return C.page.params}}),e=>T()[1]=e,()=>T()?.[1])}),h(t,r)};b(i,e=>{C.constructors[2]?e(a):e(c,-1)}),h(t,r)},$$slots:{default:!0}}),e=>T()[0]=e,()=>T()?.[0])}),h(t,r)},R=t=>{let n=o(()=>C.constructors[0]);var r=f(),i=s(r);m(i,()=>e(n),(e,t)=>{p(t(e,{get data(){return E()},get form(){return C.form},get params(){return C.page.params}}),e=>T()[0]=e,()=>T()?.[0])}),h(t,r)};b(I,e=>{C.constructors[1]?e(L):e(R,-1)});var z=c(I,2),B=n=>{var i=A(),a=v(i),o=n=>{var r=w();t(()=>g(r,e(N))),h(n,r)};b(a,t=>{e(M)&&t(o)}),r(i),h(n,i)};b(z,t=>{e(k)&&t(B)}),h(S,F),d()}var N=S(M),P=[()=>O(()=>import(`../nodes/0.DUhcgpvL.js`),__vite__mapDeps([0,1,2,3]),import.meta.url),()=>O(()=>import(`../nodes/1.Dh_qEDnK.js`),__vite__mapDeps([4,1,5,2]),import.meta.url),()=>O(()=>import(`../nodes/2.C55SYr5i.js`),__vite__mapDeps([6,1,5,2,7,8]),import.meta.url),()=>O(()=>import(`../nodes/3.BZSqsgG4.js`),__vite__mapDeps([9,1,2,7,10]),import.meta.url),()=>O(()=>import(`../nodes/4.5uyrhTtI.js`),__vite__mapDeps([11,1,5,2,7,8,10]),import.meta.url),()=>O(()=>import(`../nodes/5.z-y4HE0j.js`),__vite__mapDeps([12,1,5,2,7,8,10]),import.meta.url)],F=[],I={"/(authenticated)":[3,[2]],"/(public)/login":[4],"/(public)/register":[5]},L={handleError:(({error:e})=>{console.error(e)}),reroute:(()=>{}),transport:{}},R=Object.fromEntries(Object.entries(L.transport).map(([e,t])=>[e,t.decode])),z=Object.fromEntries(Object.entries(L.transport).map(([e,t])=>[e,t.encode])),B=!1,V=(e,t)=>R[e](t),H=()=>O(()=>import(`../chunks/Bjy-W4x2.js`).then(e=>e.default),[],import.meta.url);export{V as decode,R as decoders,I as dictionary,z as encoders,H as get_error_template,B as hash,L as hooks,k as matchers,P as nodes,N as root,F as server_loads};
@@ -0,0 +1 @@
import{n as e,s as t}from"../chunks/tTEyPFub.js";export{t as load_css,e as start};
@@ -0,0 +1 @@
import{D as e,K as t,M as n,W as r,_ as i,c as a,d as o,g as s,p as c,v as l}from"../chunks/BXe04aPf.js";import"../chunks/xihTtKlq.js";var u=t({prerender:()=>!1,ssr:()=>!1}),d=`data:image/svg+xml,%3c?xml%20version='1.0'%20encoding='UTF-8'%20standalone='no'?%3e%3c!--%20Created%20with%20Inkscape%20(http://www.inkscape.org/)%20--%3e%3csvg%20width='80mm'%20height='80mm'%20viewBox='0%200%2080%2080'%20version='1.1'%20id='svg1'%20inkscape:version='1.4.4%20(dcaf3e7d9e,%202026-05-05)'%20sodipodi:docname='dessin.svg'%20xmlns:inkscape='http://www.inkscape.org/namespaces/inkscape'%20xmlns:sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd'%20xmlns='http://www.w3.org/2000/svg'%20xmlns:svg='http://www.w3.org/2000/svg'%3e%3csodipodi:namedview%20id='namedview1'%20pagecolor='%23ffffff'%20bordercolor='%23000000'%20borderopacity='0.25'%20inkscape:showpageshadow='2'%20inkscape:pageopacity='0.0'%20inkscape:pagecheckerboard='0'%20inkscape:deskcolor='%23d1d1d1'%20inkscape:document-units='mm'%20inkscape:zoom='0.5'%20inkscape:cx='397'%20inkscape:cy='561'%20inkscape:window-width='2560'%20inkscape:window-height='1011'%20inkscape:window-x='0'%20inkscape:window-y='0'%20inkscape:window-maximized='1'%20inkscape:current-layer='layer1'%20inkscape:export-bgcolor='%23ffffff00'%20/%3e%3cdefs%20id='defs1'%20/%3e%3cg%20inkscape:label='Calque%201'%20inkscape:groupmode='layer'%20id='layer1'%3e%3ccircle%20style='fill:%23164450;stroke-width:0.265;stroke-dasharray:none'%20id='path1'%20cy='45.644962'%20cx='23.643471'%20r='20'%20/%3e%3ccircle%20style='fill:%23216778;stroke-width:0.264583'%20id='path1-3-1-6'%20cx='34.202797'%20cy='41.673168'%20r='17.5'%20/%3e%3ccircle%20style='fill:%232c89a0;stroke-width:0.264583'%20id='path1-3'%20cx='45.844456'%20cy='38.072063'%20r='15'%20/%3e%3ccircle%20style='fill:%2337abc8;stroke-width:0.264583'%20id='path1-3-1-3'%20cx='56.873432'%20cy='34.070667'%20r='12.5'%20/%3e%3ccircle%20style='fill:%235fbcd3;stroke-width:0.264583'%20id='path1-3-1'%20cx='65.639908'%20cy='30.484972'%20r='10'%20/%3e%3c/g%3e%3c/svg%3e`,f=l(`<link rel="icon"/> <link rel="stylesheet" href="/assets/fontello/css/fontello.css"/>`,1);function p(t,l){var u=i();o(`12qhfyh`,t=>{var i=f(),o=n(i);r(2),e(()=>a(o,`href`,d)),s(t,i)});var p=n(u);c(p,()=>l.children),s(t,u)}export{p as component,u as universal};
@@ -0,0 +1 @@
import{D as e,G as t,H as n,M as r,N as i,V as a,g as o,h as s,j as c,v as l}from"../chunks/BXe04aPf.js";import{a as u,r as d}from"../chunks/tTEyPFub.js";import"../chunks/xihTtKlq.js";var f={get data(){return u.data},get error(){return u.error},get form(){return u.form},get params(){return u.params},get route(){return u.route},get state(){return u.state},get status(){return u.status},get url(){return u.url}};d.updated.check;var p=f,m=l(`<h1> </h1> <p> </p>`,1);function h(l,u){n(u,!0);var d=m(),f=r(d),h=c(f,!0);t(f);var g=i(f,2),_=c(g,!0);t(g),e(()=>{s(h,p.status),s(_,p.error?.message)}),o(l,d),a()}export{h as component};
@@ -0,0 +1 @@
import{H as e,K as t,M as n,O as r,V as i,_ as a,g as o,p as s}from"../chunks/BXe04aPf.js";import{t as c}from"../chunks/tTEyPFub.js";import"../chunks/xihTtKlq.js";import{t as l}from"../chunks/kaCwo2dy.js";import"../chunks/Bthvo3p_.js";var u=t({});function d(t,u){e(u,!0),r(()=>{l.status==`unauthenticated`&&c(`/login`)});var d=a(),f=n(d);s(f,()=>u.children),o(t,d),i()}export{d as component,u as universal};
@@ -0,0 +1 @@
import{A as e,D as t,G as n,H as r,M as i,N as a,P as o,S as s,V as c,W as l,b as u,c as d,g as f,h as p,i as m,j as h,l as g,m as _,n as v,u as y,v as b,x}from"../chunks/BXe04aPf.js";import"../chunks/xihTtKlq.js";import{n as S}from"../chunks/kaCwo2dy.js";import{t as C}from"../chunks/bT040zHf.js";var w=o({sidebarDesktopOpen:!0,sidebarMobileOpen:!1,menuContentVisible:!0}),T=b(`<span class="ml-4"> </span>`),E=b(`<div class="p-2" tabindex="0" role="button"><i></i> <!></div>`);function D(e,i){r(i,!0);let o=m(i,`label`,3,``),s=m(i,`icon`,3,``);var l=E(),u=h(l),d=a(u,2),v=e=>{var r=T(),i=h(r,!0);n(r),t(()=>p(i,o())),f(e,r)};_(d,e=>{w.menuContentVisible&&e(v)}),n(l),t(()=>g(u,1,y([s()]))),f(e,l),c()}var O=b(`<div class="flex-1 p-3 font openonly"><img class="w-24"/></div>`),k=b(`<div class="p-2 flex-1 "><div>Fabien Masson</div> <div class="flex"><a href="#" class="text-xs flex-1">Modifier</a></div></div> <div class="p-2"><button class="w-full bt"><i class="icon-logout"></i></button></div>`,1),A=b(`<div class="flex flex-row italic mb-4"><!> <i id="hamburgerDesktop" class="icon-menu pointer block p-2" tabindex="0" role="button"></i> <i id="hamburgerMobile" class="icon-menu pointer block p-2" tabindex="0" role="button"></i></div> <div id="menu" class="flex-1 overflow-y-scroll"><!> <!> <!> <!></div> <div id="profile" class="w-full flex flex-row items-center border-t border-stone-200 dark:border-stone-800"><!></div>`,1);function j(e,o){r(o,!0);let s=()=>{S()};var l=A(),u=i(l),p=h(u),m=e=>{var r=O(),i=h(r);n(r),t(()=>d(i,`src`,C)),f(e,r)};_(p,e=>{w.menuContentVisible&&e(m)});var g=a(p,2),v=a(g,2);n(u);var y=a(u,2),b=h(y);D(b,{label:`Conversation`,icon:`icon-chat`});var T=a(b,2);D(T,{label:`Tâches`,icon:`icon-tasks`});var E=a(T,2);D(E,{label:`Historique`,icon:`icon-history`}),D(a(E,2),{label:`Paramétrage`,icon:`icon-cog`}),n(y);var j=a(y,2),M=h(j),N=e=>{var t=k(),r=a(i(t),2),o=h(r);n(r),x(`click`,o,s),f(e,t)},P=e=>{D(e,{label:`User`,icon:`icon-user`})};_(M,e=>{w.menuContentVisible?e(N):e(P,-1)}),n(j),x(`click`,g,function(...e){o.handleMenuDesktop?.apply(this,e)}),x(`click`,v,function(...e){o.handleMenuMobile?.apply(this,e)}),f(e,l),c()}u([`click`]);var M=b(`<div id="outer"><div id="sidebar" class="flex flex-col"><!></div> <div id="main"><div id="content">Contenu</div></div></div>`);function N(i,a){r(a,!0);let o=()=>{w.sidebarDesktopOpen=!w.sidebarDesktopOpen,w.sidebarDesktopOpen?setTimeout(()=>{w.menuContentVisible=w.sidebarDesktopOpen},200):w.menuContentVisible=w.sidebarDesktopOpen},u=()=>{w.sidebarMobileOpen=!w.sidebarMobileOpen,w.sidebarMobileOpen?setTimeout(()=>{w.menuContentVisible=w.sidebarMobileOpen},200):w.menuContentVisible=w.sidebarMobileOpen},d=()=>{w.menuContentVisible=window.innerWidth<640?w.sidebarMobileOpen:w.sidebarDesktopOpen};v(()=>{d()});var p=M();s(`resize`,e,d);var m=h(p);j(h(m),{handleMenuDesktop:o,handleMenuMobile:u}),n(m),l(2),n(p),t(()=>g(p,1,y([w.sidebarDesktopOpen?`desktopMenuOpen`:`desktopMenuClosed`,w.sidebarMobileOpen?`mobileMenuOpen`:`mobileMenuClosed`]))),f(i,p),c()}export{N as component};
@@ -0,0 +1 @@
import{D as e,G as t,H as n,M as r,N as i,P as a,R as o,S as s,V as c,W as l,b as u,c as d,g as f,j as p,m,o as h,s as g,v as _,x as v}from"../chunks/BXe04aPf.js";import{t as y}from"../chunks/tTEyPFub.js";import"../chunks/xihTtKlq.js";import{t as b}from"../chunks/kaCwo2dy.js";import"../chunks/Bthvo3p_.js";import{t as x}from"../chunks/bT040zHf.js";var S=_(`<form><div class="flex"><label for="email" class="block mb-2 flex-1">Email</label> <a href="#" class="text-xs">Mot de passe oublié ?</a></div> <div class="flex flex-row"><input type="text" class="textinput mr-1"/> <button class="bt"><i class="icon-right-big"></i></button></div></form> <div class="my-4 text-xs"><a href="/register">Pas encore de compte ? Incrivez-vous.</a></div>`,1),C=_(`<form><div class="flex"><label for="email" class="block mb-2 flex-1">Mot de passe</label></div> <div class="flex flex-row"><input type="text" class="textinput mr-1"/> <button class="bt">Entrer</button></div> <a href="/register" class="text-xs">← Retour</a></form>`),w=_(`<div class="h-full w-full flex items-center"><div class="m-auto min-w-64 p-4"><img class="h-8 mb-4"/> <h1>Préparerez votre galet ...</h1> <!> <hr class="my-4"/> <div class="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-4"><button class="bt p-5 text-center"><i class="icon-google"></i></button> <button class="bt p-5 text-center"><i class="icon-facebook"></i></button> <button class="bt p-5 text-center"><i class="icon-github"></i></button> <button class="bt p-5 text-center"><i class="icon-apple"></i></button></div></div></div>`);function T(u,_){n(_,!0);let T=a({email:``,password:``,validemail:!1}),E=()=>{T.validemail=!0},D=e=>{e.preventDefault(),console.log(`formSubmitPass `+b.status),b.status=`authenticated`,console.log(`formSubmitPass après `+b.status),y(`/`)};var O=w(),k=p(O),A=p(k),j=i(A,4),M=e=>{var n=S(),a=r(n),c=i(p(a),2),u=p(c);g(u),o(u,!0),l(2),t(c),t(a),l(2),s(`submit`,a,E),h(u,()=>T.email,e=>T.email=e),f(e,n)},N=e=>{var n=C(),r=i(p(n),2),a=p(r);g(a),o(a,!0),l(2),t(r);var c=i(r,2);t(n),s(`submit`,n,D),h(a,()=>T.password,e=>T.password=e),v(`click`,c,()=>{T.validemail=!1}),f(e,n)};m(j,e=>{T.validemail?e(N,-1):e(M)}),l(4),t(k),t(O),e(()=>d(A,`src`,x)),f(u,O),c()}u([`click`]);export{T as component};
@@ -0,0 +1 @@
import{D as e,G as t,H as n,N as r,P as i,R as a,S as o,V as s,W as c,c as l,g as u,j as d,o as f,s as p,v as m}from"../chunks/BXe04aPf.js";import{t as h}from"../chunks/tTEyPFub.js";import"../chunks/xihTtKlq.js";import"../chunks/kaCwo2dy.js";import"../chunks/Bthvo3p_.js";import{t as g}from"../chunks/bT040zHf.js";var _=m(`<div class="h-full w-full flex items-center"><div class="m-auto min-w-64 p-4"><img class="h-8 mb-4"/> <h1>Inscription</h1> <form><label for="email" class="block my-1 flex-1">Email</label> <input id="email" type="text" class="textinput mb-4"/> <label for="firstname" class="block my-1 flex-1">Prénom</label> <input id="firstname" type="text" class="textinput mb-4"/> <label for="lastname" class="block my-1 flex-1">Nom</label> <input id="lastname" type="text" class="textinput mb-4"/> <label for="password" class="block my-1 flex-1">Mot de passe</label> <input id="password" type="password" class="textinput mb-4"/> <label for="verifpassword" class="block my-1 flex-1">Vérification du mot de passe</label> <input id="verifpassword" type="password" class="textinput mb-4"/> <button class="bt my-2">Enregistrer</button></form></div></div>`);function v(m,v){n(v,!0);let y=i({email:``,firstname:``,lastname:``,password:``,verifPassword:``,validation:!1}),b=()=>{y.validation=!0,h(`/`)};var x=_(),S=d(x),C=d(S),w=r(C,4),T=r(d(w),2);p(T),a(T,!0);var E=r(T,4);p(E);var D=r(E,4);p(D);var O=r(D,4);p(O);var k=r(O,4);p(k),c(2),t(w),t(S),t(x),e(()=>l(C,`src`,g)),o(`submit`,w,b),f(T,()=>y.email,e=>y.email=e),f(E,()=>y.firstname,e=>y.firstname=e),f(D,()=>y.lastname,e=>y.lastname=e),f(O,()=>y.password,e=>y.password=e),f(k,()=>y.verifPassword,e=>y.verifPassword=e),u(m,x),s()}export{v as component};
@@ -0,0 +1 @@
{"version":"1785917155183"}
@@ -0,0 +1,12 @@
Font license info
## Font Awesome
Copyright (C) 2016 by Dave Gandy
Author: Dave Gandy
License: SIL ()
Homepage: http://fortawesome.github.com/Font-Awesome/
@@ -0,0 +1,75 @@
This webfont is generated by https://fontello.com open source project.
================================================================================
Please, note, that you should obey original font licenses, used to make this
webfont pack. Details available in LICENSE.txt file.
- Usually, it's enough to publish content of LICENSE.txt file somewhere on your
site in "About" section.
- If your project is open-source, usually, it will be ok to make LICENSE.txt
file publicly available in your repository.
- Fonts, used in Fontello, don't require a clickable link on your site.
But any kind of additional authors crediting is welcome.
================================================================================
Comments on archive content
---------------------------
- /font/* - fonts in different formats
- /css/* - different kinds of css, for all situations. Should be ok with
twitter bootstrap. Also, you can skip <i> style and assign icon classes
directly to text elements, if you don't mind about IE7.
- demo.html - demo file, to show your webfont content
- LICENSE.txt - license info about source fonts, used to build your one.
- config.json - keeps your settings. You can import it back into fontello
anytime, to continue your work
Why so many CSS files ?
-----------------------
Because we like to fit all your needs :)
- basic file, <your_font_name>.css - is usually enough, it contains @font-face
and character code definitions
- *-ie7.css - if you need IE7 support, but still don't wish to put char codes
directly into html
- *-codes.css and *-ie7-codes.css - if you like to use your own @font-face
rules, but still wish to benefit from css generation. That can be very
convenient for automated asset build systems. When you need to update font -
no need to manually edit files, just override old version with archive
content. See fontello source code for examples.
- *-embedded.css - basic css file, but with embedded WOFF font, to avoid
CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain.
We strongly recommend to resolve this issue by `Access-Control-Allow-Origin`
server headers. But if you ok with dirty hack - this file is for you. Note,
that data url moved to separate @font-face to avoid problems with <IE9, when
string is too long.
- animate.css - use it to get ideas about spinner rotation animation.
Attention for server setup
--------------------------
You MUST setup server to reply with proper `mime-types` for font files -
otherwise some browsers will fail to show fonts.
Usually, `apache` already has necessary settings, but `nginx` and other
webservers should be tuned. Here is list of mime types for our file extensions:
- `application/vnd.ms-fontobject` - eot
- `application/x-font-woff` - woff
- `application/x-font-ttf` - ttf
- `image/svg+xml` - svg
@@ -0,0 +1,112 @@
{
"name": "",
"css_prefix_text": "icon-",
"css_use_suffix": false,
"hinting": true,
"units_per_em": 1000,
"ascent": 850,
"glyphs": [
{
"uid": "dcedf50ab1ede3283d7a6c70e2fe32f3",
"css": "chat",
"code": 59392,
"src": "fontawesome"
},
{
"uid": "31972e4e9d080eaa796290349ae6c1fd",
"css": "users",
"code": 59393,
"src": "fontawesome"
},
{
"uid": "38575a803c4da31ce20d77e1e1236bcb",
"css": "paper-plane",
"code": 61912,
"src": "fontawesome"
},
{
"uid": "5bb103cd29de77e0e06a52638527b575",
"css": "wrench",
"code": 59394,
"src": "fontawesome"
},
{
"uid": "20fc52f9a88bb7bda023ef209acac095",
"css": "graduation-cap",
"code": 61853,
"src": "fontawesome"
},
{
"uid": "e99461abfef3923546da8d745372c995",
"css": "cog",
"code": 59395,
"src": "fontawesome"
},
{
"uid": "8b80d36d4ef43889db10bc1f0dc9a862",
"css": "user",
"code": 59396,
"src": "fontawesome"
},
{
"uid": "9396b2d8849e0213a0f11c5fd7fcc522",
"css": "tasks",
"code": 61614,
"src": "fontawesome"
},
{
"uid": "d4816c0845aa43767213d45574b3b145",
"css": "history",
"code": 61914,
"src": "fontawesome"
},
{
"uid": "559647a6f430b3aeadbecd67194451dd",
"css": "menu",
"code": 61641,
"src": "fontawesome"
},
{
"uid": "41087bc74d4b20b55059c60a33bf4008",
"css": "edit",
"code": 59397,
"src": "fontawesome"
},
{
"uid": "ad6b3fbb5324abe71a9c0b6609cbb9f1",
"css": "right-big",
"code": 59398,
"src": "fontawesome"
},
{
"uid": "f06fe7ff18d1c591bc1183cb3ab105e9",
"css": "google",
"code": 61856,
"src": "fontawesome"
},
{
"uid": "8e04c98c8f5ca0a035776e3001ad2638",
"css": "facebook",
"code": 61594,
"src": "fontawesome"
},
{
"uid": "e9fa538fd5913046497ac148e27cd8ea",
"css": "apple",
"code": 61817,
"src": "fontawesome"
},
{
"uid": "5e0a374728ffa8d0ae1f331a8f648231",
"css": "github",
"code": 61715,
"src": "fontawesome"
},
{
"uid": "0d20938846444af8deb1920dc85a29fb",
"css": "logout",
"code": 59399,
"src": "fontawesome"
}
]
}
@@ -0,0 +1,85 @@
/*
Animation example, for spinners
*/
.animate-spin {
-moz-animation: spin 2s infinite linear;
-o-animation: spin 2s infinite linear;
-webkit-animation: spin 2s infinite linear;
animation: spin 2s infinite linear;
display: inline-block;
}
@-moz-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-webkit-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-o-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@-ms-keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@keyframes spin {
0% {
-moz-transform: rotate(0deg);
-o-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(359deg);
-o-transform: rotate(359deg);
-webkit-transform: rotate(359deg);
transform: rotate(359deg);
}
}
@@ -0,0 +1,18 @@
.icon-chat:before { content: '\e800'; } /* '' */
.icon-users:before { content: '\e801'; } /* '' */
.icon-wrench:before { content: '\e802'; } /* '' */
.icon-cog:before { content: '\e803'; } /* '' */
.icon-user:before { content: '\e804'; } /* '' */
.icon-edit:before { content: '\e805'; } /* '' */
.icon-right-big:before { content: '\e806'; } /* '' */
.icon-logout:before { content: '\e807'; } /* '' */
.icon-facebook:before { content: '\f09a'; } /* '' */
.icon-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-github:before { content: '\f113'; } /* '' */
.icon-apple:before { content: '\f179'; } /* '' */
.icon-graduation-cap:before { content: '\f19d'; } /* '' */
.icon-google:before { content: '\f1a0'; } /* '' */
.icon-paper-plane:before { content: '\f1d8'; } /* '' */
.icon-history:before { content: '\f1da'; } /* '' */
File diff suppressed because one or more lines are too long
@@ -0,0 +1,18 @@
.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&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-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;'); }
.icon-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf113;&nbsp;'); }
.icon-apple { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf179;&nbsp;'); }
.icon-graduation-cap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf19d;&nbsp;'); }
.icon-google { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1a0;&nbsp;'); }
.icon-paper-plane { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1d8;&nbsp;'); }
.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1da;&nbsp;'); }
@@ -0,0 +1,29 @@
[class^="icon-"], [class*=" icon-"] {
font-family: 'fontello';
font-style: normal;
font-weight: normal;
/* fix buttons height */
line-height: 1em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
}
.icon-chat { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe800;&nbsp;'); }
.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe801;&nbsp;'); }
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe802;&nbsp;'); }
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe803;&nbsp;'); }
.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe804;&nbsp;'); }
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe805;&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-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;'); }
.icon-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf113;&nbsp;'); }
.icon-apple { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf179;&nbsp;'); }
.icon-graduation-cap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf19d;&nbsp;'); }
.icon-google { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1a0;&nbsp;'); }
.icon-paper-plane { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1d8;&nbsp;'); }
.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf1da;&nbsp;'); }
@@ -0,0 +1,73 @@
@font-face {
font-family: 'fontello';
src: url('../font/fontello.eot?44639875');
src: url('../font/fontello.eot?44639875#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?44639875') format('woff2'),
url('../font/fontello.woff?44639875') format('woff'),
url('../font/fontello.ttf?44639875') format('truetype'),
url('../font/fontello.svg?44639875#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
/* 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 */
/*
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
src: url('../font/fontello.svg?44639875#fontello') format('svg');
}
}
*/
[class^="icon-"]:before, [class*=" icon-"]:before {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: never;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* you can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
.icon-chat:before { content: '\e800'; } /* '' */
.icon-users:before { content: '\e801'; } /* '' */
.icon-wrench:before { content: '\e802'; } /* '' */
.icon-cog:before { content: '\e803'; } /* '' */
.icon-user:before { content: '\e804'; } /* '' */
.icon-edit:before { content: '\e805'; } /* '' */
.icon-right-big:before { content: '\e806'; } /* '' */
.icon-logout:before { content: '\e807'; } /* '' */
.icon-facebook:before { content: '\f09a'; } /* '' */
.icon-tasks:before { content: '\f0ae'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */
.icon-github:before { content: '\f113'; } /* '' */
.icon-apple:before { content: '\f179'; } /* '' */
.icon-graduation-cap:before { content: '\f19d'; } /* '' */
.icon-google:before { content: '\f1a0'; } /* '' */
.icon-paper-plane:before { content: '\f1d8'; } /* '' */
.icon-history:before { content: '\f1da'; } /* '' */
@@ -0,0 +1,277 @@
<!DOCTYPE html>
<html>
<head>
<!--[if lt IE 9]><script language="javascript" type="text/javascript" src="//html5shim.googlecode.com/svn/trunk/html5.js"></script><![endif]-->
<meta charset="UTF-8">
<style>
html {
font-size: 100%;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
a:focus {
outline: thin dotted #333;
outline: 5px auto -webkit-focus-ring-color;
outline-offset: -2px;
}
a:hover,
a:active {
outline: 0;
}
input {
margin: 0;
font-size: 100%;
vertical-align: middle;
*overflow: visible;
line-height: normal;
}
input::-moz-focus-inner {
padding: 0;
border: 0;
}
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 20px;
color: #333;
background-color: #fff;
}
a {
color: #08c;
text-decoration: none;
}
a:hover {
color: #005580;
text-decoration: underline;
}
.row {
margin-left: -20px;
*zoom: 1;
}
.row:before,
.row:after {
display: table;
content: "";
line-height: 0;
}
.row:after {
clear: both;
}
.span3 {
float: left;
min-height: 1px;
margin-left: 20px;
width: 220px;
}
.container {
width: 940px;
margin-right: auto;
margin-left: auto;
*zoom: 1;
}
.container:before,
.container:after {
display: table;
content: "";
line-height: 0;
}
.container:after {
clear: both;
}
small {
font-size: 85%;
}
h1 {
margin: 10px 0;
font-family: inherit;
font-weight: bold;
line-height: 20px;
color: inherit;
text-rendering: optimizelegibility;
line-height: 40px;
font-size: 38.5px;
}
h1 small {
font-weight: normal;
line-height: 1;
color: #999;
font-size: 24.5px;
}
body {
margin-top: 90px;
}
.header {
position: fixed;
top: 0;
left: 50%;
margin-left: -480px;
background-color: #fff;
border-bottom: 1px solid #ddd;
padding-top: 10px;
z-index: 10;
}
.footer {
color: #ddd;
font-size: 12px;
text-align: center;
margin-top: 20px;
}
.footer a {
color: #ccc;
text-decoration: underline;
}
.the-icons {
font-size: 14px;
line-height: 24px;
}
.switch {
position: absolute;
right: 0;
bottom: 10px;
color: #666;
}
.switch input {
margin-right: 0.3em;
}
.codesOn .i-name {
display: none;
}
.codesOn .i-code {
display: inline;
}
.i-code {
display: none;
}
@font-face {
font-family: 'fontello';
src: url('./font/fontello.eot?35012786');
src: url('./font/fontello.eot?35012786#iefix') format('embedded-opentype'),
url('./font/fontello.woff?35012786') format('woff'),
url('./font/fontello.ttf?35012786') format('truetype'),
url('./font/fontello.svg?35012786#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
.demo-icon {
font-family: "fontello";
font-style: normal;
font-weight: normal;
speak: never;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
/* opacity: .8; */
/* For safety - reset parent styles, that can break glyph codes*/
font-variant: normal;
text-transform: none;
/* fix buttons height, for twitter bootstrap */
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
/* remove if not needed */
margin-left: .2em;
/* You can be more comfortable with increased icons size */
/* font-size: 120%; */
/* Font smoothing. That was taken from TWBS */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/* Uncomment for 3D effect */
/* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
}
</style>
<link rel="stylesheet" href="css/animation.css"><!--[if IE 7]><link rel="stylesheet" href="css/" + font.fontname + "-ie7.css"><![endif]-->
<script>
function toggleCodes(on) {
var obj = document.getElementById('icons');
if (on) {
obj.className += ' codesOn';
} else {
obj.className = obj.className.replace(' codesOn', '');
}
}
</script>
</head>
<body>
<div class="container header">
<h1>fontello <small>font demo</small></h1>
<label class="switch">
<input type="checkbox" onclick="toggleCodes(this.checked)">show codes
</label>
</div>
<div class="container" id="icons">
<div class="row">
<div class="span3" title="Code: 0xe800">
<i class="demo-icon icon-chat">&#xe800;</i> <span class="i-name">icon-chat</span><span class="i-code">0xe800</span>
</div>
<div class="span3" title="Code: 0xe801">
<i class="demo-icon icon-users">&#xe801;</i> <span class="i-name">icon-users</span><span class="i-code">0xe801</span>
</div>
<div class="span3" title="Code: 0xe802">
<i class="demo-icon icon-wrench">&#xe802;</i> <span class="i-name">icon-wrench</span><span class="i-code">0xe802</span>
</div>
<div class="span3" title="Code: 0xe803">
<i class="demo-icon icon-cog">&#xe803;</i> <span class="i-name">icon-cog</span><span class="i-code">0xe803</span>
</div>
</div>
<div class="row">
<div class="span3" title="Code: 0xe804">
<i class="demo-icon icon-user">&#xe804;</i> <span class="i-name">icon-user</span><span class="i-code">0xe804</span>
</div>
<div class="span3" title="Code: 0xe805">
<i class="demo-icon icon-edit">&#xe805;</i> <span class="i-name">icon-edit</span><span class="i-code">0xe805</span>
</div>
<div class="span3" title="Code: 0xe806">
<i class="demo-icon icon-right-big">&#xe806;</i> <span class="i-name">icon-right-big</span><span class="i-code">0xe806</span>
</div>
<div class="span3" title="Code: 0xe807">
<i class="demo-icon icon-logout">&#xe807;</i> <span class="i-name">icon-logout</span><span class="i-code">0xe807</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>
<div class="span3" title="Code: 0xf0ae">
<i class="demo-icon icon-tasks">&#xf0ae;</i> <span class="i-name">icon-tasks</span><span class="i-code">0xf0ae</span>
</div>
<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 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>
<div class="span3" title="Code: 0xf19d">
<i class="demo-icon icon-graduation-cap">&#xf19d;</i> <span class="i-name">icon-graduation-cap</span><span class="i-code">0xf19d</span>
</div>
<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 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>
</div>
</div>
<div class="container footer">Generated by <a href="https://fontello.com">fontello.com</a></div>
</body>
</html>
@@ -0,0 +1,44 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2026 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="chat" unicode="&#xe800;" d="M786 421q0-77-53-143t-143-104-197-38q-48 0-98 9-70-49-155-72-21-5-48-9h-2q-6 0-12 5t-6 12q-1 1-1 3t1 4 1 3l1 3t2 3 2 3 3 3 2 2q3 3 13 14t15 16 12 17 14 21 11 25q-69 40-108 98t-40 125q0 78 53 144t143 104 197 38 197-38 143-104 53-144z m214-142q0-67-40-126t-108-98q5-14 11-25t14-21 13-16 14-17 13-14q0 0 2-2t3-3 2-3 2-3l1-3t1-3 1-4-1-3q-2-8-7-13t-12-4q-28 4-48 9-86 23-156 72-50-9-98-9-151 0-263 74 32-3 49-3 90 0 172 25t148 72q69 52 107 119t37 141q0 43-13 85 72-39 114-99t42-128z" horiz-adv-x="1000" />
<glyph glyph-name="users" unicode="&#xe801;" d="M331 350q-90-3-148-71h-75q-45 0-77 22t-31 66q0 197 69 197 4 0 25-11t54-24 66-12q38 0 75 13-3-21-3-37 0-78 45-143z m598-356q0-66-41-105t-108-39h-488q-68 0-108 39t-41 105q0 30 2 58t8 61 14 61 24 54 35 45 48 30 62 11q6 0 24-12t41-26 59-27 76-12 75 12 60 27 41 26 24 12q34 0 62-11t47-30 35-45 24-54 15-61 8-61 2-58z m-572 713q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z m393-214q0-89-63-152t-151-62-152 62-63 152 63 151 152 63 151-63 63-151z m321-126q0-43-31-66t-77-22h-75q-57 68-147 71 45 65 45 143 0 16-3 37 37-13 74-13 33 0 67 12t54 24 24 11q69 0 69-197z m-71 340q0-59-42-101t-101-42-101 42-42 101 42 101 101 42 101-42 42-101z" horiz-adv-x="1071.4" />
<glyph glyph-name="wrench" unicode="&#xe802;" d="M214 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m360 234l-381-381q-21-20-50-20-29 0-51 20l-59 61q-21 20-21 50 0 29 21 51l380 380q22-55 64-97t97-64z m354 243q0-22-13-59-27-75-92-122t-144-46q-104 0-177 73t-73 177 73 176 177 74q32 0 67-10t60-26q9-6 9-15t-9-16l-163-94v-125l108-60q2 2 44 27t75 45 40 20q8 0 13-5t5-14z" horiz-adv-x="928.6" />
<glyph glyph-name="cog" unicode="&#xe803;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
<glyph glyph-name="user" unicode="&#xe804;" d="M714 69q0-60-35-104t-84-44h-476q-49 0-84 44t-35 104q0 48 5 90t17 85 33 73 52 50 76 19q73-72 174-72t175 72q42 0 75-19t52-50 33-73 18-85 4-90z m-143 495q0-88-62-151t-152-63-151 63-63 151 63 152 151 63 152-63 62-152z" horiz-adv-x="714.3" />
<glyph glyph-name="edit" unicode="&#xe805;" d="M496 189l64 65-85 85-64-65v-31h53v-54h32z m245 402q-9 9-18 0l-196-196q-9-9 0-18t18 0l196 196q9 9 0 18z m45-331v-106q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h464q35 0 65-14 9-4 10-13 2-10-5-16l-27-28q-8-8-18-4-13 3-25 3h-464q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v70q0 7 5 12l36 36q8 8 20 4t11-16z m-54 411l161-160-375-375h-161v160z m248-73l-51-52-161 161 51 52q16 15 38 15t38-15l85-85q16-16 16-38t-16-38z" horiz-adv-x="1000" />
<glyph glyph-name="right-big" unicode="&#xe806;" d="M821 314q0-30-20-50l-363-364q-22-20-51-20-29 0-50 20l-42 42q-22 21-22 51t22 51l163 163h-393q-29 0-47 21t-18 51v71q0 30 18 51t47 20h393l-163 165q-22 20-22 50t22 50l42 42q21 21 50 21 29 0 51-21l363-363q20-20 20-51z" horiz-adv-x="857.1" />
<glyph glyph-name="logout" unicode="&#xe807;" d="M357 46q0-2 1-11t0-14-2-14-5-11-12-3h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
<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="menu" unicode="&#xf0c9;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
<glyph glyph-name="github" unicode="&#xf113;" d="M357 171q0-22-7-45t-24-43-40-19-41 19-24 43-7 45 7 46 24 43 41 19 40-19 24-43 7-46z m357 0q0-22-7-45t-24-43-40-19-41 19-24 43-7 45 7 46 24 43 41 19 40-19 24-43 7-46z m90 0q0 67-39 114t-104 47q-23 0-109-12-40-6-88-6t-87 6q-85 12-109 12-66 0-104-47t-39-114q0-49 18-85t45-58 68-33 78-17 83-4h94q46 0 83 4t78 17 69 33 45 58 18 85z m125 99q0-116-34-185-22-43-59-74t-79-48-95-27-96-12-93-3q-43 0-79 2t-82 7-85 17-77 29-67 45-48 64q-35 69-35 185 0 132 76 221-15 45-15 95 0 64 28 121 61 0 106-22t106-69q82 20 172 20 83 0 157-18 58 46 104 67t105 22q29-57 29-121 0-49-15-94 76-89 76-222z" horiz-adv-x="928.6" />
<glyph glyph-name="apple" unicode="&#xf179;" d="M777 172q-21-70-68-139-72-110-144-110-27 0-78 18-48 18-84 18-34 0-79-19-45-19-74-19-85 0-168 145-82 146-82 281 0 127 63 208 63 81 159 81 40 0 98-17 58-17 77-17 25 0 80 19 57 19 97 19 66 0 119-36 29-20 58-56-44-37-64-66-36-52-36-115 0-69 38-125t88-70z m-209 655q0-34-17-76-16-42-52-77-30-30-60-40-20-7-58-10 2 83 44 143 41 60 139 83 1-2 2-6t1-6q0-2 0-6t1-5z" horiz-adv-x="785.7" />
<glyph glyph-name="graduation-cap" unicode="&#xf19d;" d="M990 384l10-177q2-38-46-71t-131-52-180-20-180 20-131 52-46 71l10 177 320-101q12-4 27-4t27 4z m296 180q0-12-13-17l-625-196q-2-1-5-1t-6 1l-364 115q-24-19-39-63t-19-99q35-20 35-61 0-39-32-60l32-242q1-7-4-13-5-7-14-7h-107q-8 0-13 7-6 6-5 13l33 242q-33 21-33 60 0 41 36 62 7 115 55 184l-186 58q-12 5-12 17t12 18l625 196q3 1 6 1t5-1l625-196q13-5 13-18z" horiz-adv-x="1285.7" />
<glyph glyph-name="google" unicode="&#xf1a0;" d="M429 411h404q7-37 7-71 0-121-51-216t-145-149-215-54q-88 0-167 34t-137 91-91 137-34 167 34 167 91 137 137 91 167 34q167 0 287-113l-117-112q-68 67-170 67-72 0-133-37t-97-98-36-136 36-136 97-98 133-37q48 0 89 14t67 33 46 46 28 49 13 43h-243v147z" horiz-adv-x="857.1" />
<glyph glyph-name="paper-plane" unicode="&#xf1d8;" d="M984 844q19-13 15-36l-142-857q-3-16-18-25-8-5-18-5-6 0-13 3l-253 104-135-165q-10-13-27-13-7 0-12 2-11 4-17 13t-7 21v195l482 590-596-516-221 91q-20 8-22 30-1 23 18 33l928 536q9 5 18 5 11 0 20-6z" horiz-adv-x="1000" />
<glyph glyph-name="history" unicode="&#xf1da;" d="M857 350q0-87-34-166t-91-137-137-92-166-34q-96 0-183 41t-147 114q-4 6-4 13t5 11l76 77q6 5 14 5 9-1 13-7 41-53 100-82t126-29q58 0 110 23t92 61 61 91 22 111-22 111-61 91-92 61-110 23q-55 0-105-20t-90-57l77-77q17-16 8-38-10-23-33-23h-250q-15 0-25 11t-11 25v250q0 24 22 33 22 10 39-8l72-72q60 57 137 88t159 31q87 0 166-34t137-92 91-137 34-166z m-357 161v-250q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13v35q0 8 5 13t13 5h125v197q0 8 5 13t12 5h36q8 0 13-5t5-13z" horiz-adv-x="857.1" />
</font>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

+36
View File
@@ -0,0 +1,36 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="text-scale" content="scale" />
<link href="/_app/immutable/entry/start.BDBpcwWA.js" rel="modulepreload">
<link href="/_app/immutable/chunks/tTEyPFub.js" rel="modulepreload">
<link href="/_app/immutable/chunks/BXe04aPf.js" rel="modulepreload">
<link href="/_app/immutable/entry/app.3rrbglG1.js" rel="modulepreload">
<link href="/_app/immutable/chunks/xihTtKlq.js" rel="modulepreload">
<link href="/_app/immutable/nodes/0.DUhcgpvL.js" rel="modulepreload">
<link href="/_app/immutable/assets/0.CHStj0oN.css" rel="stylesheet">
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">
<script>
{
__sveltekit_1tkqncq = {
base: ""
};
const element = document.currentScript.parentElement;
Promise.all([
import("/_app/immutable/entry/start.BDBpcwWA.js"),
import("/_app/immutable/entry/app.3rrbglG1.js")
]).then(([kit, app]) => {
kit.start(app, element);
});
}
</script>
</div>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
# allow crawling everything by default
User-agent: *
Disallow:
@@ -0,0 +1,49 @@
package auth
import (
"context"
"time"
"github.com/labstack/echo/v5"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
type Credentials struct {
Method string // "password" ou "oidc"
Email string
Password string
OIDCCode string
}
type Registration struct {
Method string `json:"method"` // "password" ou "oidc"
Email string `json:"email"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Password string `json:"password"`
}
type Session struct {
User *domain.User
AccessToken string
RefreshToken string
ExpiresAt time.Time
}
const ContextUserIDKey = "userID"
const ContextEmailKey = "userEmail"
const ContextNameKey = "userName"
const ContextAdminKey = "admin"
// Interface définie ici car c'est un port propre au cas d'usage "auth"
type Authenticator interface {
Authenticate(ctx context.Context, creds Credentials) (*Session, error)
Register(ctx context.Context, registration Registration) (*Session, error)
}
type TokenManager interface {
GenerateAccessToken(user *domain.User) (string, time.Time, error)
GenerateRefreshToken(userID string) (string, time.Time, error)
ParseAndValidate(tokenString string) (*domain.User, error)
TokenMiddleware(next echo.HandlerFunc) echo.HandlerFunc
}
+60
View File
@@ -0,0 +1,60 @@
package auth
import (
"context"
"errors"
"fmt"
"trankilou.fr/lassistanoque/backend/internal/domain"
)
var (
ErrRegistrationNotAllowed = errors.New("registration not allowed")
ErrMethodUnknown = errors.New("auth method unknown")
ErrUnauthorized = errors.New("unauthorized")
)
type Service struct {
authenticators map[string]Authenticator // "password" -> ..., "oidc" -> ...
userRepository domain.UserRepository
settingsRepository domain.SettingsRepository
}
func NewService(
settingsRepository domain.SettingsRepository,
userRepository domain.UserRepository,
authenticators map[string]Authenticator,
) *Service {
return &Service{
authenticators: authenticators,
userRepository: userRepository,
settingsRepository: settingsRepository,
}
}
func (s *Service) Login(ctx context.Context, creds Credentials) (*Session, error) {
authn, ok := s.authenticators[creds.Method]
if !ok {
return nil, fmt.Errorf("auth method unsupported: %s", creds.Method)
}
return authn.Authenticate(ctx, creds)
}
func (s *Service) Register(registration Registration) (*Session, error) {
settings, err := s.settingsRepository.GetSettings()
if err != nil {
return nil, err
}
if !settings.RegisterEnabled {
return nil, ErrRegistrationNotAllowed
}
if authenticator, ok := s.authenticators[registration.Method]; ok {
return authenticator.Register(context.Background(), registration)
} else {
return nil, ErrMethodUnknown
}
}
func (s *Service) Status(session *Session) (*domain.User, error) {
return nil, nil
}
+17
View File
@@ -0,0 +1,17 @@
package user
import "trankilou.fr/lassistanoque/backend/internal/domain"
type Service struct {
repo domain.UserRepository
}
func NewService(repo domain.UserRepository) *Service {
return &Service{
repo,
}
}
func (s *Service) GetUser(id string) (*domain.User, error) {
return s.repo.FindUser(id)
}
+9
View File
@@ -0,0 +1,9 @@
package utility
import "github.com/sixafter/nanoid"
func GenID() string {
//21 caractères - équivalent à un UUID v4 en sécurité contre les collisions
id, _ := nanoid.New()
return id.String()
}
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
package main
import (
"trankilou.fr/lassistanoque/backend/cmd"
)
func main() {
cmd.Execute()
}
Submodule
+1
Submodule backend/web added at fd6aeabbd5
+27
View File
@@ -0,0 +1,27 @@
module trankilou.fr/lassistanoque
go 1.26.5
require (
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/golang-migrate/migrate/v4 v4.19.1
github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1
github.com/labstack/echo/v5 v5.3.1
github.com/mattn/go-sqlite3 v1.14.42
github.com/sixafter/nanoid v1.64.5
github.com/spf13/cobra v1.10.2
golang.org/x/crypto v0.54.0
turso.tech/database/tursogo v0.7.2
)
require (
github.com/ebitengine/purego v0.9.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/sixafter/aes-ctr-drbg v1.19.2 // indirect
github.com/sixafter/prng-chacha v1.16.5 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/tursodatabase/turso-go-platform-libs v0.7.2 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
)
+63
View File
@@ -0,0 +1,63 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/labstack/echo/v5 v5.3.1 h1:75maCxkQVGualckLc/5s/ihgpH1a1Dc6AuGWNVNs6bw=
github.com/labstack/echo/v5 v5.3.1/go.mod h1:4iEGNQiPPZnkfYpNR/L6fINd3NLiGWUD5+eBotFALas=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sixafter/aes-ctr-drbg v1.19.2 h1:+Bd1tS9/y/GapqgyyRjmrce6a5wUAQPusJXlAOh44yM=
github.com/sixafter/aes-ctr-drbg v1.19.2/go.mod h1:iOBiPPkiy5Z5cEWm2yCoqEtnkfafxNTokJLL0zOPZeQ=
github.com/sixafter/nanoid v1.64.5 h1:Aei4SU9i9I35EyJQawo9XYpzYoARGobQRU8EnBf/Pjo=
github.com/sixafter/nanoid v1.64.5/go.mod h1:+9tqjRHutec4D9mCKGvTaGSAxj1Q+eEHOvfp2WQgWa4=
github.com/sixafter/prng-chacha v1.16.5 h1:RNPMMVDq6rzBS+wDc9BAkpcKoVRhFGfBIwGRUCo5+Cc=
github.com/sixafter/prng-chacha v1.16.5/go.mod h1:fY7WtbBwx94oa5qN417ZzCLCozjQ7JDag7dr1f/v2Ss=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tursodatabase/turso-go-platform-libs v0.7.2 h1:5ysGFHJC+I39jT1CFq2TukwscyEjMJN8zAPFXTSwvFE=
github.com/tursodatabase/turso-go-platform-libs v0.7.2/go.mod h1:bo+Lpv5OYOX1gRV9L5DLKMsYxmDs56SkZwnCOLEFcxU=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
turso.tech/database/tursogo v0.7.2 h1:WhsLl67w/Kw7ACEsJaTlXIb1parIgRqUcB8AbNZsBqU=
turso.tech/database/tursogo v0.7.2/go.mod h1:tmC+H2Ot+guA9bs5oOcbbmpj1mHFTa/g7QREXG5QHq0=
+139
View File
@@ -0,0 +1,139 @@
# Application CONCIERGE
## Généralités
Nom d'application: lassistanoque
Objectif :
Le but de l'application est d'être à la fois un outil de chat, mais surtout un lieu ou l'utilisateur peut créer des agents autonomes, à la fois dans un contexte professionnel et personnel.
Fonctionnalité :
- Espace de chat
- Espace de visualisation/gestion des tâches (tâches longues ou tâches récurrente)
- Paramétrage :
- Sélection et paramétrage du/des modèles de LLM
- Outils et serveurs MCP
- Canaux de communication (en lecture et écriture)
- Paramétrage de la mémoire
Cibles :
- Petite entreprise
- Particulier (en selfhosting)
Il doit permettre parexemple à une PME d'automatiser les réponse à son adresse de contact, faire des études de marché, sourcer des produits, codes des applications, ...
## Technologies
Composants
- backend : Go avec le framework Echo
- Base de données : turso
- frontend web : pure HTML 5 + tailwind + Typescript
- frontend desktop : Wails
Style :
- Style épuré, minimaliste
- 2 thèmes fournis : light et dark
- Tailwind
Authentification :
- multi-utilisateur
- connexion en Openid Connect ou bien par utilisateur/mot de passe
Développe :
- le squelette du backend + app web
- le squelette de l'application Wail
- la gestion des utilisateurs
- les thèmes
Ne développe pas tout de suite :
- L'accès aux LLM
Simule :
- L'accès au LLM
- La création des tâches
Le projet doit être structuré selon les règles de l'art.
Le code doit être maintenable par un développeur moyen.
Privilégie la création d'un seul exécutable pour le serveur (avec go embed pour embarquer l'app web), et un executable pour l'app desktop.
Un mode d'installation docker-compose doit être proposée.
## Mode Web et mode Desktop
Le mode web et le mode desktop utilisent la même authentification.
Le mode Desktop est juste la même applicatoin que l'application web, mais davantage intégrée à l'OS (system tray, notifications, accès par touche de raccourci, ...).
Le mode Desktop se connecte soit à un serveur distant, soit lance lui-même un serveur lorsqu'aucun serveur distant n'est configuré. Dans le dernier cas, le serveur doit être tué à la fermeture de l'application.
## Tâches
les tâches sont créer lors de conversation de Chat.
Par exemple si l'utilisateur demande : "Regarde tous les jours sur ebay si tu trouve des timbres rares, et enregistre les annonces intéressantes dans ma base Grist (table Timbres). Envoie-moi un email de compte-rendu de tes recherches"
Les tâches peuvent aussi être modifié par Chat.
Les tâche peuvent être répétitives ou ponctuelles, longues avec Harness ou non.
## Base de données
Table provider : Fournisseurs de LLM
- id -> uuid
- name -> string
- provider -> string
- key -> string
- url -> string
Table models : Modèles LLM
- id -> uuid
- provider_uuid -> uuid
- name -> string
- modelname -> string
- configuration -> json
Table system : Configuration system
- chat_model_uuid -> uuid
- default_lang -> string
- port -> string
- register_enabled -> bool
- password_enabled -> bool
- oidc_wellknown_url -> string
- oidc_client_id -> string
- oidc_client_secret -> string
Table users : Utilisateur
- id -> uuid
- name -> string
- picture -> blob
- ...
Table user_adresses : Adresse de contact de l'utilisateur
- id -> uuid
- user_uuid : uuid
- type (email, telegram, whatsapp, ...) -> string
Table channels : Canaux de communication utilisés par l'agent
- id -> uuid
- name -> string
- enabled -> boolean
- type (email, telegram, whatsapp, ...) -> string
- configuration -> json
Table tools : Paramétrage des outils
- id -> uuid
- name -> string
- enabled -> boolean
- configuration -> json
Table tasks : Tâches
- id -> uuid
- owner_uuid -> uuid
- model_uuid -> uuid
- name -> string
- prompt -> string
- cron -> string
- status (scheduled, done, paused, error) -> string
- next_datetime -> datetime
Table task_history : Historique de tâche
- id -> uuid
- task_uuid -> uuid
- start_datetime -> datetime
- enb_datetime -> datetime
- prompt -> string
- log -> string
- response -> string