Softwareentwickler / Programmierer / Fachinformatiker

EveryDocs – Entwicklung einer Ruby on Rails Anwendung

In diesem Blog habe ich mich bereits an einer Reihe verschiedener Programmiersprachen und Frameworks ausprobiert. Als nächste Technologie auf meiner Liste stand Ruby mit dem Web-Framework Ruby on Rails. Dafür habe ich mir als Projekt die Entwicklung eines DMS (Document Management System) überlegt und möchte im Folgenden meinen Einstieg in Ruby on Rails schildern.

Warum ein DMS entwickeln?

Aktuell benutzte ich ecoDMS, um alle meine eingescannten Dokumente digital zu organisieren. In der kostenfreien Version gibt es aber zum Beispiel keinen Web-Client oder eine App, sodass ich auf meine Dokumente nur von meinem Laptop aus zugreifen kann. Zudem umfasst das Tool auch viele Funktionen, die für ein Unternehmen wahrscheinlich von Bedeutung, für meine private Nutzung aber zu umfangreich sind. Da ich auch keine andere zufriendestellende Lösung gefunden habe, habe ich mich dazu entschlossen, ein eigene Software dafür zu programmieren.

Ein neues Rails-Projekt erstellen

Kurz vorweg: Das Projekt habe ich auf GitHub veröffentlicht und kann hier gefunden werden: EveryDocs Github-Repository.

Die Entwicklung des Projektes führe ich meinem Linux-Server durch. Wenn Ruby installiert ist, kann man mit folgendem Befehl ein neues Projekt erstellen. In diesem Fall möchte ich mit der Anwendung nur eine REST-API anbieten, weswegen ich hier noch den Parameter ––api ergänze:

rails new everydocs-core --api

Wechselt man anschließend in den Ordner, wurde ein bereits lauffähiges Rails-Projekt angelegt. Nachdem alle notwendigen Pakete installiert worden sind, kann man den in Rails integrierten Webserver starten und im Browser aufrufen. Dieser läuft standardmäßig auf Port 3000, kann aber über den Parameter ––port geändert werden.

bundle install
rails server --port 1234
Die Standard-Landingpage eines neu erstellten Rails-Projektes im Browser
Die Standard-Landingpage eines neu erstellten Rails-Projektes im Browser

Datenmodelle erstellen

Dabei möchte ich es natürlich nicht belassen. Beginnen möchte ich damit, die Models zu erstellen, also die Klassen, die für die Datenhaltung zuständig sein sollen. Davon muss es mehrere geben, wie dem nachfolgenden ER-Modell zu entnehmen ist. Diese lassen sich sehr bequem über die Kommandozeile generieren.

rails generate model State name:string:uniq
rails generate model User name:string password_digest:string email:string:uniq
rails generate model Folder name:string:uniq folder:references user:references
rails generate model Document title:string description:text document_date:date document_url:string version:decimal folder:references user:references state:references

Die Syntax lautet also rails generate model #MODEL_NAME# und anschließend die Attributnamen jeweils mit einem Doppelpunkt getrennt von ihrem Datentyp. Über references lässt sich zudem eine Beziehung zu einem anderen Model abbilden. So hat ein Ordner unter Umständen einen Oberordner und ist einem Benutzer zugeordnet.

Durch die aufgeführten Befehle werden im Ordner app/models/ die Klassen-Dateien angelegt. Einige kleine Anpassungen müssen hier im Folgenden noch vorgenommen werden. Zudem werden unter db/migrate/ Dateien generiert, die dafür dienen, die Datenbank-Tabellen anzulegen, sobald diese konfiguriert ist. Die jeweiligen IDs werden später automatisch hinzugefügt und müssen hier nicht aufgeführt werden. Zudem werden automatisch zwei Spalten created_at und updated_at angelegt.

Generierte Datenmodelle anpassen

In /app/models/user.rb wird has_secure_password ergänzt, wodurch Methoden zum Setzen und Authentifizieren mit einem BCrypt-Passwort bereitgestellt werden (siehe auch has_secure_password Rails Dokumentation). Dazu ist es noch wichtig, „gem ‚bcrypt‘, ‚~> 3.1.7′“ – sowie vorbereitend schon einmal „gem ‚jwt'“ (später für JSON Web Token) – in das Gemfile einzutragen.

Zudem können hier die 1:n-Beziehungen angegeben werden, auf die ein User Zugriff haben soll. In diesem Fall handelt es sich um has_many :documents. Diese Beziehungen müssen an entsprechenden anderen Stellen ergänzt werden. Außerdem kann über validates_presence_of festgelegt werden, welche Attribute beim Anlegen eines neuen Objektes vorhanden sein müssen.

Datenbankverbindung einrichten

Standardmäßig verwendet Ruby on Rails eine SQLite-Datenbank. In meinem Fall möchte ich dies ändern und eine MariaDB einsetzen, die auf dem gleichen Server läuft. Dafür muss im Gemfile eine neues Paket eingetragen und anschließend über den Befehl bundle install heruntergeladen werden: gem ‚mysql2‘, ‚~> 0.3.18‘

Die Datenbank-Verbindung wird in der Datei /config/database.yml konfiguriert. Dort ist es möglich, verschiedene Datenbanken für die Entwicklungs-, Test- und Produktionsumgebung einzurichten. Für die Entwicklungsumgebung wähle ich folgende Konfiguration:

development:
  adapter: mysql2
  database: everydocs
  username: USER
  password: PASSWORD
  host: localhost

Anschließend kann man in der Datenbank anhand der generieten Dateien über folgenden Befehl die gewünschten Tabellen und Relationen anlegen lassen:

rake db:migrate RAILS_ENV=development

An der API authentifizieren

Die Datenbank ist eingerichtet und die Models sind vorhanden. Nun können wir damit beginnen, die Schnittstelle zu implementieren. Beginnen möchte ich dabei mit der Authentifizierung. Um zu verhindern, dass nicht eingeloggte Benutzer diese Anwendung benutzen können, muss die Datei app/controllers/ApplicationController.rb erweitert werden.

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks
  protect_from_forgery with: :null_session

  include Response
  include ExceptionHandler

  # Called before every action on controllers
  before_action :authorize_request
  attr_reader :current_user

  skip_before_filter :verify_authenticity_token

  private

  # Check for valid request token and return user
  def authorize_request
    @current_user = (AuthorizeApiRequest.new(request.headers).call)[:user]
  end
end

Über diesen vorher automatisch angelegten Controller wird nun über before_action festgelegt, dass die Methode authorize_request vor jeder anderen Methode eines Controllers ausgeführt wird, um den aktuell angemeldeten Benutzer zu ermitteln. Das include-Statement bindet noch zwei Module für die Fehlerbehandlung und Formatierung der Antwort als JSON ein.

Um die Authentifizierung aber überhaupt durchführen zu können, ist noch ein weiterer Controller notwendig, der diese Aufgabe übernimmt. An dieser Stelle darf die authorize_request-Methode jedoch nicht ausgeführt werden und wird über skip_before_action deaktiviert.

rails generate controller authentication
class AuthenticationController < ApplicationController
  skip_before_action :authorize_request, only: :authenticate
  
  # return auth token once user is authenticated
  def authenticate
    auth_token =
      AuthenticateUser.new(auth_params[:email], auth_params[:password]).call
    json_response(auth_token: auth_token)
  end

  private

  def auth_params
    params.permit(:email, :password)
  end
end

In den beiden Controllern werden zwei weitere Module verwendet, die ich im zu erstellenden Ordner app/auth/ abgelegt habe. Hier wiederum wird ein Modul zur De- und Encodierung von JSON verwendet, das ich im Ordner app/lib/ angelegt habe, da dies später auch noch an anderer Stelle zum Einsatz kommt.

Die Methoden zum Login sind nun vorhanden. Anschließend muss noch festgelegt werden, unter welchem Pfad diese Methoden zu erreichen sein sollen. Dafür ist die Datei config/routes.rb zuständig.

Rails.application.routes.draw do
  post 'auth/login', to: 'authentication#authenticate'
  post 'signup', to: 'users#create'
end

Über die erste Zeile wird festgelegt, dass bei einem POST-Request auf den Pfad ‚auth/login‘ die Methode authenticate aus dem AuthenticationController aufgerufen wird. Die zweite Zeile habe ich bereits hinzugefügt, um auch die Möglichkeit zu bieten, einen neuen Benutzer anzulegen. Dafür ist ein neuer UsersController erforderlich, der mit dem obigen Befehl wieder generiert werden kann.

class UsersController < ApplicationController
  skip_before_action :authorize_request, only: :create

  # return authenticated token upon signup
  def create
    user = User.create!(user_params)
    auth_token = AuthenticateUser.new(user.email, user.password).call
    response = { message: Message.account_created, auth_token: auth_token }
    json_response(response, :created)
  end

  private

  def user_params
    params.permit(:name, :email, :password, :password_confirmation)
  end
end

Auch hier wird die vorherige Authentifizierung wieder deaktiviert. Es wird ein neuer User erstellt, wobei die Parameter aus dem POST-Request vorher überprüft werden. Anschließend wird ein Token generiert, der dann zur Authentifizierung genutzt werden kann.

Auf die Daten der Anwendung zugreifen

Für die Ressourcen werden ebenfalls jeweils eigene Controller benötigt. Jede davon besitzt Methoden, um alle und anhand der ID jeweils einen einzelnen Satz auszuliefern und einen Satz anzulegen, upzudaten und zu löschen. Ein solcher Controller könnte dann folgendermaßen aussehen:

class FoldersController < ApplicationController
  before_action :set_folder, only: [:show, :update, :destroy]

  # GET /states
  def index
    @folders = current_user.folders
    json_response(@folders)
  end

  # POST /folders
  def create
    @folder = current_user.folders.create!(folder_params)
    json_response(@folder, :created)
  end

  # GET /folders/:id
  def show
    json_response(@folder)
  end

  # PUT /folders/:id
  def update
    @folder.update(folder_params)
    head :no_content
  end

  # DELETE /folders/:id
  def destroy
    @folder.destroy
    head :no_content
  end

  private

  def folder_params
    params.permit(:name, :folder, :user)
  end

  def set_folder
    @folder = Folder.find(params[:id])
  end
end

Wieder muss die Datei config/routes.rb angepasst werden. Jedoch muss nicht für für jede einzelne der oben genannten Operationen eine einzelne Zeile eingetragen werden.

Rails.application.routes.draw do
  post 'auth/login', to: 'authentication#authenticate'
  post 'signup', to: 'users#create'

  resources :documents
  resources :folders
  resources :states
end

Über das resources-Keyword werden automatisch die notwendigen Routen erzeugt. Über folgenden Befehl kann dies überprüft werden, indem alle konfigurierten Routen ausgegeben werden.

rake routes

Anwendung testen

Alle Routen sind nun konfiguriert, sodass es an der Zeit ist, die Anwendung zu testen. Dazu habe ich mir Postman installiert, um verschiedene Requests an die API zu senden. Natürlich ist dies auch mit cURL oder ähnlichen Programmen möglich. Im Browser lassen sich die GET-Requests einfach ausführen, jedoch wird es bei POST-Requests schwieriger. Zuerst einmal muss aber wieder der Server gestartet werden.

rails s --port 5678 -e development

Führe ich nun ein GET auf http://localhost:5678/documents durch, erhalte ich einen JSON String mit der Nachricht „Missing token“. Um mich anzumelden, muss ich mir zuerst einen neuen Benutzer einrichten. Dazu mache ich ein POST auf http://localhost:5678/signup durch und gebe im Body folgende Parameter mit:

  • name: Jonas Hellmann
  • email: test@test.com
  • password: Test123
  • password_digest: Test123

Als Antwort erhalte ich einen JSON-String mit der Nachricht, dass der Account erfolgreich angelegt wurde und einem Token. Führe ich nun erneut ein GET auf die Dokumente durch und gebe als „Authorization“-Header diesen Token an, ist die Fehlermeldung weg und ich erhalte einen leeren JSON-Array, was natürlich Sinn macht, da noch keine Dokumente vorhanden sind. Es zeigt aber, dass das Anmelden grundsätzlich funktioniert. Ein praktischer Nebeneffekt von der Nutzung von has_secure_password ist, dass das Passwort auch automatisch verschlüsselt in der Datenbank gespeichert wird und nicht im Klartext.

Fazit

Einige Anpassungen werden später noch notwendig sein, wenn ich noch einige andere Funktionalitäten ergänzen möchte (z.B. Import und Export der Dokumente). Fürs Erste sollte der Artikel aber einen kurzen Einblick in Rails geben und wie man mit relativ geringem Aufwand eine REST-API bereitstellen kann. Es wäre auch möglich, mit dieser Technologie die Oberfläche bereitzustellen, aber da habe ich mir schon etwas neues ausgedacht. Wahrscheinlich folgt also demnächst ein weiterer Artikel! 🙂

P.S.: Falls mir ein Fehler unterlaufen sein sollte oder ich noch Verbesserungen vornehmen kann, bin ich über jeden Kommentar dankbar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.