add personal address

This commit is contained in:
fabien committed 2026-08-18 23:57:08 +02:00
1 parent 50d1ba630e
commit d2a153536f
17 files changed
+239 -50

No files matched your search

@@ -59,7 +59,6 @@ func (fr *TursoFileRepository) Upload(reader io.Reader, path string, name string
ContentType: contentType, ContentType: contentType,
VersionId: utility.GenID(), VersionId: utility.GenID(),
DateCreated: time.Now(), DateCreated: time.Now(),
DateUpdated: time.Now(),
Content: bytes, Content: bytes,
} }
@@ -131,13 +131,16 @@ func (ur *TursoUserRepository) ListUserAddresses(id string) ([]*domain.UserAddre
return ur.AddressTable.SelectWhere("user_id=$1", id) return ur.AddressTable.SelectWhere("user_id=$1", id)
} }
func (ur *TursoUserRepository) CreateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) { func (ur *TursoUserRepository) GetUserAddress(addrID string) (*domain.UserAddress, error) {
addr.UserID = userid return ur.AddressTable.Get(addrID)
}
func (ur *TursoUserRepository) CreateUserAddress(addr *domain.UserAddress) (*domain.UserAddress, error) {
return ur.AddressTable.Insert(addr) return ur.AddressTable.Insert(addr)
} }
func (ur *TursoUserRepository) UpdateUserAddress(userid string, addr *domain.UserAddress) (*domain.UserAddress, error) { func (ur *TursoUserRepository) UpdateUserAddress(addr *domain.UserAddress) (*domain.UserAddress, error) {
return ur.AddressTable.Update(addr) return ur.AddressTable.Update(addr)
} }
func (ur *TursoUserRepository) DeleteUserAddress(userid string, addrID string) error { func (ur *TursoUserRepository) DeleteUserAddress(addrID string) error {
return ur.AddressTable.DeleteWhere("id=$1 and user_id=$2", addrID, userid) return ur.AddressTable.DeleteWhere("id=$1", addrID)
} }
+2 -2
View File
@@ -117,8 +117,8 @@ func (tm *JwtTokenManager) TokenMiddleware(next echo.HandlerFunc) echo.HandlerFu
// Rend les infos disponibles aux handlers suivants via c.Get(...) // Rend les infos disponibles aux handlers suivants via c.Get(...)
c.Set(auth.ContextUserIDKey, claims.ID) c.Set(auth.ContextUserIDKey, claims.ID)
c.Set(auth.ContextEmailKey, claims.Email) //c.Set(auth.ContextEmailKey, claims.Email)
c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname) //c.Set(auth.ContextNameKey, claims.Firstname+" "+claims.Lastname)
return next(c) return next(c)
} }
+9 -9
View File
@@ -6,15 +6,15 @@ import (
) )
type File struct { type File struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Name string `db:"name" json:"name"` Name string `db:"name" json:"name"`
ContentType string `db:"content_type" json:"contentType"` ContentType string `db:"content_type" json:"contentType"`
StoragePath string `db:"storage_path" json:"-"` StoragePath string `db:"storage_path" json:"-"`
StorageFilename string `db:"storage_filename" json:"-"` StorageFilename string `db:"storage_filename" json:"-"`
Content []byte `db:"content" json:"-"` Content []byte `db:"content" json:"-"`
DateCreated time.Time `db:"date_created" json:"-"` DateCreated time.Time `db:"date_created" json:"-"`
DateUpdated time.Time `db:"date_updated" json:"-"` DateUpdated *time.Time `db:"date_updated" json:"-"`
VersionId string `db:"_version" json:"-"` VersionId string `db:"_version" json:"-"`
} }
type FileRepository interface { type FileRepository interface {
+1 -1
View File
@@ -7,7 +7,7 @@ type Settings struct {
DefaultLang string `db:"default_lang"` DefaultLang string `db:"default_lang"`
RegisterEnabled bool `db:"register_enabled"` RegisterEnabled bool `db:"register_enabled"`
PasswordEnabled bool `db:"password_enabled"` PasswordEnabled bool `db:"password_enabled"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
+29 -19
View File
@@ -5,25 +5,25 @@ import (
) )
type User struct { type User struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Firstname string `db:"firstname" json:"firstname,omitempty"` Firstname string `db:"firstname" json:"firstname,omitempty"`
Lastname string `db:"lastname" json:"lastname,omitempty"` Lastname string `db:"lastname" json:"lastname,omitempty"`
Password string `db:"password" json:"-"` Password string `db:"password" json:"-"`
Email string `db:"email" json:"email,omitempty"` Email string `db:"email" json:"email,omitempty"`
PictureID string `db:"picture" json:"picture,omitempty"` PictureID string `db:"picture" json:"picture,omitempty"`
Enabled bool `db:"enabled" json:"enabled"` Enabled bool `db:"enabled" json:"enabled"`
Theme string `db:"theme" json:"theme"` Theme string `db:"theme" json:"theme"`
Lang string `db:"lang" json:"lang"` Lang string `db:"lang" json:"lang"`
DateCreated time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
type Team struct { type Team struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
Label string `db:"label" json:"label"` Label string `db:"label" json:"label"`
DefaultModelID *string `db:"default_model_id" json:"default_model_id"` DefaultModelID *string `db:"default_model_id" json:"default_model_id"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
@@ -33,7 +33,7 @@ type UserTeam struct {
UserID string `db:"user_id" json:"user_id"` UserID string `db:"user_id" json:"user_id"`
TeamID string `db:"team_id" json:"team_id"` TeamID string `db:"team_id" json:"team_id"`
Administrator bool `db:"administrator" json:"administrator"` Administrator bool `db:"administrator" json:"administrator"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
@@ -42,8 +42,8 @@ type UserAddress struct {
ID string `db:"id" json:"id"` ID string `db:"id" json:"id"`
UserID string `db:"user_id" json:"user_id"` UserID string `db:"user_id" json:"user_id"`
Type string `db:"type" json:"type"` Type string `db:"type" json:"type"`
Address string `db:"address_id" json:"address"` Address string `db:"address" json:"address"`
DateCreated *time.Time `db:"_date_created" json:"_date_created"` DateCreated time.Time `db:"_date_created" json:"_date_created"`
DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"` DateUpdated *time.Time `db:"_date_updated" json:"_date_updated"`
VersionId string `db:"_version" json:"_version"` VersionId string `db:"_version" json:"_version"`
} }
@@ -58,6 +58,15 @@ type UserWithTeams struct {
Teams []*TeamRelation `json:"teams"` Teams []*TeamRelation `json:"teams"`
} }
func AddressTypes() []string {
return []string{
"email",
"telegram",
"whatsapp",
"signal",
}
}
type UserRepository interface { type UserRepository interface {
FindUser(id string) (*User, error) FindUser(id string) (*User, error)
FindUserByEmail(email string) (*User, error) FindUserByEmail(email string) (*User, error)
@@ -81,7 +90,8 @@ type UserRepository interface {
PopulateUserWithTeams(user *User) (*UserWithTeams, error) PopulateUserWithTeams(user *User) (*UserWithTeams, error)
ListUserAddresses(userid string) ([]*UserAddress, error) ListUserAddresses(userid string) ([]*UserAddress, error)
CreateUserAddress(userid string, addr *UserAddress) (*UserAddress, error) GetUserAddress(addrID string) (*UserAddress, error)
UpdateUserAddress(userid string, addr *UserAddress) (*UserAddress, error) CreateUserAddress(addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(userid string, addrID string) error UpdateUserAddress(addr *UserAddress) (*UserAddress, error)
DeleteUserAddress(addrID string) error
} }
+78 -3
View File
@@ -17,6 +17,10 @@ func NewUserGroup(prefix string, e *echo.Group, service *user.Service, middlewar
auth := e.Group(prefix, middlewares...) auth := e.Group(prefix, middlewares...)
auth.GET("/me", userHandler.Me) auth.GET("/me", userHandler.Me)
auth.PUT("/:id", userHandler.Update) auth.PUT("/:id", userHandler.Update)
auth.GET("/:id/address", userHandler.Addresses)
auth.POST("/:id/address", userHandler.CreateAddress)
auth.PUT("/:id/address", userHandler.UpdateAddress)
auth.DELETE("/:id/address/:address", userHandler.DeleteAddress)
return auth return auth
} }
@@ -43,15 +47,86 @@ func (h UserHandler) Update(c *echo.Context) error {
} }
updUser.ID = id updUser.ID = id
isAdmin := false // TODO : check if caller is admin if !(userID == id) {
if !(isAdmin || userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update user") return echo.NewHTTPError(http.StatusForbidden, "unauthorized to update user")
} }
user, err := h.userService.UpdateUser(&updUser, isAdmin) user, err := h.userService.UpdateUser(&updUser, false)
if err != nil { if err != nil {
return c.String(http.StatusBadRequest, err.Error()) return c.String(http.StatusBadRequest, err.Error())
} }
return c.JSON(http.StatusOK, user) return c.JSON(http.StatusOK, user)
} }
func (h UserHandler) Addresses(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
addresses, err := h.userService.ListAdresses(userID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, addresses)
}
func (h UserHandler) CreateAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
var address domain.UserAddress
if err := c.Bind(&address); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
address.UserID = userID
newaddress, err := h.userService.AddAddress(userID, address.Type, address.Address)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, newaddress)
}
func (h UserHandler) UpdateAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
var address domain.UserAddress
if err := c.Bind(&address); err != nil {
return c.String(http.StatusBadRequest, "bad request")
}
address.UserID = userID
newaddress, err := h.userService.UpdateAddress(userID, &address)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, newaddress)
}
func (h UserHandler) DeleteAddress(c *echo.Context) error {
userID := c.Get(auth.ContextUserIDKey).(string)
id := c.Param("id")
addressID := c.Param("address")
if !(userID == id) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
err := h.userService.DeleteAddress(userID, addressID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "internal error")
}
return c.JSON(http.StatusOK, "deleted")
}
@@ -31,8 +31,10 @@ type Session struct {
} }
const ContextUserIDKey = "userID" const ContextUserIDKey = "userID"
const ContextEmailKey = "userEmail" const ContextUserAdminKey = "userAdmin"
const ContextNameKey = "userName"
//const ContextEmailKey = "userEmail"
//const ContextNameKey = "userName"
// Interface définie ici car c'est un port propre au cas d'usage "auth" // Interface définie ici car c'est un port propre au cas d'usage "auth"
type Authenticator interface { type Authenticator interface {
+4 -1
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"time"
"trankilou.fr/lassistanoque/backend/internal/domain" "trankilou.fr/lassistanoque/backend/internal/domain"
) )
@@ -53,8 +54,10 @@ func (s *Service) Register(registration Registration) (*Session, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
now := time.Now()
_, err = s.userRepository.CreateTeam(session.User.ID, &domain.Team{ _, err = s.userRepository.CreateTeam(session.User.ID, &domain.Team{
Label: "Espace personnel", Label: "Espace personnel",
DateCreated: now,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
+35
View File
@@ -45,3 +45,38 @@ func (s *Service) UpdateUser(updUser *domain.User, fromAdmin bool) (*domain.User
return s.repo.UpdateUser(user) return s.repo.UpdateUser(user)
} }
func (s *Service) ListAdresses(userId string) ([]*domain.UserAddress, error) {
return s.repo.ListUserAddresses(userId)
}
func (s *Service) AddAddress(userId string, typ string, address string) (*domain.UserAddress, error) {
addr := &domain.UserAddress{
UserID: userId,
Type: typ,
Address: address,
}
return s.repo.CreateUserAddress(addr)
}
func (s *Service) UpdateAddress(userId string, address *domain.UserAddress) (*domain.UserAddress, error) {
old, err := s.repo.GetUserAddress(address.ID)
if err != nil {
return nil, err
}
if old.UserID != userId {
return nil, fmt.Errorf("unauthorized")
}
return s.repo.UpdateUserAddress(address)
}
func (s *Service) DeleteAddress(userId string, addressId string) error {
old, err := s.repo.GetUserAddress(addressId)
if err != nil {
return err
}
if old.UserID != userId {
return fmt.Errorf("unauthorized")
}
return s.repo.DeleteUserAddress(addressId)
}
Binary file not shown.
+6 -1
View File
@@ -1,7 +1,12 @@
import { api } from '../client'; import { api } from '../client';
import { type User } from "$lib/types/api" import type { User, Address } from "$lib/types/api"
export const userApi = { export const userApi = {
me: () => api.get<User>('/user/me'), me: () => api.get<User>('/user/me'),
update: (id: string, data: Partial<User>) => api.put<User>(`/user/${id}`, data), update: (id: string, data: Partial<User>) => api.put<User>(`/user/${id}`, data),
listAddresses: (id: string) => api.get<Address[]>(`/user/${id}/address`),
createAddress: (id: string, data: Partial<Address>) => api.post<Address>(`/user/${id}/address`, data),
updateAddress: (id: string, data: Partial<Address>) => api.put<Address>(`/user/${id}/address`, data),
deleteAddress: (id:string, addrid: string) => api.delete<Address[]>(`/user/${id}/address/${addrid}`),
}; };
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { uiState } from "$lib/state/ui.svelte" import { uiState } from "$lib/state/ui.svelte"
import { auth } from "$lib/state/auth.svelte"
import Menu from "$lib/components/Menu.svelte" import Menu from "$lib/components/Menu.svelte"
import { logout } from "$lib/state/auth.svelte" import { logout } from "$lib/state/auth.svelte"
import logo from "$lib/assets/logo-txt.png" import logo from "$lib/assets/logo-txt.png"
@@ -31,9 +30,7 @@
<Menu label="Tâches" icon="icon-tasks" link="/tasks"/> <Menu label="Tâches" icon="icon-tasks" link="/tasks"/>
<Menu label="Connaissances" icon="icon-graduation-cap" link="/kb"/> <Menu label="Connaissances" icon="icon-graduation-cap" link="/kb"/>
<Menu label="Historique" icon="icon-history" link="/history"/> <Menu label="Historique" icon="icon-history" link="/history"/>
{#if auth.user?.administrator}
<Menu label="Paramétrage" icon="icon-cog" link="/settings"/> <Menu label="Paramétrage" icon="icon-cog" link="/settings"/>
{/if}
</div> </div>
<div id="profile" class="w-full flex flex-row items-center border-t border-stone-200 dark:border-stone-800"> <div id="profile" class="w-full flex flex-row items-center border-t border-stone-200 dark:border-stone-800">
+7 -1
View File
@@ -3,8 +3,14 @@ export interface User {
firstname: string firstname: string
lastname: string lastname: string
email: string email: string
administrator: boolean
theme: string theme: string
lang: string lang: string
_version: string _version: string
} }
export interface Address {
id: string
type: string
address: string
_version: string
}
@@ -2,17 +2,24 @@
import { SvelteMap } from 'svelte/reactivity'; import { SvelteMap } from 'svelte/reactivity';
import type { PageProps } from './$types'; import type { PageProps } from './$types';
import { type User } from '$lib/types/api' import type { User, Address } from '$lib/types/api'
import { userApi } from '$lib/api/endpoints/user' import { userApi } from '$lib/api/endpoints/user'
import FormError from '$lib/components/FormError.svelte'; import FormError from '$lib/components/FormError.svelte';
import { applyTheme, themeToLocalStorage } from '$lib/ui/themeswitcher'; import { applyTheme, themeToLocalStorage } from '$lib/ui/themeswitcher';
import { validateEmail } from '$lib/utilities/validators' import { validateEmail } from '$lib/utilities/validators'
import { onMount } from 'svelte';
let { data }: PageProps = $props(); let { data }: PageProps = $props();
let form : User = $state(data.me) let form : User = $state(data.me)
let addresses : Address[] = $state([] as Address[])
let newAddress : Address = $state({} as Address)
let errors = new SvelteMap<string,string>() let errors = new SvelteMap<string,string>()
onMount(async ()=> {
addresses = await userApi.listAddresses(form.id)
})
$effect(()=>{ $effect(()=>{
themeToLocalStorage(form.theme) themeToLocalStorage(form.theme)
applyTheme() applyTheme()
@@ -36,6 +43,21 @@
} }
} }
const handleAddAddress = () => {
userApi.createAddress(form.id, newAddress)
.then((data)=>{
addresses.push(data)
newAddress = {} as Address
})
.catch((error)=>{ console.log(error) })
}
const handleDeleteAddress = (id: string) => {
userApi.deleteAddress(form.id,id).then(()=>{
addresses = addresses.filter((a)=> a.id!=id)
}).catch((error)=>{console.log(error)})
}
</script> </script>
<div class="mb-4 formgroup"> <div class="mb-4 formgroup">
@@ -106,4 +128,30 @@
<div class="mb-4 formgroup"> <div class="mb-4 formgroup">
<h2>Canaux de communication</h2> <h2>Canaux de communication</h2>
{#each addresses as address (address.id) }
<div class="m-2">
<div class="bg-stone-600 rounded flex inline px-2 py-1">
<span>{address.type} : </span>
<span class="font-bold">{address.address}</span>
<button class="ml-4 pointer" onclick={()=>{handleDeleteAddress(address.id)}}>X</button>
</div>
</div>
{/each}
<div class="flex gap-2 mt-6">
<div>
<select bind:value={newAddress.type} class="textinput">
<option>Email</option>
<option>Telegram</option>
<option>Whatsapp</option>
</select>
</div>
<div class="flex-1">
<input type="text" bind:value={newAddress.address} class="textinput" />
</div>
<div>
<button class="bt" onclick={handleAddAddress}>Ajouter</button>
</div>
</div>
</div> </div>
@@ -31,6 +31,12 @@
if (!validateEmail(form.email)) { if (!validateEmail(form.email)) {
errors.set("email","Adresse email invalide !") errors.set("email","Adresse email invalide !")
} }
if (!form.firstname) {
errors.set("firstname","Le prénom est obligatoire !")
}
if (!form.lastname) {
errors.set("lastname","Le nom est obligatoire !")
}
if (form.password!==form.verifPassword) { if (form.password!==form.verifPassword) {
errors.set("verifPassword","Les mots de pass ne correspondent pas.") errors.set("verifPassword","Les mots de pass ne correspondent pas.")
} }
+1 -1
View File
@@ -28,7 +28,7 @@ h2 {
@apply cursor-pointer; @apply cursor-pointer;
} }
.bt { .bt {
@apply cursor-pointer rounded bg-stone-200 px-4 py-2 hover:bg-stone-300 dark:bg-stone-800 dark:hover:bg-stone-700; @apply cursor-pointer rounded bg-stone-200 px-4 py-1 hover:bg-stone-300 dark:bg-stone-800 dark:hover:bg-stone-700;
} }
.menuitem { .menuitem {
@apply text-stone-800 text-stone-950 dark:text-stone-300 hover:dark:text-stone-50; @apply text-stone-800 text-stone-950 dark:text-stone-300 hover:dark:text-stone-50;