diff --git a/cours/sem6/sem6.md b/cours/sem6/sem6.md deleted file mode 100644 index 7bbec21..0000000 --- a/cours/sem6/sem6.md +++ /dev/null @@ -1,712 +0,0 @@ ---- -marp: true -theme: default -paginate: true -backgroundColor: #fff -footer: WEB : Ruby - Semaine 6 - Active Record / Sessions / Authentification ---- - - - -![bg right:40% 40%](../shared/images/Ruby_On_Rails_Logo.svg) - -# ActiveRecord / Sessions - -* inverse_of -* Includes -* Join -* Cookies -* Session -* Authentification - ---- - -### Associations bi-directionnelle : Détection automatique - -* Rails détecte les associations bi-directionnelle à partir du nom des associations. - -```ruby -class Author < ApplicationRecord - has_many :books -end - -class Book < ApplicationRecord - belongs_to :author -end -``` - -```ruby -irb> author = Author.first -irb> book = authors.books.first -irb> author.first_name == book.first.author.first_name # => true -irb> author.first_name = 'David' -irb> author.first_name == book.author.first_name # => true ; author.object_id == book.author.object_id -``` - -* Active Record ne charge qu'une seule copie de l'objet Author, ça évite une requête à la base de donnée, et évite les données incohérentes. - ---- - -### Associations bi-directionnelle : Détection automatique impossible - -* Rails ne parvient pas à determiner les associations inverses quand on utilise `:through`, `:foreign_key`, `:order`, - -```ruby -class Author < ApplicationRecord - has_many :books, inverse_of: 'writer' -end - -class Book < ApplicationRecord - belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' -end -``` - -```ruby -irb> author = Author.first -irb> book = authors.books.first -irb> author.first_name == book.first.author.first_name # => true -irb> author.first_name = 'David' -irb> author.first_name == book.author.first_name # => false ; author.object_id != book.author.object_id -``` - ---- - -### Associations bi-directionnelle : inverse_of - -* `inverse_of` permet d'indiquer quelle est la relation inverse d'un `belongs_to`, `has_one`, `has_many` - -```ruby -class Author < ApplicationRecord - has_many :books, inverse_of: 'writer' -end - -class Book < ApplicationRecord - belongs_to :writer, class_name: 'Author', foreign_key: 'author_id' -end -``` - -```ruby -irb> author = Author.first -irb> book = authors.books.first -irb> author.first_name == book.first.author.first_name # => true -irb> author.first_name = 'David' -irb> author.first_name == book.author.first_name # => true ; author.object_id == book.author.object_id -``` - ---- - -### Includes : eager loader les données - -* Eviter le N+1 - -```erb -<% Message.limit(25).each do |message| %> -

- <%= message.title %> - <%= message.author.name %> -

-<% end %> - -# (0.2ms) SELECT "messages".* FROM "messages" -# (0.2ms) SELECT "authors".* FROM "authors" WHERE "author"."id" = ? -# (0.2ms) SELECT "authors".* FROM "authors" WHERE "author"."id" = ? -# (0.2ms) SELECT "authors".* FROM "authors" WHERE "author"."id" = ? -.... -# => 26 requêtes -``` - ---- - -### Includes : eager loader les données - -* Toutes les associations spécifiées sont chargées en utilisant le nombre minimum de requêtes possible. - -```erb -<% Message.includes(:author).limit(10).each do |message| %> -

- <%= message.title %> - <%= message.author.name %> -

-<% end %> - -# (0.2ms) SELECT "messages".* FROM "messages" -# (0.3ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" IN (?, ?, ..) - -# => 2 requêtes -``` - ---- - -### Includes : eager loader les associations - -* On peut spécifier une liste d'associations - -```ruby -Message.includes(:author, :comments) -``` - -* Et des associations imbriquées - -```ruby -Customer.includes(orders: {books: [:supplier, :author]}).find(1) -``` - ---- - -### Join : INNER JOIN - -* Jointure à partir d'une ou plusieurs associations - -```ruby -Book.joins(:comments) # Les livres ayant des commentaires -# SELECT books.* FROM books -# INNER JOIN comments ON comments.book_id = books.id -``` - -* Récupère un livre pour chaque livre avec un commentaire - - * Attention aux doublons, pour les éviter : `Book.joins(:comments).distinct` - -* Jointure à partir de plusieurs associations - -```ruby -Book.joins(:author, :comments) # Les livres avec leur auteur qui ont au moins un commentaire -# SELECT books.* FROM books -# INNER JOIN authors ON authors.id = books.author_id -# INNER JOIN comments ON comments.book_id = books.id -``` ---- - -### Join : INNER JOIN - -* Jointures imbriquées - -```ruby -Book.joins(comments: :customer) # Les livres qui ont un commentaire par un client -# SELECT books.* FROM books -# INNER JOIN reviews ON reviews.book_id = books.id -# INNER JOIN customers ON customers.id = reviews.customer_id -``` - -* Spécifier des conditions - -```ruby -Book.joins(:comments).where('comments.created_at' => (1.week.ago..Time.now).distinct - -# Les livres uniques avec des commentaires datant de moins d'une semaine. - -Book.joins(:author).where('author.first_name' => "Nabilla" ).distinct - -# Les livres uniques avec un auteur qui se prénomme Nabilla. -``` ---- - -### Cookies - -* Les cookies permettent de stocker des informations sur le navigateur de l'utilisateur. - -* Limité à une petite quantité (4ko) de texte - -* Ils sont stockés en clair sur la navigateur et facilement accessibles, recopiables, modifiables - - * On ne stocke donc dedans aucune donnée sensible ! - -* Ils ont une date d'expiration (par défaut à la fin de la session du navigateur) - -* On peut aussi y accéder en Javascript (sauf si on l'interdit explicitement) - ---- - -### Cookies - Header HTTP Set-cookie - -* Si le serveur souhaite créer ou modifier des cookies, ils sont envoyés dans le header `Set-Cookie` de la réponse HTTP. - -``` -HTTP/2.0 200 OK -Content-Type: text/html -Set-Cookie: delicieux_cookie=choco -Set-Cookie: savoureux_cookie=menthe - -[contenu de la page] -``` - -* Peut spécifier la date d'expiration (`Expires`), restreindre l'accès (`Secure`, `HttpOnly`), et définir où les cookies sont envoyés (`Domain`, `Path`, `SameSite`) - -``` -Set-Cookie: savoureux_cookie=menthe; Expires=Mon, 27 Mar 2050 07:28:00 GMT; Secure; HttpOnly -``` - ---- - -### Cookies - Header HTTP Cookie - -* Pour toutes requêtes suivantes, le client envoie tous les cookies enregistrés (qui ont le droit d'être envoyés), dans le header `Cookie` de la requête HTTP. - -``` -GET /page_exemple.html HTTP/2.0 -Host: www.example.org -Cookie: delicieux_cookie=choco; savoureux_cookie=menthe -``` - -* Dans Rails, le module `ActionController#cookies` permet de lire et écrire les cookies HTTP. - ---- - -### Cookies - ActionController#cookies - Écrire un cookie - -* Écrire un cookie basique - -```ruby -cookies[:user_name] = 'david' - -# On doit sérialiser "à la main" les données -cookies[:lat_lon] = JSON.generate([47.68, -122.37]) -``` - -* Préciser une date d'expiration - -```ruby -cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) } - -cookies[:login] = { value: "XJ-122", expires: 1.hour } - -# Définir un cookie 'permanent', qui expire dans 20 ans -cookies.permanent[:login] = "XJ-122" -``` ---- - -### Cookies - ActionController#cookies - Écrire un cookie (suite) - -* Sécuriser les cookies - -```ruby -# Définir un cookie signé, qui empêche la modification de la valeur du cookie -cookies.signed[:user_id] = current_user.id - -# Définir un cookie chiffré, qui empêche la modification et la lecture de sa valeur -cookies.encrypted[:discount] = 45 - -# Définir un cookie chiffré, qui expire dans 20 ans -cookies.signed.permanent[:login] = 'XJ-122' -``` - -* Supprimer un cookie - -```ruby -cookies.delete :user_name -``` - ---- -### Cookies - ActionController#cookies - -* `cookies[:name]` permet de lire le cookie - -```ruby -cookies[:user_name] # => "david" -cookies[:login] # => "XJ-122" - -# On doit sérialiser les données le cas échéant -JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] - -# Lire des cookies sécurisés -# encrypted et signed retournent nil si le cookie a été modifié -cookies.encrypted[:discount] # => 45 -cookies.signed[:login] # => "XJ-122" - -# Pour connaître le nombre de cookies -cookies.size # => 2 -``` - ---- - -### Sessions: HTTP : Protocole stateless - -* HTTP est un protocole sans état, c'est à dire que le serveur oubli le client entre chaque requête. - -* Chaque requête est indépendante, et tous les clients sont traités de la même manière. - -* Plus simple pour le serveur, mais peu rendre compliqué et lourd le mécanisme d'identification - -* Plutôt utilisé par les APIs - -* Un exemple d'authentification stateless: La `basic authentication` - On fournit un header spécifique - * `Authorization: Basic ` - * Où `` = base64(:) - - ---- - -### Sessions: HTTP : Authentification stateless - -```ruby -class ApplicationController < ActionController::Base - before_action :authenticate_http_basic - - def authenticate_http_basic - # Helper de rails pour parser le header Authorization en mode basic - authenticate_with_http_basic do |username, password| - # Gestion des passwords chiffrés - user = User.find_by(username: username) - @current_user = user.authenticate(password) if user - end - end -end -``` - ---- - -### Sessions : HTTP "stateful" - -* Permet d'ajouter du "stateful" à l'application, en fournissant une session par utilisateur pour y lire et y stocker des données. - -* Lors d'une requête, si le serveur ne reçoit pas d’identifiant de session, il va en créer un et va le transmettre dans sa réponse dans un cookie. - -* Le client va donc transmettre ensuite cet identifiant de session à chaque requête. - -* Par défaut Rails stocke toute la session (id et contenu) sur le client dans un cookie chiffré et ne nécessite aucune configuration. - - * On stockera en général peu de choses en session. (ce n'est pas utile et on est limité par la taille max du cookie) - - * On peut modifier ce comportement et stocker les sessions en cache, en base de donnée, ou via memcached (mais cela a ses inconvénients). ---- - -### Sessions : Accès - -* Écriture - -```ruby -session[:user_id] = 5 -session[:last_activity] = Time.zone.now -session[:active_filters] = { - department: :all, - groups: [:web1, :web2, :web3], -} -``` - -* Lecture - -```ruby -current_user = User.find(session[:user_id]) -session[:last_activity].class # => ActiveSupport::TimeWithZone -session[:active_filters][:groups].include?(:web3) # => true -``` - -* Rails serialize automatiquement les données à la lecture et l'écriture - ---- - -### Sessions : Reset - -* `reset_session` supprime la session actuelle et en créé une nouvelle vide - -```ruby -class UserSessionsController < ActionController::Base - def logout - reset_session - @current_user = nil - end -end -``` - ---- - -### Sessions : Authentification stateful - Login - -```ruby -class UserSessionsController < ApplicationController - skip_before_action :check_login # slide suivante - - def create - if params[:username] && params[:password] - user = User.find_by(username: params[:username]) - if user && user.authenticate(params[:password]) - # La session est disponible partout - session[:current_user] = user - redirect_to root_url - return - end - end - - redirect_to(login_url) - end -end -``` ---- - -### Sessions : Retrouver un utilisateur logué - -```ruby -class ApplicationController < ActionController::Base - before_action :check_login - - def check_login - if session[:current_user] - @current_user = session[:current_user] - else - redirect_to(login_url) - end - end -end -``` - ---- - - - -![bg right:40% 40%](../shared/images/Ruby_On_Rails_Logo.svg) - - -# Correction TP - -* Strong Params -* CRUD -* Enums -* Migration en 2 (ou 3 temps) -* Gérer les foreign keys -* Filtrer des queries - ---- - -### Strong Parameters - -* Permettent de valider des inputs - -* Rails auto-wrap les params dans une clef qui correspond à la resource du contrôleur -Exemple: `CreaturesController`, rails wrappe tout dans `creature`. - -```ruby -class CreaturesController - private - def create_params - permitted = params.require(:creature).permit(:name) - permitted.require(:name) # rend le nom obligatoire - permitted - end -end -``` - -On peut forcer ou autoriser la présence d'un paramètre. - ---- - -### CRUD + L - -```ruby -class CreaturesController < ApplicationController - # route: get '/creatures', to: 'creatures#index' - def index # liste les créatures - end - # route: get '/creatures/:id', to: 'creatures#show' - def show # affiche une créature - end - # route: post '/creatures', to: 'creatures#create' - def create # créer une créature - end - # route: put '/creatures/:id', to: 'creatures#update' - def update # mettre à jour une créature - end - # route: delete '/creatures', to: 'creatures#destroy' - def destroy # supprimer une créature - end -end -``` - ---- - -### Enumération - -* Concept ActiveRecord pour gérer des flags facilement, basé sur des colonnes `integer` - -```ruby -class AddSizeColumnToCreatures < ActiveRecord::Migration[7.0] - def change - add_column :creatures, :size, :integer - end -end -``` - -```ruby -class Creature < ApplicationRecord - enum :size, [:small, :big, :giant] -end -``` - -```ruby -creature = Creature.new(name: "Big Chungus", health_points: 42, size: :big) -``` - ---- - -### Migrations en plusieurs phases - -Si on veut ajouter un nouveau champ en base de données : - -1. On met à jour le schéma pour ajouter le champ (1 migration) - -2. On met à jour la partie du code qui affecte le champ (1 update de code) - -3. On met à jour les records de la base qui n'ont pas le champ (1 migration) - -4. On met à jour le schéma pour forcer la présence du champ (1 migration optionnelle) - -5. On affiche et on utilise le champ pour les raisons métier (# updates de code) - -Dans le TP : - -Ajout de `size` -> Ajout du callback pour setter la `size` -> Update des records en DB -> Affichage de la `size` - ---- - -### Ajouter des Foreign Keys - -ActiveRecord vient avec le concept de référence qui simplifie. Il ajoute : - -* Le champ pour l'id (ex: `left_fighter_id`) -* L'index pour le champ id (ex: `index_on_left_fighter_id`) -* La Foreign Key pour le champ en question pour lier les tables -* Si le champ référence ne s'appelle pas comme une table (ex: `creature_id`), il faut préciser la table. - -```ruby -create_table :combats do |t| - # ... - t.references :left_fighter, null: false, foreign_key: { to_table: 'creatures' } - t.references :right_fighter, null: false, foreign_key: { to_table: 'creatures' } - t.references :winner, null: true, foreign_key: { to_table: 'creatures' } -end -``` - ---- - -### Ajouter des Foreign Keys - -On oubliera pas d'ajouter les relations sur les modèles : - -```ruby -class Combat < ApplicationRecord - enum :result, [:draw, :domination] - - belongs_to :left_fighter, class_name: 'Creature' - belongs_to :right_fighter, class_name: 'Creature' - # belongs_to rend la relation obligatoire, ici c'est facultatif - belongs_to :winner, class_name: 'Creature', optional: true -end -``` - ---- - -### Utilisation des Foreign Keys - -On évitera globalement de manipuler des identifiants et on utilisera des records. - -```ruby -def baston! - return if left_fighter.nil? || right_fighter.nil? - - left_hp = left_fighter.health_points - right_hp = right_fighter.health_points - left_fighter.health_points -= right_hp - right_fighter.health_points -= right_hp - - if left_fighter.alive? - self.winner = left_fighter - self.result = :domination - elsif right_fighter.alive? - self.winner = right_fighter - self.result = :domination - else - self.result = :draw - end -end -``` - ---- - -### Ne pas oublier de sauvegarder les records liés - -* C'est différent de la création automatique des records liés - -```ruby - def create - left_fighter = Creature.find(params[:left_fighter_id]) - right_fighter = Creature.find(params[:right_fighter_id]) - - @combat = Combat.new(create_params) - @combat.left_fighter = left_fighter - @combat.right_fighter = right_fighter - @combat.baston! # baston met à jour les créatures - @combat.left_fighter.save! # il faut save pour persister la perte de pv - @combat.right_fighter.save! - @combat.save! - - render json: @combat.as_json(include: COMBATS_RENDER_CONFIG) - rescue ActiveRecord::RecordNotFound - render json: {}, status: 404 - end -``` - ---- - -### Filtrer des relations à la volée - -```ruby - def index - @combats = Combat.all - - # on supporte uniquement les réels résultats - # possibles qui sont listés sur le modèle Combat - if Combat.results.include?(params[:result]) - @combats = @combats.where(result: params[:result]) - end - - # Si on fournit une query, on l'utilise pour la requête like - if params.include?(:query) - @combats = @combats.where("name LIKE ?", "%#{params[:query]}%") - end - - render json: @combats.as_json(include: COMBATS_RENDER_CONFIG) - end -``` \ No newline at end of file diff --git a/cours/sem6/sem6.pdf b/cours/sem6/sem6.pdf new file mode 100644 index 0000000..882ed4b Binary files /dev/null and b/cours/sem6/sem6.pdf differ