diff --git a/article_scraper/resources/tests/thumbnails/a-chacon.html b/article_scraper/resources/tests/thumbnails/a-chacon.html new file mode 100644 index 0000000..3e1bc5e --- /dev/null +++ b/article_scraper/resources/tests/thumbnails/a-chacon.html @@ -0,0 +1,808 @@ + + + + + + + + + + + +PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only. | a-chacon + + + + + + + + + + + + + + + + + + + + + + +
+

Building an API with Rails? Discover + + OasRails, a Rails engine for generate automatic interactive documentation. +

+
+ + + + + + + + +
+
+ + +

PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only.

+ + + + PoC: Usando el Generador de Autenticación de Rails 8 (Beta) En Modo API-Only. + +
+

Como ya saben, una de las funcionalidades nuevas de Rails 8 es el nuevo generador básico de autenticación que viene a demostrar que no es tan complejo desarrollar todo lo que respecta a autenticación en una aplicación con Rails y que muchas veces no es necesario depender de terceros (gemas). La discusión comenzó aquí.

+ +

Dicho esto, veamos que pasa usando el generador en una aplicación API-Only:

+ +
 rails -v
+Rails 8.0.0.beta1
+
+ +
 rails new app --api & cd app
+
+ +

Y ejecutamos el nuevo comando:

+ +
 rails g authentication
+      create  app/models/session.rb
+      create  app/models/user.rb
+      create  app/models/current.rb
+      create  app/controllers/sessions_controller.rb
+      create  app/controllers/concerns/authentication.rb
+      create  app/controllers/passwords_controller.rb
+      create  app/mailers/passwords_mailer.rb
+      create  app/views/passwords_mailer/reset.html.erb
+      create  app/views/passwords_mailer/reset.text.erb
+      create  test/mailers/previews/passwords_mailer_preview.rb
+        gsub  app/controllers/application_controller.rb
+       route  resources :passwords, param: :token
+       route  resource :session
+        gsub  Gemfile
+      bundle  install --quiet
+    generate  migration CreateUsers email_address:string!:uniq password_digest:string! --force
+       rails  generate migration CreateUsers email_address:string!:uniq password_digest:string! --force
+      invoke  active_record
+      create    db/migrate/20241016002139_create_users.rb
+    generate  migration CreateSessions user:references ip_address:string user_agent:string --force
+       rails  generate migration CreateSessions user:references ip_address:string user_agent:string --force
+      invoke  active_record
+      create    db/migrate/20241016002140_create_sessions.rb
+
+ +

Ok, ahora por ejemplo, si revisamos SessionsController veremos que el método de Login se ve de la siguiente forma:

+ +
  def create
+    if user = User.authenticate_by(params.permit(:email_address, :password))
+      start_new_session_for user
+      redirect_to after_authentication_url
+    else
+      redirect_to new_session_url, alert: "Try another email address or password."
+    end
+  end
+
+ +

O sea, redirecciona a rutas y/o vistas que en nuestra API no existen ni hacen sentido, y además si inspeccionamos el metodo start_new_session_for nos daremos cuenta de que el sistema está basado 100% en autenticación mediante cookies. Entonces, ¿qué hacemos?

+ +

Mi propuesta es la siguiente: el generador crea las bases para la autenticación y creo que funciona bastante bien, por lo que con unas pequeñas modificaciones podemos dejar funcionando una autenticación Bearer (Token Authentication) rápidamente en nuestra API con Rails 8 más los archivos ya generados.

+ +

El primer paso será agregar persistencia para nuestro token, para esto modificaremos la migración que crea las sessiones y agregaremos un nuevo campo llamado token:

+ +
    create_table :sessions do |t|
+      t.references :user, null: false, foreign_key: true
+      t.string :ip_address
+      t.string :user_agent
+      t.string :token     # HERE
+
+      t.timestamps
+    end
+
+ +

Ahora simplemente ejecuta rails db:migrate y create un usuario de prueba por consola, yo lo haré con esta línea User.create(email_address: "[email protected]", password: "123456789") (Lo utilizaremos más tarde). Luego debemos crear un nuevo token para cada sesión nueva de un usuario, para esto lo más simple es usar un callback en el modelo Session:

+ +
# app/models/sessions.rb
+class Session < ApplicationRecord
+  belongs_to :user
+  before_create :generate_token # Here call
+
+  private
+  def generate_token # Here implement, generate the token as you wish.
+    self.token = Digest::SHA1.hexdigest([ Time.now, rand ].join)
+  end
+end
+
+ +

Ahora volviendo al metodo start_new_session_for en el concern Authentication, no es necesario que creemos una cookie, asi que debemos remover esa linea y dejar el metodo de la siguiente forma:

+ +
# app/controllers/concerns/authentication.rb
+def start_new_session_for(user)
+  user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
+    Current.session = session
+  end
+end
+
+ +

Y modificaremos el create de SessionsController para que las respuestas sean en formato json y no redirecciones:

+ +
# app/controllers/sessions_controller.rb
+def create
+  if user = User.authenticate_by(params.permit(:email_address, :password))
+    start_new_session_for user
+    render json: { data: { token: Current.session.token  } }
+  else
+    render json: {}, status: :unauthorized
+  end
+end
+
+ +

Para hacer que todo esto funcione debemos hacer dos cosas:

+ +
    +
  1. +

    Incluir el modulo Authentication en ApplicationController:

    + +
    # app/controllers/application_controller.rb
    +class ApplicationController < ActionController::API
    +  include Authentication
    +end
    +
    +
  2. +
  3. +

    Eliminar la linea numero 6 de este mismo concern:

    + +
    # app/controllers/concerns/authentication.rb
    +  included do
    +    before_action :require_authentication
    +    helper_method :authenticated? # This, we don't use helpers in APIs
    +  end
    +
    +
  4. +
+ +

Hasta este punto ya deberíamos tener el login funcionando. Para probar esto voy a agregar OasRails, que a propósito ya está funcionando con Rails 8 y voy a enviar un par de peticiones a ver como se comporta, no explicaré como implementar OasRails, para eso puedes ver el repositorio o leer más en este post.

+ +

Inicio de sesión exitoso:

+ +

+ +

Inicio de sesión fallido:

+ +

+ +
+ +

Ya podemos generar tokens, ahora modificaremos el código para autenticarnos con ese mismo token. Para eso, cambiaremos la lógica de buscar la sesión actual del usuario con base en la cookie a buscarla basándonos en la cabecera Authorization:

+ +

+# app/controllers/concerns/authentication.rb
+  def resume_session
+    Current.session = find_session_by_token
+  end
+
+  def find_session_by_cookie
+    Session.find_by(token: request.headers[:authorization]&.split(" ")[-1])
+  end
+
+ +

Para probar esto creo que tendremos que hacer rápidamente un modelo que dependa de User y que requiera autenticación para utilizar. Intentemos con rails g scaffold project title:string description:text user:references y le agregamos al principio del controlador la línea de código before_action :require_authentication.

+ +

Aquí les dejo una pequeña prueba del index de Projects autenticado con el token que obtuve en las pruebas anteriores:

+ +

+ +
+ +

Con esto ya tienes gran parte de la lógica de autenticación funcionando en la aplicación API-Only. Te queda continuar con las modificaciones en el resto de los endpoints para que las respuestas sean en formato json y no supuestas vistas que no existen en la aplicación.

+ +

Probablemente de aquí a que se lance la versión final de Rails 8 aparezca un PR solucionando esto y el generador funcione correctamente en modo API-Only. Hasta entonces, con estas pequeñas modificaciones ya puedes seguir construyendo tu API.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + diff --git a/article_scraper/src/full_text_parser/mod.rs b/article_scraper/src/full_text_parser/mod.rs index 37a5f32..a45b36d 100644 --- a/article_scraper/src/full_text_parser/mod.rs +++ b/article_scraper/src/full_text_parser/mod.rs @@ -275,7 +275,7 @@ impl FullTextParser { /// See: /// - /// - - /// These two functions should be removed when the issue is fixed in libxml crate. + /// These two functions should be removed when the issue is fixed in libxml crate. fn try_usize_to_i32(value: usize) -> Result { if cfg!(target_pointer_width = "16") || (value < i32::MAX as usize) { // Cannot safely use our value comparison, but the conversion if always safe. @@ -514,6 +514,22 @@ impl FullTextParser { return Some(thumb); } + if let Ok(thumb) = Util::get_attribute( + context, + "//meta[contains(@property, 'twitter:image')]", + "content", + ) { + return Some(thumb); + } + + if let Ok(thumb) = Util::get_attribute( + context, + "//meta[contains(@property, 'og:image')]", + "content", + ) { + return Some(thumb); + } + if let Ok(thumb) = Util::get_attribute(context, "//link[contains(@rel, 'image_src')]", "href") { diff --git a/article_scraper/src/full_text_parser/tests.rs b/article_scraper/src/full_text_parser/tests.rs index 99a5235..b111fcd 100644 --- a/article_scraper/src/full_text_parser/tests.rs +++ b/article_scraper/src/full_text_parser/tests.rs @@ -278,3 +278,17 @@ Foto: IMAGO/Vaclav Salek / IMAGO/CTK Photo "https://cdn.prod.www.spiegel.de/images/a4573666-f15e-4290-8c73-a0c6cd4ad3b2_w948_r1.778_fpx29.99_fpy44.98.jpg" ) } + +#[test] +fn extract_thumbnail_a_chacon() { + let html = std::fs::read_to_string(format!("./resources/tests/thumbnails/a-chacon.html")) + .expect("Failed to read source HTML"); + let doc = FullTextParser::parse_html_string_patched(&html).unwrap(); + let ctx = Context::new(&doc).unwrap(); + + let thumb = FullTextParser::check_for_thumbnail(&ctx).unwrap(); + assert_eq!( + thumb, + "https://a-chacon.com/assets/images/rails8-poc-api-auth.webp" + ) +}