renommage lassistanoque
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
lassistanoque.db
|
||||||
@@ -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 |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
@@ -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 |
@@ -0,0 +1 @@
|
|||||||
|
LASSISTANOQUE_JWT_SECRET=bCrRlJVsCmMhvKGbZFUbUPeYngvIDqJ6
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 = ¶ms{
|
||||||
|
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 := ¶ms{}
|
||||||
|
_, 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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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(®istration)
|
||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 = ' '); }
|
||||||
|
.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-facebook { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-apple { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-graduation-cap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-google { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-paper-plane { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
@@ -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 = ' '); }
|
||||||
|
.icon-users { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-wrench { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-cog { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-user { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-edit { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-right-big { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-logout { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-facebook { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-tasks { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-github { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-apple { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-graduation-cap { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-google { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-paper-plane { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
|
.icon-history { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = ' '); }
|
||||||
@@ -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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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"></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>
|
||||||
Binary file not shown.
@@ -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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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="" 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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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.
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"trankilou.fr/lassistanoque/backend/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
Submodule
+1
Submodule backend/web added at fd6aeabbd5
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user