diff options
| author | Paul Duncan <pabs@pablotron.org> | 2016-05-21 05:25:09 -0400 | 
|---|---|---|
| committer | Paul Duncan <pabs@pablotron.org> | 2016-05-21 05:25:09 -0400 | 
| commit | 1988056df3bb929c9e34fe7f6c6e32210abf6da4 (patch) | |
| tree | f096817ecd6df37fcfdce483ad7f40ea05279055 | |
| parent | 3a6e1f52d920390041fdb0b1991546f61bc1a8ba (diff) | |
| download | guff-1988056df3bb929c9e34fe7f6c6e32210abf6da4.tar.xz guff-1988056df3bb929c9e34fe7f6c6e32210abf6da4.zip | |
add initial model stubs, login, logout, etc
| -rw-r--r-- | src/guff.cr | 337 | ||||
| -rw-r--r-- | src/views/admin-page.ecr | 22 | ||||
| -rw-r--r-- | src/views/login-page.ecr | 23 | 
3 files changed, 340 insertions, 42 deletions
| diff --git a/src/guff.cr b/src/guff.cr index 70bbc71..b5c3d44 100644 --- a/src/guff.cr +++ b/src/guff.cr @@ -1,8 +1,19 @@  require "option_parser"  require "http/server"  require "ecr/macros" +require "json" +require "secure_random"  require "./guff/*" +private macro define_model_set_getters(hash) +  {% for name, klass in hash %} +    def {{ name.id }} : {{ klass.id }} +      (@cache[{{ name }}] ||= {{ klass.id }}.new(@context)) as {{ klass.id }} +    end +  {% end %} +end + +  module Guff    class Config      property :mode, :env, :host, :port, :data_dir, :assets_dir @@ -157,11 +168,183 @@ module Guff      end    end +  module Models +    abstract class Model +      def initialize(@context : Context) +      end +    end + +    class UserModel < Model +      def login(user : String, pass : String) : String? +        if @context.development? +          if user == "test" && pass == "test" +            "0" +          else +            nil +          end +        else +          # TODO: handle login +          nil +        end +      end + +      def has_role?(user_id : String?, roles : Array(String)) +        raise "empty role list" unless roles.size > 0 + +        if user_id && user_id.size > 0 +          if @context.development? +            user_id == "0" +          else +            # TODO: add role query +          end +        else +          # empty user id +          false +        end +      end +    end + +    class SessionModel < Model +      def initialize(context : Context) +        super(context) +        @sessions = {} of String => String +      end + +      def load(id : String) : String? +        @sessions[id]? +      end + +      def save(id : String, data : String) +        if @sessions.has_key?(id) +          @sessions[id] = data +          true +        else +          false +        end +      end + +      def delete(id : String?) +        @sessions.delete(id) if id +        false +      end + +      def create(hash : Hash(String, String)) : String +        # generate id +        r = SecureRandom.hex(32) + +        # save session +        @sessions[r] = hash.to_json + +        # return session id +        r +      end +    end +  end + +  class ModelSet +    def initialize(@context : Context) +      @cache = {} of Symbol => Models::Model +    end + +    define_model_set_getters({ +      user:     Models::UserModel, +      session:  Models::SessionModel, +    }) +  end + +  class Session < Hash(String, String) +    getter :session_id + +    def initialize(@context : Context) +      super() +      @session_id = nil +    end + +    def load(id : String) +      begin +        # clear existing session +        clear + +        # load session values +        JSON.parse(@context.models.session.load(id).not_nil!).each do |key, val| +          self[key.as_s] = val.as_s +        end + +        # save session id +        @session_id = id + +        # return success +        true +      rescue err +        STDERR.puts "session load failed: #{err}" +        # invalid session id, return failure +        false +      end +    end + +    def create(hash : Hash(String, String)) : String +      clear +      merge!(hash) +      @session_id = @context.models.session.create(hash) +    end + +    def save +      if valid? +        @context.models.session.save(@session_id, to_json) + +        # return success +        true +      else +        # no session, return failure +        false +      end +    end + +    def clear +      super +      @session_id = nil +    end + +    def delete : String? +      r = @session_id + +      if valid? +        @context.models.session.delete(r) +        clear +      end + +      r +    end + +    def valid? +      @session_id != nil +    end +  end +    class Context -    property :config #, :models +    getter :config      def initialize(@config : Config) -      # @models = nil +    end + +    def models +      @models ||= ModelSet.new(self) +    end + +    def session +      @session ||= Session.new(self) +    end + +    def user_id +      session["user_id"]? +    end + +    def has_role?(roles : Array(String)) +      models.user.has_role?(user_id, roles) +    end + +    def development? +      @is_development ||= (@config.env == "development") as Bool      end    end @@ -182,7 +365,6 @@ module Guff        private def assets(key : Symbol, paths : Array(String))          paths.map { |path| TEMPLATES[key] % [h(path)] }.join        end -        def scripts(paths : Array(String))          assets(:script, paths)        end @@ -197,8 +379,16 @@ module Guff      end      class LoginPageView < View +      def initialize(context : Context, @error : String? = nil) +        super(context) +      end +        ECR.def_to_s("src/views/login-page.ecr")      end + +    class LogoutPageView < View +      ECR.def_to_s("src/views/logout-page.ecr") +    end    end    module Handlers @@ -209,15 +399,12 @@ module Guff      end      abstract class AuthenticatedHandler < Handler -      def initialize(context : Context, roles : Array(String)) +      def initialize(context : Context, @roles : Array(String))          super(context) -        @roles = roles        end        def call(context : HTTP::Server::Context) -        role = context.request.headers["x-guff-role"]? - -        if role && @roles.includes?(role) +        if @context.has_role?(@roles)            authenticated_call(context)          else            call_next(context) @@ -229,17 +416,28 @@ module Guff      class SessionHandler < Guff::Handlers::Handler        def call(context : HTTP::Server::Context) -        if @context.config.env == "development" -          # TODO: add handler support -          context.request.headers["x-guff-user-id"] = "0" -          context.request.headers["x-guff-role"] = "admin" -        else -          context.request.headers["x-guff-user-id"] = "" -          context.request.headers["x-guff-role"] = "" +        # check for forged headers +        check_headers(context.request.headers) + +        # clear session +        @context.session.clear + +        if context.request.cookies.has_key?("guff_sid") +          # load session +          @context.session.load(context.request.cookies["guff_sid"].value)          end          call_next(context)        end + +      private def check_headers(headers : HTTP::Headers) +        # FIXME: this isn't needed any more +        %w{x-guff-user-id x-guff-role}.each do |key| +          if headers.has_key?(key) +            raise "forged header: #{key}" +          end +        end +      end      end      # TODO: check referrer, add x-frame-options @@ -314,12 +512,13 @@ module Guff        end      end +    # TODO: check referrer, add x-frame-options      class AdminPageHandler < AuthenticatedHandler        def initialize(context : Context)          super(context, %w{admin editor})        end -      PATH_RE = %r{^/guff/admin/$} +      PATH_RE = %r{^/guff/admin.html$}        def authenticated_call(context : HTTP::Server::Context)          if context.request.path.not_nil!.match(PATH_RE) @@ -333,19 +532,41 @@ module Guff      end      class LoginPageHandler < Handler -      PATH_RE = %r{^/guff/login/$} -      VALID_METHODS = %w{GET HEAD} +      PATH_RE = %r{^/guff/login.html$} +      VALID_METHODS = %w{GET POST}        def call(context : HTTP::Server::Context)          if VALID_METHODS.includes?(context.request.method) &&             PATH_RE.match(context.request.path.not_nil!)            case context.request.method            when "GET" -            context.response.content_type = "text/html; charset=utf-8" -            context.response.status_code = 200 -            Views::LoginPageView.new(@context).to_s(context.response) +            reply(context.response)            when "POST" -            # TODO: try login +            begin +              # create session +              session_id = @context.session.create({ +                "user_id": login(context.request.body), +              }) + +              # add cookie +              context.response.cookies << HTTP::Cookie.new( +                name:       "guff_sid", +                value:      session_id as String, +                http_only:  true, + +                # TODO +                # expires: +                # secure: +              ) + +              # redirect to admin panel +              context.response.headers["location"] = "/guff/admin.html" +              context.response.status_code = 302 +            rescue err +              # log error +              STDERR.puts "login failed: #{err}" +              reply(context.response, "invalid login") +            end            else              raise "invalid HTTP method"            end @@ -353,6 +574,69 @@ module Guff            call_next(context)          end        end + +      private def reply( +        response : HTTP::Server::Response, +        error : String? = nil +      ) +        response.content_type = "text/html; charset=utf-8" +        response.status_code = 200 +        Views::LoginPageView.new(@context, error).to_s(response) +      end + +      private def login(body : String?) : String +        # check body +        raise "empty request body" if body.nil? || body.size == 0 + +        # parse request parameters +        params = HTTP::Params.parse(body.not_nil!) + +        # check login parameters +        raise "missing login parameters" unless %w{ +          username +          password +        }.all? do |key| +          params.has_key?(key) && params[key].size > 0 +        end + +        # try login +        user_id =  @context.models.user.login( +          params["username"], +          params["password"] +        ) + +        # check user id +        raise "invalid credentials" unless user_id + +        # return user id +        user_id +      end +    end + +    class LogoutPageHandler < Handler +      PATH_RE = %r{^/guff/logout.html$} +      VALID_METHODS = %w{GET} + +      def call(context : HTTP::Server::Context) +        if context.request.method == "GET" && +           PATH_RE.match(context.request.path.not_nil!) +          # delete session +          @context.session.delete + +          # clear cookie +          context.response.cookies << HTTP::Cookie.new( +            name:       "guff_sid", +            value:      "", +            expires:    Time.epoch(0), +            http_only:  true, +          ) + +          # draw page +          Views::LogoutPageView.new(@context).to_s(context.response) +        else +          call_next(context) +        end +      end      end      HANDLERS = [{ @@ -376,13 +660,14 @@ module Guff      }, {        dev:  false,        id:   :login, +    }, { +      dev:  false, +      id:   :logout,      }]      def self.get(context : Context) : Array(HTTP::Handler) -      is_dev = (context.config.env == "development") -              HANDLERS.select { |row| -        !(row[:dev] as Bool) || is_dev +        !(row[:dev] as Bool) || context.development?        }.map { |row|          make_handler(row[:id] as Symbol, context)        } @@ -407,6 +692,8 @@ module Guff          AdminPageHandler.new(context)        when :login          LoginPageHandler.new(context) +      when :logout +        LogoutPageHandler.new(context)        else          raise "unknown handler id: #{handler_id}"        end diff --git a/src/views/admin-page.ecr b/src/views/admin-page.ecr index 284b1a7..47931a8 100644 --- a/src/views/admin-page.ecr +++ b/src/views/admin-page.ecr @@ -6,9 +6,9 @@      <%=        styles %w{ -        ../assets/font-awesome-4.5.0/css/font-awesome.min.css -        ../assets/bootstrap-3.3.6/css/bootstrap.min.css -        ../assets/bootstrap-3.3.6/css/bootstrap-theme.min.css +        assets/font-awesome-4.5.0/css/font-awesome.min.css +        assets/bootstrap-3.3.6/css/bootstrap.min.css +        assets/bootstrap-3.3.6/css/bootstrap-theme.min.css        }      %>    </head> @@ -18,14 +18,14 @@    <%=      scripts %w{ -      ../assets/jquery-2.2.1.min.js -      ../assets/luigi-template-0.4.1.min.js -      ../assets/js/util.js -      ../assets/bootstrap-3.3.6/js/bootstrap.min.js -      ../assets/ckeditor-4.5.8-custom/ckeditor.js -      ../assets/js/search-field.js -      ../assets/test/tab-users.js -      ../assets/test/tab-posts.js +      assets/jquery-2.2.1.min.js +      assets/luigi-template-0.4.1.min.js +      assets/js/util.js +      assets/bootstrap-3.3.6/js/bootstrap.min.js +      assets/ckeditor-4.5.8-custom/ckeditor.js +      assets/js/search-field.js +      assets/test/tab-users.js +      assets/test/tab-posts.js      }    %>  </html> diff --git a/src/views/login-page.ecr b/src/views/login-page.ecr index 5e3c8ce..9f2082a 100644 --- a/src/views/login-page.ecr +++ b/src/views/login-page.ecr @@ -6,9 +6,9 @@      <%=        styles %w{ -        ../assets/font-awesome-4.5.0/css/font-awesome.min.css -        ../assets/bootstrap-3.3.6/css/bootstrap.min.css -        ../assets/bootstrap-3.3.6/css/bootstrap-theme.min.css +        assets/font-awesome-4.5.0/css/font-awesome.min.css +        assets/bootstrap-3.3.6/css/bootstrap.min.css +        assets/bootstrap-3.3.6/css/bootstrap-theme.min.css        }      %>    </head> @@ -25,7 +25,18 @@            </div><!-- panel-heading -->            <div class='panel-body'> -            <form method='post'> +            <% if @error %> +              <div class='panel panel-danger'> +                <div class='panel-heading'> +                  <b> +                    <i class='fa fa-exclamation-triangle'></i> +                    Error: Invalid Login +                  </b> +                </div><!-- panel-heading --> +              </div><!-- panel --> +            <% end %> + +            <form method='post' action='login.html'>                <div class='form-group'>                  <label for='user'>                    User @@ -73,8 +84,8 @@    <%=      scripts %w{ -      ../assets/jquery-2.2.1.min.js -      ../assets/bootstrap-3.3.6/js/bootstrap.min.js +      assets/jquery-2.2.1.min.js +      assets/bootstrap-3.3.6/js/bootstrap.min.js      }    %>  </html> | 
