basic login

This commit is contained in:
Dan Elbert 2018-03-30 17:08:09 -05:00
parent bb223af9ae
commit 97eca6d319
17 changed files with 410 additions and 35 deletions

View File

@ -1,6 +1,17 @@
class UsersController < ApplicationController class UsersController < ApplicationController
before_action :ensure_valid_user, except: [:login, :verify_login, :new, :create] UserProxy = Struct.new(:user_id)
before_action :ensure_valid_user, except: [:show, :login, :verify_login, :new, :create]
skip_before_action :verify_authenticity_token, only: [:verify_login]
def show
if current_user
render json: { id: current_user.id, name: current_user.display_name }
else
render json: nil
end
end
def login def login
@ -14,13 +25,16 @@ class UsersController < ApplicationController
end end
def verify_login def verify_login
if user = User.authenticate(params[:username], params[:password])
set_current_user(user) respond_to do |format|
flash[:notice] = "Welcome, #{user.display_name}" if user = User.authenticate(params[:username], params[:password])
redirect_to root_path set_current_user(user)
else format.html { redirect_to root_path, notice: "Welcome, #{user.display_name}" }
flash[:error] = "Invalid credentials" format.json { render json: { success: true, user: { id: user.id, name: user.display_name } } }
render :login else
format.html { flash[:error] = "Invalid credentials"; render :login }
format.json { render json: { success: false, message: 'Invalid Credentials', user: nil } }
end
end end
end end

View File

@ -15,17 +15,33 @@
<script> <script>
import { mapState } from "vuex"; import { mapMutations, mapState } from "vuex";
import AppNavbar from "./AppNavbar"; import AppNavbar from "./AppNavbar";
import api from "../lib/Api";
export default { export default {
computed: { computed: {
...mapState({ ...mapState({
hasError: state => state.error !== null, hasError: state => state.error !== null,
error: state => state.error error: state => state.error,
authChecked: state => state.authChecked
}) })
}, },
methods: {
...mapMutations([
'setUser'
])
},
mounted() {
if (this.user === null && this.authChecked === false) {
this.loadResource(api.getCurrentUser(), user => {
this.setUser(user);
})
}
},
components: { components: {
AppNavbar AppNavbar
} }

View File

@ -0,0 +1,100 @@
<template>
<span class="icon" :class="sizeClass" @click="$emit('click', $event)">
<svg v-html="svgContent" v-bind="svgAttributes"></svg>
</span>
</template>
<script>
import X from "open-iconic/svg/x";
import Person from "open-iconic/svg/person";
import LockLocked from "open-iconic/svg/lock-locked";
import LockUnlocked from "open-iconic/svg/lock-unlocked";
const iconMap = {
x: X,
person: Person,
'lock-locked': LockLocked,
'lock-unlocked': LockUnlocked
};
const sizeMap = {
sm: 'is-small',
md: '' ,
lg: 'is-medium',
xl: 'is-large'
};
export default {
props: {
icon: {
validator: (i) => iconMap[i] !== undefined
},
size: {
required: false,
type: String,
validator: (s) => sizeMap[s] !== undefined,
default: 'md'
}
},
computed: {
svgObj() {
return iconMap[this.icon];
},
svgAttributes() {
const attrs = {
class: this.size
};
for (let a of ['viewBox', 'xmlns']) {
if (this.svgObj.attributes[a]) {
attrs[a] = this.svgObj.attributes[a];
}
}
return attrs;
},
svgContent() {
return this.svgObj.content;
},
sizeClass() {
return sizeMap[this.size];
}
}
}
</script>
<style lang="scss" scoped>
.icon {
svg {
width: 100%;
height: 100%;
fill: currentColor;
&.sm {
width: 1em;
height: 1em;
}
&.md {
width: 1.33em;
height: 1.33em;
}
&.lg {
width: 2em;
height: 2em;
}
&.xl {
width: 3em;
height: 3em;
}
}
}
</style>

View File

@ -0,0 +1,59 @@
<template>
<div ref="container">
<div ref="modal" :class="['popup', 'modal', { 'is-active': open }]">
<div class="modal-background" @click="close"></div>
<div class="modal-card">
<header class="modal-card-head">
<slot name="title">
<p class="modal-card-title">{{ title }}</p>
<app-icon icon="x" aria-label="close" @click="close"></app-icon>
</slot>
</header>
<section class="modal-card-body">
<slot></slot>
</section>
</div>
</div>
</div>
</template>
<script>
import AppIcon from "./AppIcon";
export default {
props: {
open: {
type: Boolean,
default: false
},
title: String
},
mounted() {
document.body.appendChild(this.$refs.modal);
},
beforeDestroy() {
this.$refs.container.appendChild(this.$refs.modal);
},
methods: {
close() {
this.$emit("dismiss");
}
},
components: {
AppIcon
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -23,17 +23,23 @@
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item has-dropdown is-hoverable" > <div class="navbar-item has-dropdown is-hoverable" >
<a class="navbar-link" href="#" @click.prevent> <div v-if="isLoggedIn">
Dan <a class="navbar-link" href="#" @click.prevent>
</a> {{ user.name }}
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="#">
Profile
</a>
<a class="navbar-item" href="#">
Logout
</a> </a>
<div class="navbar-dropdown is-boxed">
<a class="navbar-item" href="#">
Profile
</a>
<a class="navbar-item" href="#">
Logout
</a>
</div>
</div> </div>
<div v-else>
<user-login></user-login>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -43,11 +49,16 @@
<script> <script>
import UserLogin from "./UserLogin";
export default { export default {
data() { data() {
return { return {
menuActive: false menuActive: false
}; };
},
components: {
UserLogin
} }
} }

View File

@ -0,0 +1,99 @@
<template>
<div>
<button class="button" type="button" @click="showLogin = true">
Login
</button>
<app-modal title="Login" :open="showLogin" @dismiss="showLogin = false">
<div>
<form @submit.prevent="login">
<div v-if="error" class="notification is-danger">
{{error}}
</div>
<div class="field">
<label class="label">Username</label>
<div class="control has-icons-left">
<input class="input" type="text" placeholder="username" v-model="username">
<app-icon icon="person" size="sm" class="is-left"></app-icon>
</div>
</div>
<div class="field">
<label class="label">Password</label>
<div class="control has-icons-left">
<input class="input" type="password" placeholder="password" v-model="password">
<app-icon icon="lock-locked" size="sm" class="is-left"></app-icon>
</div>
</div>
<div class="field is-grouped">
<div class="control">
<button type="submit" class="button is-primary" :disabled="!enableSubmit">Login</button>
</div>
<div class="control">
<button class="button is-secondary" @click="showLogin = false">Cancel</button>
</div>
</div>
</form>
</div>
</app-modal>
</div>
</template>
<script>
import AppModal from "./AppModal";
import AppIcon from "./AppIcon";
import api from "../lib/Api";
import { mapMutations } from "vuex";
export default {
data() {
return {
showLogin: false,
error: '',
username: '',
password: ''
};
},
computed: {
enableSubmit() {
return this.username !== '' && this.password !== '' && !this.isLoading;
}
},
methods: {
...mapMutations([
'setUser'
]),
login() {
if (this.username !== '' && this.password != '') {
this.loadResource(api.postLogin(this.username, this.password), data => {
if (data.success) {
this.setUser(data.user);
this.showLogin = false;
} else {
this.error = data.message;
}
});
}
}
},
components: {
AppIcon,
AppModal
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -33,7 +33,8 @@ class Api {
const opts = { const opts = {
headers, headers,
method: method method: method,
credentials: "same-origin"
}; };
if (hasBody) { if (hasBody) {
@ -66,6 +67,10 @@ class Api {
return this.performRequest(url, "GET"); return this.performRequest(url, "GET");
} }
post(url, params = {}) {
return this.performRequest(url, "POST", params);
}
getRecipeList(page, per, sortColumn, sortDirection, name, tags) { getRecipeList(page, per, sortColumn, sortDirection, name, tags) {
const params = { const params = {
page: page || null, page: page || null,
@ -78,6 +83,19 @@ class Api {
return this.get("/recipes", params); return this.get("/recipes", params);
} }
postLogin(username, password) {
const params = {
username: username,
password: password
};
return this.post("/login", params);
}
getCurrentUser() {
return this.get("/user")
}
} }
const api = new Api(); const api = new Api();

View File

@ -1,12 +1,16 @@
import Vue from 'vue'; import Vue from 'vue';
import { mapMutations, mapState } from 'vuex'; import { mapGetters, mapMutations, mapState } from 'vuex';
Vue.mixin({ Vue.mixin({
computed: { computed: {
...mapState({ ...mapGetters([
isLoading: state => state.loading "isLoading",
}) "isLoggedIn"
]),
...mapState([
"user"
])
}, },
methods: { methods: {
...mapMutations([ ...mapMutations([

View File

@ -7,10 +7,17 @@ export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
state: { state: {
loading: false, loading: false,
error: null error: null,
authChecked: false,
user: null
}, },
getters: { getters: {
isLoading(state) {
return state.loading === true;
},
isLoggedIn(state) {
return state.user !== null;
}
}, },
mutations: { mutations: {
setLoading(state, value) { setLoading(state, value) {
@ -19,6 +26,11 @@ export default new Vuex.Store({
setError(state, value) { setError(state, value) {
state.error = value; state.error = value;
},
setUser(state, user) {
state.authChecked = true;
state.user = user;
} }
}, },
actions: { actions: {

View File

@ -3,6 +3,7 @@
@import "~bulma/sass/utilities/_all"; @import "~bulma/sass/utilities/_all";
@import "~bulma/sass/base/_all"; @import "~bulma/sass/base/_all";
@import "~bulma/sass/components/navbar"; @import "~bulma/sass/components/navbar";
@import "~bulma/sass/components/modal";
@import "~bulma/sass/elements/_all"; @import "~bulma/sass/elements/_all";
@import "~bulma/sass/grid/columns"; @import "~bulma/sass/grid/columns";
@import "~bulma/sass/layout/section"; @import "~bulma/sass/layout/section";

View File

@ -0,0 +1,2 @@
json.ex

View File

@ -35,6 +35,7 @@ Rails.application.routes.draw do
get '/login' => 'users#login', as: :login get '/login' => 'users#login', as: :login
post '/login' => 'users#verify_login' post '/login' => 'users#verify_login'
get '/logout' => 'users#logout', as: :logout get '/logout' => 'users#logout', as: :logout
get '/user' => 'users#show', as: :show
get '/about' => 'home#about', as: :about get '/about' => 'home#about', as: :about

View File

@ -1,5 +1,11 @@
const { environment } = require('@rails/webpacker'); const { environment } = require('@rails/webpacker');
const vue = require('./loaders/vue'); const vue = require('./loaders/vue');
const svg = require('./loaders/svg');
environment.loaders.append('vue', vue);
environment.loaders.append('svg', svg);
const fileLoader = environment.loaders.get('file');
fileLoader.exclude = /\.(svg)$/i;
environment.loaders.append('vue', vue)
module.exports = environment; module.exports = environment;

View File

@ -0,0 +1,7 @@
module.exports = {
test: /\.svg$/,
use: [{
loader: 'svg-loader'
}]
};

View File

@ -4,6 +4,8 @@
"bulma": "^0.6.2", "bulma": "^0.6.2",
"caniuse-lite": "^1.0.30000815", "caniuse-lite": "^1.0.30000815",
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"open-iconic": "^1.1.1",
"svg-loader": "^0.0.2",
"vue": "^2.5.16", "vue": "^2.5.16",
"vue-loader": "^14.2.2", "vue-loader": "^14.2.2",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",

View File

@ -1,5 +1,5 @@
Arguments: Arguments:
/home/dan/.nvm/versions/node/v8.7.0/bin/node /home/dan/.nvm/versions/node/v8.7.0/bin/yarn install /home/dan/.nvm/versions/node/v8.7.0/bin/node /home/dan/.nvm/versions/node/v8.7.0/bin/yarn add svg-router
PATH: PATH:
/home/dan/.rvm/gems/ruby-2.5.1/bin:/home/dan/.rvm/gems/ruby-2.5.1@global/bin:/home/dan/.rvm/rubies/ruby-2.5.1/bin:/home/dan/.rvm/bin:/home/dan/.cargo/bin:/home/dan/.nvm/versions/node/v8.7.0/bin:/home/dan/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games /home/dan/.rvm/gems/ruby-2.5.1/bin:/home/dan/.rvm/gems/ruby-2.5.1@global/bin:/home/dan/.rvm/rubies/ruby-2.5.1/bin:/home/dan/.rvm/bin:/home/dan/.cargo/bin:/home/dan/.nvm/versions/node/v8.7.0/bin:/home/dan/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
@ -17,6 +17,7 @@ npm manifest:
{ {
"dependencies": { "dependencies": {
"@rails/webpacker": "^3.4.1", "@rails/webpacker": "^3.4.1",
"bulma": "^0.6.2",
"caniuse-lite": "^1.0.30000815", "caniuse-lite": "^1.0.30000815",
"css-loader": "^0.28.11", "css-loader": "^0.28.11",
"vue": "^2.5.16", "vue": "^2.5.16",
@ -24,7 +25,7 @@ npm manifest:
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.16", "vue-template-compiler": "^2.5.16",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"webpack": "^3.11.0", "webpack": "^3.11.0"
}, },
"devDependencies": { "devDependencies": {
"webpack-dev-server": "^2.11.2" "webpack-dev-server": "^2.11.2"
@ -1065,6 +1066,10 @@ Lockfile:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
bulma@^0.6.2:
version "0.6.2"
resolved "https://registry.yarnpkg.com/bulma/-/bulma-0.6.2.tgz#f4b1d11d5acc51a79644eb0a2b0b10649d3d71f5"
bytes@3.0.0: bytes@3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
@ -2663,7 +2668,7 @@ Lockfile:
version "1.2.7" version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
http-errors@1.6.2, http-errors@~1.6.2: http-errors@1.6.2:
version "1.6.2" version "1.6.2"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736"
dependencies: dependencies:
@ -2672,6 +2677,15 @@ Lockfile:
setprototypeof "1.0.3" setprototypeof "1.0.3"
statuses ">= 1.3.1 < 2" statuses ">= 1.3.1 < 2"
http-errors@~1.6.2:
version "1.6.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
http-parser-js@>=0.4.0: http-parser-js@>=0.4.0:
version "0.4.11" version "0.4.11"
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.11.tgz#5b720849c650903c27e521633d94696ee95f3529" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.11.tgz#5b720849c650903c27e521633d94696ee95f3529"
@ -5398,7 +5412,7 @@ Lockfile:
define-property "^0.2.5" define-property "^0.2.5"
object-copy "^0.1.0" object-copy "^0.1.0"
"statuses@>= 1.3.1 < 2": "statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
@ -5989,7 +6003,7 @@ Lockfile:
source-list-map "^2.0.0" source-list-map "^2.0.0"
source-map "~0.6.1" source-map "~0.6.1"
webpack@^3.10.0: webpack@^3.10.0, webpack@^3.11.0:
version "3.11.0" version "3.11.0"
resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894" resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.11.0.tgz#77da451b1d7b4b117adaf41a1a93b5742f24d894"
dependencies: dependencies:
@ -6174,10 +6188,11 @@ Lockfile:
window-size "0.1.0" window-size "0.1.0"
Trace: Trace:
SyntaxError: /home/dan/Development/parsley/package.json: Unexpected token } in JSON at position 292 Error: Couldn't find package "svg-router" on the "npm" registry.
at JSON.parse (<anonymous>) at new MessageError (/home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:186:110)
at /home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:1036:59 at NpmResolver.<anonymous> (/home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:51625:15)
at Generator.next (<anonymous>) at Generator.next (<anonymous>)
at step (/home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:98:30) at step (/home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:98:30)
at /home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:109:13 at /home/dan/.nvm/versions/node/v8.7.0/lib/node_modules/yarn/lib/cli.js:109:13
at <anonymous> at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)

View File

@ -3779,6 +3779,10 @@ onecolor@^3.0.4:
version "3.0.5" version "3.0.5"
resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.5.tgz#36eff32201379efdf1180fb445e51a8e2425f9f6" resolved "https://registry.yarnpkg.com/onecolor/-/onecolor-3.0.5.tgz#36eff32201379efdf1180fb445e51a8e2425f9f6"
open-iconic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/open-iconic/-/open-iconic-1.1.1.tgz#9dcfc8c7cd3c61cdb4a236b1a347894c97adc0c6"
opn@^5.1.0: opn@^5.1.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c" resolved "https://registry.yarnpkg.com/opn/-/opn-5.3.0.tgz#64871565c863875f052cfdf53d3e3cb5adb53b1c"
@ -5516,6 +5520,10 @@ supports-color@^5.1.0, supports-color@^5.3.0:
dependencies: dependencies:
has-flag "^3.0.0" has-flag "^3.0.0"
svg-loader@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/svg-loader/-/svg-loader-0.0.2.tgz#601ab2fdaa1dadae3ca9975b550de92a07e1d92b"
svgo@^0.7.0: svgo@^0.7.0:
version "0.7.2" version "0.7.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5" resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"