diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000..378fa4d --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,193 @@ +body { + color: #999; + background: #f5f5f5; + font-family: 'Roboto', sans-serif; +} + +.avatar { + color: #fff; + margin: 0 auto 30px; + text-align: center; + width: 100px; + height: 100px; + border-radius: 50%; + z-index: 9; + background: #2389cd; + padding: 15px; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.1); +} + +.avatar i { + font-size: 62px; +} + +.form-control { + height: 41px; + background: #f2f2f2; + box-shadow: none !important; + border: none; +} + +.form-control:focus { + border-color: #2389cd; +} + +.login-form { + width: 400px; + margin: 30px auto; + padding: 30px 0; +} + +.login-form form { + color: #999; + border-radius: 3px; + margin-bottom: 15px; + background: #fff; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); + padding: 30px; +} + +.login-form h4 { + text-align: center; + font-size: 22px; + margin-bottom: 20px; +} + +.login-form .form-group { + margin-bottom: 20px; +} + +.login-form .form-control, +.login-form .btn { + min-height: 40px; + border-radius: 2px; + transition: all 0.5s; +} + +.login-form .close { + position: absolute; + top: 15px; + right: 15px; +} + +.login-form .btn, +.login-form .btn:active { + background: #2389cd !important; + border: none; + line-height: normal; +} + +.login-form .btn:hover, +.login-form .btn:focus { + background: #2389cd !important; +} + +.login-form .checkbox-inline { + float: left; +} + +.login-form input[type="checkbox"] { + position: relative; + top: 2px; +} + +.login-form .forgot-link { + float: right; +} + +.login-form .small { + font-size: 13px; +} + +.login-form a { + color: #2389cd; +} + +.form-control { + height: 41px; + background: #f2f2f2; + box-shadow: none !important; + border: none; +} + +.form-control:focus { + background: #e2e2e2; +} + +.form-control, +.btn { + border-radius: 3px; +} + +.signup-form { + width: 400px; + margin: 30px auto; + padding: 30px 0; +} + +.signup-form form { + color: #999; + border-radius: 3px; + margin-bottom: 15px; + background: #fff; + box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); + padding: 30px; +} + +.signup-form h2 { + font-size: 22px; + margin-bottom: 20px; + +} + +.signup-form hr { + margin: 0 -30px 20px; +} + +.signup-form .form-group { + margin-bottom: 20px; +} + +.signup-form input[type="checkbox"] { + margin-top: 3px; +} + +.signup-form .row div:first-child { + padding-right: 10px; +} + +.signup-form .row div:last-child { + padding-left: 10px; +} + +.signup-form .btn { + font-size: 16px; + font-weight: bold; + background: #3598dc; + border: none; + min-width: 140px; +} + +.signup-form .btn:hover, +.signup-form .btn:focus { + background: #2389cd !important; + outline: none; +} + +.signup-form a:hover { + text-decoration: none; +} + +.signup-form form a { + color: #3598dc; + text-decoration: none; +} + +.signup-form form a:hover { + text-decoration: underline; +} + +.signup-form .hint-text { + padding-bottom: 15px; + text-align: center; +} \ No newline at end of file diff --git a/public/css/signin.css b/public/css/signin.css deleted file mode 100644 index 4732d1f..0000000 --- a/public/css/signin.css +++ /dev/null @@ -1,39 +0,0 @@ -html, -body { - height: 100%; -} - -body { - display: flex; - align-items: center; - padding-top: 40px; - padding-bottom: 40px; - background-color: #f5f5f5; -} - -.form-signin { - width: 100%; - max-width: 330px; - padding: 15px; - margin: auto; -} - -.form-signin .checkbox { - font-weight: 400; -} - -.form-signin .form-floating:focus-within { - z-index: 2; -} - -.form-signin input[type="email"] { - margin-bottom: -1px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} - -.form-signin input[type="password"] { - margin-bottom: 10px; - border-top-left-radius: 0; - border-top-right-radius: 0; -} diff --git a/public/templates/authorize.html b/public/templates/authorize.html index 00d4536..e42a9cc 100644 --- a/public/templates/authorize.html +++ b/public/templates/authorize.html @@ -1,78 +1,45 @@ - - +{% extends "layout.html" %} +{% set title = "Authorize" %} - - - - - - - Authority - Authorize {{client.name}} +{% block body %} +
- - - + {% include "errors.html" %} +
+ + + + + + + - - +
- -
-
-
- - - - - - - - -
- -
-
{{client.name}}
-

{{client.description}}

-
- - -
-
-
+ +

{{client.description}}

+ +
- +
- +
+ +
+ +
- \ No newline at end of file +

+ Authorizing will redirect to +
+ {{client.redirect_uri}} +

+
+ +
+{% endblock %} \ No newline at end of file diff --git a/public/templates/errors.html b/public/templates/errors.html new file mode 100644 index 0000000..fc887c9 --- /dev/null +++ b/public/templates/errors.html @@ -0,0 +1,12 @@ +{% if errors %} + +{% endif %} \ No newline at end of file diff --git a/public/templates/header.html b/public/templates/header.html new file mode 100644 index 0000000..10f1473 --- /dev/null +++ b/public/templates/header.html @@ -0,0 +1,11 @@ + + + + {% block page_title %}Authority - {{title}}{% endblock %} + + + + + + \ No newline at end of file diff --git a/public/templates/layout.html b/public/templates/layout.html new file mode 100644 index 0000000..bb765f2 --- /dev/null +++ b/public/templates/layout.html @@ -0,0 +1,18 @@ + + +{% block header %} +{% include "header.html" %} +{% endblock %} + + + + {% block body %} + {% endblock %} + + + + + + \ No newline at end of file diff --git a/public/templates/new_owner.html b/public/templates/new_owner.html new file mode 100644 index 0000000..2390d70 --- /dev/null +++ b/public/templates/new_owner.html @@ -0,0 +1,52 @@ +{% extends "layout.html" %} +{% set title = "Signup" %} + +{% block body %} +
+ + {% include "errors.html" %} +
+ +
+ +

Please fill in this form to create an account!

+
+
+
+
+ +
+
+
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
Already have an account? Login here
+
+{% endblock %} \ No newline at end of file diff --git a/public/templates/new_session.html b/public/templates/new_session.html new file mode 100644 index 0000000..fa525b6 --- /dev/null +++ b/public/templates/new_session.html @@ -0,0 +1,28 @@ +{% extends "layout.html" %} +{% set title = "Signin" %} + +{% block body %} +
+ {% include "errors.html" %} +
+ +
+ +
+ +
+
+ +
+
+ + Forgot Password? +
+
+ +
+
+
Don't have an account? Sign up
+
+{% endblock %} \ No newline at end of file diff --git a/public/templates/signin.html b/public/templates/signin.html deleted file mode 100644 index f7ea76a..0000000 --- a/public/templates/signin.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - Authority - Signin - - - - - - - - - -
- {% if errors %} - - {% endif %} - -
- -

Please sign in

-
- - -
- - - -
-
-
-
- - - \ No newline at end of file diff --git a/shard.yml b/shard.yml index ddc592a..3614b76 100755 --- a/shard.yml +++ b/shard.yml @@ -1,6 +1,6 @@ --- name: authority -version: 0.1.0 +version: 1.1.0 crystal: 1.2.1 license: MIT diff --git a/spec/flows/authorization_code_flux.cr b/spec/flows/authorization_code_flux.cr index 0df235a..5633042 100644 --- a/spec/flows/authorization_code_flux.cr +++ b/spec/flows/authorization_code_flux.cr @@ -12,17 +12,19 @@ class AuthorizationCodeFlux < Flux def call redirect = step do + fullscreen visit @url + sleep 3.seconds fill "#username", @username, by: :css fill "#password", @password, by: :css submit "#signin" - implicit_wait 5.seconds + sleep 3.seconds submit "#approve" - implicit_wait 5.seconds + sleep 3.seconds URI.parse(current_url).query_params end diff --git a/spec/flows/register_owner_flux.cr b/spec/flows/register_owner_flux.cr new file mode 100644 index 0000000..62ad5be --- /dev/null +++ b/spec/flows/register_owner_flux.cr @@ -0,0 +1,39 @@ +require "flux" +require "uri" + +class RegisterOwnerFlux < Flux + def self.flow(url) + new(url).call + end + + def initialize(@url : String) + super() + end + + def call + password = Faker::Internet.password + email = Faker::Internet.email + + step do + fullscreen + visit @url + + fill "[name=first_name]", Faker::Name.first_name, by: :css + fill "[name=last_name]", Faker::Name.last_name, by: :css + fill "[name=email]", email, by: :css + fill "[name=username]", email, by: :css + fill "[name=password]", password, by: :css + fill "[name=confirm_password]", password, by: :css + + checkbox "terms" + + sleep 1.seconds + + submit "[type=submit]" + + sleep 2.seconds + + current_url + end + end +end diff --git a/spec/flows/session_flux.cr b/spec/flows/session_flux.cr index 9945e1e..2c3a0df 100644 --- a/spec/flows/session_flux.cr +++ b/spec/flows/session_flux.cr @@ -22,10 +22,14 @@ class SessionFlux < Flux step do visit "http://localhost:4000/signin" + sleep 3.seconds + fill "#username", username, by: :css fill "#password", password, by: :css submit "#signin" + sleep 3.seconds + URI.parse(current_url).query_params end end diff --git a/spec/register_owner_spec.cr b/spec/register_owner_spec.cr new file mode 100644 index 0000000..9c5c624 --- /dev/null +++ b/spec/register_owner_spec.cr @@ -0,0 +1,10 @@ +require "./spec_helper" + +describe "Register User Flow" do + it "creates a new user" do + url = RegisterOwnerFlux.flow("http://localhost:4000/register") + + Authority::User.query.count.should eq 1 + url.should eq "http://localhost:4000/signin" + end +end diff --git a/src/authority.cr b/src/authority.cr index efbed84..383ef96 100755 --- a/src/authority.cr +++ b/src/authority.cr @@ -8,7 +8,7 @@ module Authority include Azu configure do |c| # Default HTML templates path - c.templates.path = "public/templates" + c.templates.path = "./public/templates" # Uncomment to enable Spark real time apps # Docs: https://azutopia.gitbook.io/azu/spark-1 @@ -20,6 +20,7 @@ module Authority end require "./config/**" +require "./validators/**" require "./services/**" require "./requests/**" require "./providers/**" diff --git a/src/endpoints/owner/create_endpoint.cr b/src/endpoints/owner/create_endpoint.cr new file mode 100644 index 0000000..1c2205a --- /dev/null +++ b/src/endpoints/owner/create_endpoint.cr @@ -0,0 +1,28 @@ +module Authority + module Owner + class CreateEndpoint + include Endpoint(NewOwnerRequest, NewOwnerResponse | EmptyResponse) + + post "/register" + + def call : NewOwnerResponse | EmptyResponse + return owner_error unless new_owner_request.valid? + OwnerService.register new_owner_request + + redirect to: "/signin" + EmptyResponse.new + end + + private def owner_error + status 400 + NewOwnerResponse.new new_owner_request, owner_errors_html + end + + private def owner_errors_html + new_owner_request.errors.map do |error| + "#{error.field}: #{error.message}" + end + end + end + end +end diff --git a/src/endpoints/owner/new_endpoint.cr b/src/endpoints/owner/new_endpoint.cr new file mode 100644 index 0000000..ae90f65 --- /dev/null +++ b/src/endpoints/owner/new_endpoint.cr @@ -0,0 +1,13 @@ +module Authority + module Owner + class NewEndpoint + include Endpoint(NewOwnerRequest, NewOwnerResponse) + + get "/register" + + def call : NewOwnerResponse + NewOwnerResponse.new new_owner_request + end + end + end +end diff --git a/src/requests/new_owner_request.cr b/src/requests/new_owner_request.cr new file mode 100644 index 0000000..e94d2e3 --- /dev/null +++ b/src/requests/new_owner_request.cr @@ -0,0 +1,20 @@ +module Authority + struct NewOwnerRequest + include Request + + getter first_name : String = "" + getter last_name : String = "" + getter email : String = "" + getter username : String = "" + getter password : String = "" + getter confirm_password : String = "" + + use ConfirmPasswordValidator + + validate first_name, message: "Param first_name must be present.", presence: true + validate last_name, message: "Param last_name must be present.", presence: true + validate email, message: "Param email must be present.", presence: true + validate username, message: "Param username must be present.", presence: true + validate password, message: "Param password must be present.", presence: true + end +end diff --git a/src/responses/new_owner_response.cr b/src/responses/new_owner_response.cr new file mode 100644 index 0000000..c25f76f --- /dev/null +++ b/src/responses/new_owner_response.cr @@ -0,0 +1,23 @@ +module Authority + struct NewOwnerResponse + include Response + include Templates::Renderable + + TEMPLATE = "new_owner.html" + getter req : NewOwnerRequest, errors : Array(String)? + + def initialize(@req : NewOwnerRequest, @errors = nil) + end + + def render + render(TEMPLATE, { + errors: errors, + first_name: req.first_name, + last_name: req.last_name, + email: req.email, + username: req.username, + password: req.password, + }) + end + end +end diff --git a/src/responses/session_show_response.cr b/src/responses/session_show_response.cr index 3dbd5f7..0b09c55 100644 --- a/src/responses/session_show_response.cr +++ b/src/responses/session_show_response.cr @@ -4,7 +4,7 @@ module Authority include Response include Templates::Renderable - TEMPLATE = "signin.html" + TEMPLATE = "new_session.html" def initialize(@forward_url : String = "/user-info", @errors : Array(String)? = nil) end diff --git a/src/services/owner_service.cr b/src/services/owner_service.cr new file mode 100644 index 0000000..800bcdd --- /dev/null +++ b/src/services/owner_service.cr @@ -0,0 +1,22 @@ +module Authority + class OwnerService + def self.register(req : NewOwnerRequest) + new(req).create! + end + + def initialize(@req : NewOwnerRequest) + end + + def create! + User.new({ + first_name: @req.first_name, + last_name: @req.last_name, + email: @req.email, + username: @req.username, + password: @req.password, + email_verified: false, + scope: "", + }).save! + end + end +end diff --git a/src/validators/confirm_password_validator.cr b/src/validators/confirm_password_validator.cr new file mode 100644 index 0000000..60416e1 --- /dev/null +++ b/src/validators/confirm_password_validator.cr @@ -0,0 +1,18 @@ +module Authority + class ConfirmPasswordValidator < Schema::Validator + getter :record, :field, :message + + def initialize(@record : Authority::NewOwnerRequest) + @field = :password + @message = "Password did not match with confirm password." + end + + def valid? : Array(Schema::Error) + if @record.password != @record.confirm_password + [Schema::Error.new @field, @message] + else + [] of Schema::Error + end + end + end +end