checkin form fix, user/token route for auth

pull/773/head
hunterlong 2020-08-03 17:31:42 -07:00
parent aeaba54f82
commit 79b6e620bf
19 changed files with 302 additions and 97 deletions

View File

@ -7,6 +7,7 @@
- Modified SCSS/SASS files to be generated from 1, main.scss to main.css
- Modified index page to use /assets directory for assets, (main.css, style.css)
- Modified index page to use CDN asset paths
- Fixed New Checkin form
# 0.90.61 (07-22-2020)
- Modified sass layouts, organized and split up sections

131
dev/postman.json vendored
View File

@ -3275,6 +3275,8 @@
"pm.test(\"Check Login JWT Token\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData).to.have.property('token');",
" pm.expect(jsonData).to.have.property('admin');",
" pm.globals.set(\"token\", jsonData.token);",
"});"
],
"type": "text/javascript"
@ -3371,24 +3373,141 @@
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Length",
"value": "174"
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Set-Cookie",
"value": "statping_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInNjb3BlcyI6ImFkbWluIiwiZXhwIjoxNTk2NzQzMDUzfQ.dQQGgUDhFEjCL2Gi-Seg0hBp_sqVsDn3cXB0GpSorJI; Path=/; Expires=Thu, 06 Aug 2020 19:44:13 GMT; Max-Age=259200"
},
{
"key": "Date",
"value": "Mon, 03 Aug 2020 19:44:13 GMT"
},
{
"key": "Content-Length",
"value": "197"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsInNjb3BlcyI6ImFkbWluIiwiZXhwIjoxNTk2NzQzMDUzfQ.dQQGgUDhFEjCL2Gi-Seg0hBp_sqVsDn3cXB0GpSorJI\",\n \"admin\": true\n}"
}
]
},
{
"name": "Check User Token",
"event": [
{
"listen": "test",
"script": {
"id": "560e439b-d588-4a2f-a8a6-a0607531d74c",
"exec": [
"pm.test(\"Response is ok\", function () {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test(\"View Token Response\", function () {",
" var jsonData = pm.response.json();",
" pm.expect(jsonData.username).to.eql(\"admin\");",
" pm.expect(jsonData.admin).to.eql(true);",
" pm.expect(jsonData.scopes).to.eql(\"admin\");",
"});"
],
"type": "text/javascript"
}
}
],
"request": {
"auth": {
"type": "bearer",
"bearer": [
{
"key": "token",
"value": "{{api_key}}",
"type": "string"
}
]
},
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "token",
"value": "{{token}}",
"type": "text"
}
]
},
"url": {
"raw": "{{endpoint}}/api/users/token",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"users",
"token"
]
},
"description": "Send your JWT token from login to this endpoint to return the JSON values."
},
"response": [
{
"name": "Check User Token",
"originalRequest": {
"method": "POST",
"header": [],
"body": {
"mode": "urlencoded",
"urlencoded": [
{
"key": "token",
"value": "{{token}}",
"type": "text"
}
]
},
"url": {
"raw": "{{endpoint}}/api/users/token",
"host": [
"{{endpoint}}"
],
"path": [
"api",
"users",
"token"
]
}
},
"status": "OK",
"code": 200,
"_postman_previewlanguage": "json",
"header": [
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "Date",
"value": "Sat, 02 May 2020 00:56:17 GMT"
"value": "Mon, 03 Aug 2020 19:47:23 GMT"
},
{
"key": "Set-Cookie",
"value": "statping_auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImV4cCI6MTU4ODY0MDE3N30.tf399_LfAphSGlKMtgphg6qpPrn-_w92XfCrK5FwbZY; Expires=Tue, 05 May 2020 00:56:17 GMT"
"key": "Content-Length",
"value": "68"
},
{
"key": "Connection",
"value": "close"
}
],
"cookie": [],
"body": "{\n \"token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiYWRtaW4iOnRydWUsImV4cCI6MTU4ODY0MDE3N30.tf399_LfAphSGlKMtgphg6qpPrn-_w92XfCrK5FwbZY\",\n \"admin\": true\n}"
"body": "{\n \"username\": \"admin\",\n \"admin\": true,\n \"scopes\": \"admin\",\n \"exp\": 1596743053\n}"
}
]
},

View File

@ -235,6 +235,11 @@ class Api {
return axios.post('api/theme', data).then(response => (response.data))
}
async check_token(token) {
const f = {token: token}
return axios.post('api/users/token', qs.stringify(f)).then(response => (response.data))
}
async login(username, password) {
const f = {username: username, password: password}
return axios.post('api/login', qs.stringify(f)).then(response => (response.data))

View File

@ -15,7 +15,7 @@ A:HOVER {
}
.text-muted {
color: darken($text-color, 30%) !important;
color: lighten($text-color, 30%) !important;
}
.day-success {

View File

@ -15,7 +15,7 @@ $navbar-background: #ffffff;
$input-background: #fdfdfd;
$input-color: #4e4e4e;
$input-border: 1px solid #c9c9c9;
$day-success-background: #18ce08;
$day-success-background: #20ac13;
$day-error-background: #d50a0a;
/* Status Container */

View File

@ -9,7 +9,6 @@
<button @click="deleteCheckin(checkin)" class="btn btn-sm small btn-danger float-right text-uppercase">Delete</button>
</div>
<div class="card-body">
<div class="input-group">
<input type="text" class="form-control" :value="`${core.domain}/checkin/${checkin.api_key}`" readonly>
<div class="input-group-append copy-btn">
@ -137,6 +136,9 @@ export default {
},
last_record(checkin) {
const r = this.records(checkin)
if (r.length === 0) {
return {success: false}
}
return r[0]
},
fixInts() {

View File

@ -24,10 +24,6 @@
<label for="checkin_interval" class="col-form-label">Interval (minutes)</label>
<input v-model="checkin.interval" type="number" name="interval" class="form-control" id="checkin_interval" placeholder="1" min="1">
</div>
<div class="col-12 col-md-5">
<label for="grace_period" class="col-form-label">Grace Period</label>
<input v-model="checkin.grace" type="number" name="grace" class="form-control" id="grace_period" placeholder="10">
</div>
<div class="col-12 col-md-5">
<label class="col-form-label"></label>
<button @click.prevent="saveCheckin" type="submit" id="submit" class="btn btn-success d-block mt-2">Save Checkin</button>
@ -54,7 +50,6 @@
checkin: {
name: "",
interval: 60,
grace: 60,
service_id: this.service.id
}
}

View File

@ -5,11 +5,9 @@
No updates found, create a new Incident Update below.
</div>
<transition-group name="fade" tag="div">
<div v-for="update in updates.reverse()" :key="update.id">
<IncidentUpdate :update="update" :onUpdate="loadUpdates" :admin="true"/>
</div>
</transition-group>
<div v-for="update in updates.reverse()" :key="update.id">
<IncidentUpdate :update="update" :onUpdate="loadUpdates" :admin="true"/>
</div>
<form class="row" @submit.prevent="createIncidentUpdate">
<div class="col-12 col-md-3 mb-3 mb-md-0">
@ -51,7 +49,7 @@
},
data () {
return {
updates: [],
updates: null,
incident_update: {
incident: this.incident.id,
message: "",

View File

@ -4,13 +4,13 @@
<div class="form-group row">
<label for="username" class="col-sm-2 col-form-label">{{$t('username')}}</label>
<div class="col-sm-10">
<input @keyup="checkForm" type="text" v-model="username" name="username" class="form-control" id="username" placeholder="Username" autocorrect="off" autocapitalize="none">
<input @keyup="checkForm" type="text" v-model="username" autocomplete="username" name="username" class="form-control" id="username" placeholder="Username" autocorrect="off" autocapitalize="none">
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-2 col-form-label">{{$t('password')}}</label>
<div class="col-sm-10">
<input @keyup="checkForm" type="password" v-model="password" name="password" class="form-control" id="password" placeholder="Password">
<input @keyup="checkForm" type="password" v-model="password" autocomplete="current-password" name="password" class="form-control" id="password" placeholder="Password">
</div>
</div>
<div class="form-group row">
@ -46,6 +46,7 @@
<script>
import Api from "../API";
import store from "@/store";
export default {
name: 'FormLogin',
@ -59,17 +60,20 @@
},
data() {
return {
username: "",
password: "",
auth: {},
loading: false,
error: false,
disabled: true,
username: "",
password: "",
auth: {},
loading: false,
error: false,
disabled: true,
google_scope: "https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email",
slack_scope: "identity.email,identity.basic"
}
},
methods: {
mounted() {
this.$cookies.remove("statping_auth")
},
methods: {
checkForm() {
if (!this.username || !this.password) {
this.disabled = true
@ -84,9 +88,10 @@
if (auth.error) {
this.error = true
} else if (auth.token) {
// this.$cookies.set("statping_auth", auth.token)
this.$cookies.set("statping_auth", auth.token)
await this.$store.dispatch('loadAdmin')
this.$store.commit('setAdmin', auth.admin)
this.$store.commit('setLoggedIn', true)
this.$router.push('/dashboard')
}
this.loading = false

View File

@ -14,10 +14,10 @@
<div v-if="notifier.method==='mobile'">
<div class="form-group row mt-3">
<label for="domain" class="col-sm-4 col-form-label">Statping Domain</label>
<label for="statping_domain" class="col-sm-4 col-form-label">Statping Domain</label>
<div class="col-sm-8">
<div class="input-group">
<input v-bind:value="$store.getters.core.domain" type="text" class="form-control" id="domain" readonly>
<input v-bind:value="$store.getters.core.domain" type="text" class="form-control" id="statping_domain" readonly>
<div class="input-group-append copy-btn">
<button @click.prevent="copy($store.getters.core.domain)" class="btn btn-outline-secondary" type="button">Copy</button>
</div>

View File

@ -106,10 +106,6 @@ export default Vue.mixin({
isAdmin() {
return this.$store.state.admin
},
loggedIn() {
const core = this.$store.getters.core
return core.logged_in === true
},
iconName(name) {
switch (name) {
case "fas fa-terminal":

View File

@ -32,6 +32,9 @@
</template>
<script>
import Api from "@/API";
import store from "@/store";
const Group = () => import('@/components/Index/Group')
const Header = () => import('@/components/Index/Header')
const MessageBlock = () => import('@/components/Index/MessageBlock')
@ -44,10 +47,10 @@ export default {
components: {
IncidentsBlock,
GroupServiceFailures,
ServiceBlock,
MessageBlock,
Group,
Header
ServiceBlock,
MessageBlock,
Group,
Header
},
data() {
return {
@ -67,14 +70,24 @@ export default {
services_no_group() {
return this.$store.getters.servicesNoGroup
}
},
async created() {
this.logged_in = this.loggedIn()
},
async mounted() {
},
methods: {
async checkLogin() {
const token = this.$cookies.get('statping_auth')
if (!token) {
this.$store.commit('setLoggedIn', false)
return
}
try {
const jwt = await Api.check_token(token)
this.$store.commit('setAdmin', jwt.admin)
if (jwt.username) {
this.$store.commit('setLoggedIn', true)
}
} catch (e) {
console.error(e)
}
},
inRange(message) {
return this.isBetween(this.now(), message.start_on, message.start_on === message.end_on ? this.maxDate().toISOString() : message.end_on)
}

View File

@ -22,7 +22,10 @@
}
},
methods: {
mounted() {
},
methods: {
}
}

View File

@ -17,6 +17,7 @@ const NotFound = () => import('@/pages/NotFound')
import VueRouter from "vue-router";
import Api from "./API";
import store from "./store"
const Loading = {
template: '<div class="jumbotron">LOADING</div>'
@ -26,7 +27,10 @@ const routes = [
{
path: '/setup',
name: 'Setup',
component: Setup
component: Setup,
meta: {
title: 'Statping Setup',
}
},
{
path: '/',
@ -37,14 +41,37 @@ const routes = [
path: '/dashboard',
component: Dashboard,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Dashboard',
},
beforeEnter: async (to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
let tk = await Api.token()
if (to.path !== '/login' && !tk) {
next('/login')
return
if (to.path !== '/login') {
if(store.getters.loggedIn) {
next()
return
}
const token = $cookies.get('statping_auth')
if (!token) {
next('/login')
return
}
try {
const jwt = await Api.check_token(token)
store.commit('setAdmin', jwt.admin)
if (jwt.admin) {
store.commit('setLoggedIn', true)
store.commit('setUser', true)
} else {
store.commit('setLoggedIn', false)
next('/login')
return
}
} catch (e) {
console.error(e)
next('/login')
return
}
}
next()
} else {
@ -55,81 +82,96 @@ const routes = [
path: '',
component: DashboardIndex,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Dashboard',
}
},{
path: 'users',
component: DashboardUsers,
loading: Loading,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Users',
}
},{
path: 'services',
component: DashboardServices,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Services',
}
},{
path: 'create_service',
component: EditService,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Create Service',
}
},{
path: 'edit_service/:id',
component: EditService,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Edit Service',
}
},{
path: 'service/:id/incidents',
component: Incidents,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Incidents',
}
},{
path: 'service/:id/checkins',
component: Checkins,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Checkins',
}
},{
path: 'service/:id/failures',
component: Failures,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Service Failures',
}
},{
path: 'messages',
component: DashboardMessages,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Messages',
}
},{
path: 'settings',
component: Settings,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Settings',
}
},{
path: 'logs',
component: Logs,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Logs',
}
},{
path: 'help',
component: Logs,
meta: {
requiresAuth: true
requiresAuth: true,
title: 'Statping - Help',
}
}]
},
{
path: '/login',
name: 'Login',
component: Login
component: Login,
meta: {
title: 'Statping - Login',
}
},
{ path: '/logout', redirect: '/' },
{
@ -157,23 +199,23 @@ const router = new VueRouter({
routes
})
let CheckAuth = (to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
let item = this.$cookies.get("statping_auth")
window.console.log(item)
if (to.path !== '/login' && !item) {
next('/login')
return
}
const auth = JSON.parse(item)
if (!auth.token) {
next('/login')
return
}
next()
} else {
next()
}
}
router.beforeEach((to, from, next) => {
const nearestWithTitle = to.matched.slice().reverse().find(r => r.meta && r.meta.title);
const nearestWithMeta = to.matched.slice().reverse().find(r => r.meta && r.meta.metaTags);
const previousNearestWithMeta = from.matched.slice().reverse().find(r => r.meta && r.meta.metaTags);
if(nearestWithTitle) document.title = nearestWithTitle.meta.title;
Array.from(document.querySelectorAll('[data-vue-router-controlled]')).map(el => el.parentNode.removeChild(el));
if(!nearestWithMeta) return next();
nearestWithMeta.meta.metaTags.map(tagDef => {
const tag = document.createElement('meta');
Object.keys(tagDef).forEach(key => {
tag.setAttribute(key, tagDef[key]);
});
tag.setAttribute('data-vue-router-controlled', '');
return tag;
})
.forEach(tag => document.head.appendChild(tag));
next();
});
export default router

View File

@ -30,7 +30,8 @@ export default new Vuex.Store({
notifiers: [],
checkins: [],
admin: false,
user: false
user: false,
loggedIn: false
},
getters: {
hasAllData: state => state.hasAllData,
@ -46,6 +47,7 @@ export default new Vuex.Store({
users: state => state.users,
notifiers: state => state.notifiers,
checkins: state => state.checkins,
loggedIn: state => state.loggedIn,
isAdmin: state => state.admin,
isUser: state => state.user,
@ -131,6 +133,9 @@ export default new Vuex.Store({
setAdmin (state, admin) {
state.admin = admin
},
setLoggedIn (state, loggedIn) {
state.loggedIn = loggedIn
},
setUser (state, user) {
state.user = user
},

View File

@ -51,17 +51,9 @@ func setJwtToken(user *users.User, w http.ResponseWriter) (JwtClaim, string) {
return jwtClaim, tokenString
}
func getJwtToken(r *http.Request) (JwtClaim, error) {
c, err := r.Cookie(cookieName)
if err != nil {
if err == http.ErrNoCookie {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
func parseToken(token string) (JwtClaim, error) {
var claims JwtClaim
tkn, err := jwt.ParseWithClaims(c.Value, &claims, func(token *jwt.Token) (interface{}, error) {
tkn, err := jwt.ParseWithClaims(token, &claims, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
@ -74,5 +66,16 @@ func getJwtToken(r *http.Request) (JwtClaim, error) {
if !tkn.Valid {
return claims, errors.New("token is not valid")
}
return claims, err
return claims, nil
}
func getJwtToken(r *http.Request) (JwtClaim, error) {
c, err := r.Cookie(cookieName)
if err != nil {
if err == http.ErrNoCookie {
return JwtClaim{}, err
}
return JwtClaim{}, err
}
return parseToken(c.Value)
}

View File

@ -154,6 +154,7 @@ func Router() *mux.Router {
// API USER Routes
api.Handle("/api/users", authenticated(apiAllUsersHandler, false)).Methods("GET")
api.Handle("/api/users", authenticated(apiCreateUsersHandler, false)).Methods("POST")
api.Handle("/api/users/token", http.HandlerFunc(apiCheckUserTokenHandler)).Methods("POST")
api.Handle("/api/users/{id}", authenticated(apiUserHandler, false)).Methods("GET")
api.Handle("/api/users/{id}", authenticated(apiUserUpdateHandler, false)).Methods("POST")
api.Handle("/api/users/{id}", authenticated(apiUserDeleteHandler, false)).Methods("DELETE")

View File

@ -80,6 +80,23 @@ func apiAllUsersHandler(w http.ResponseWriter, r *http.Request) {
returnJson(allUsers, w, r)
}
func apiCheckUserTokenHandler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
token := r.PostForm.Get("token")
if token == "" {
sendErrorJson(errors.New("missing token parameter"), w, r)
return
}
claim, err := parseToken(token)
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(claim, w, r)
}
func apiCreateUsersHandler(w http.ResponseWriter, r *http.Request) {
var user *users.User
err := DecodeJSON(r, &user)

View File

@ -19,11 +19,11 @@ var (
)
func TestMobileNotifier(t *testing.T) {
t.SkipNow()
err := utils.InitLogs()
require.Nil(t, err)
t.Parallel()
t.SkipNow()
mobileToken = utils.Params.GetString("MOBILE_TOKEN")
if mobileToken == "" {