Rework login page to be AJAX/Vue.js based
This commit is contained in:
125
app/assets/app/auth/login/Form.component.js
Normal file
125
app/assets/app/auth/login/Form.component.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { Component } from '../../../lib/vues6/vues6.js'
|
||||
import { auth_api } from '../../service/AuthApi.service.js'
|
||||
import { location_service } from '../../service/Location.service.js'
|
||||
|
||||
const template = `
|
||||
<div class="coreid-login-form col-lg-6 col-md-8 col-sm-10 col-xs-12 offset-lg-3 offset-md-2 offset-sm-1 offset-xs-0 text-left">
|
||||
<div class="coreid-login-form-inner">
|
||||
<div class="coreid-login-form-header font-weight-light">{{ app_name }}</div>
|
||||
<div class="coreid-login-form-message">{{ login_message }}</div>
|
||||
<form class="coreid-form" v-on:submit.prevent="do_nothing">
|
||||
<div class="form-group">
|
||||
<input
|
||||
type="text"
|
||||
id="coreid-login-form-username"
|
||||
name="username"
|
||||
class="form-control"
|
||||
placeholder="Username"
|
||||
v-model="username"
|
||||
autofocus
|
||||
@keyup="on_key_up"
|
||||
:disabled="loading || step_two"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group" v-if="step_two">
|
||||
<input
|
||||
type="password"
|
||||
id="coreid-login-form-password"
|
||||
name="password"
|
||||
class="form-control"
|
||||
placeholder="Password"
|
||||
v-model="password"
|
||||
:disabled="loading"
|
||||
@keyup="on_key_up"
|
||||
ref="password_input"
|
||||
>
|
||||
</div>
|
||||
<div v-if="error_message" class="error-message">{{ error_message }}</div>
|
||||
<div v-if="other_message" class="other-message">{{ other_message }}</div>
|
||||
<div class="buttons text-right">
|
||||
<button type="button" class="btn btn-primary" :disabled="loading" v-if="step_two" v-on:click="back_click">Back</button>
|
||||
<button type="button" class="btn btn-primary" :disabled="loading || btn_disabled" v-on:click="step_click">{{ button_text }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="coreid-loading-spinner" v-if="loading"><div class="inner"></div></div>
|
||||
</div>
|
||||
`
|
||||
|
||||
export default class AuthLoginForm extends Component {
|
||||
static get selector() { return 'coreid-login-form' }
|
||||
static get props() { return ['app_name', 'login_message'] }
|
||||
static get template() { return template }
|
||||
|
||||
username = ''
|
||||
password = ''
|
||||
button_text = 'Next'
|
||||
step_two = false
|
||||
btn_disabled = true
|
||||
loading = false
|
||||
error_message = ''
|
||||
other_message = ''
|
||||
|
||||
watch_username(new_username, old_username) {
|
||||
this.btn_disabled = !new_username
|
||||
}
|
||||
|
||||
back_click() {
|
||||
this.step_two = false
|
||||
this.button_text = 'Next'
|
||||
}
|
||||
|
||||
async on_key_up(event) {
|
||||
if ( event.keyCode === 13 ) {
|
||||
// Enter was pressed
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if ( !this.step_two && this.username ) return this.step_click(event)
|
||||
else if ( this.step_two && this.username && this.password ) return this.step_click(event)
|
||||
}
|
||||
}
|
||||
|
||||
async step_click(event) {
|
||||
if ( !this.step_two ) {
|
||||
this.loading = true
|
||||
try {
|
||||
const is_valid = await auth_api.validate_username(this.username)
|
||||
if ( !is_valid ) {
|
||||
this.error_message = 'That username is invalid. Please try again.'
|
||||
} else {
|
||||
this.step_two = true
|
||||
this.button_text = 'Continue'
|
||||
this.error_message = ''
|
||||
this.$nextTick(() => {
|
||||
this.$refs.password_input.focus()
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.error_message = 'Sorry, an unknown error has occurred and we are unable to continue at this time.'
|
||||
}
|
||||
this.loading = false
|
||||
} else {
|
||||
this.loading = true
|
||||
this.error_message = ''
|
||||
|
||||
const result = await auth_api.attempt({
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
create_session: true, // TODO support this being passed in
|
||||
})
|
||||
|
||||
if ( !result.success ) {
|
||||
this.error_message = result.message || 'Sorry, an unknown error has occurred and we are unable to continue at this time.'
|
||||
this.loading = false
|
||||
return
|
||||
}
|
||||
|
||||
// TODO handle 2FA
|
||||
|
||||
this.other_message = 'Success! Let\'s get you on your way...'
|
||||
await location_service.redirect(result.next, 1500)
|
||||
}
|
||||
}
|
||||
|
||||
do_nothing() {}
|
||||
}
|
||||
7
app/assets/app/components.js
Normal file
7
app/assets/app/components.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import AuthLoginForm from "./auth/login/Form.component.js"
|
||||
|
||||
const components = {
|
||||
AuthLoginForm
|
||||
}
|
||||
|
||||
export { components }
|
||||
23
app/assets/app/service/AuthApi.service.js
Normal file
23
app/assets/app/service/AuthApi.service.js
Normal file
@@ -0,0 +1,23 @@
|
||||
class AuthAPI {
|
||||
async validate_username(username) {
|
||||
const result = await axios.post('/api/v1/auth/validate/username', { username })
|
||||
return result && result.data && result.data.data && result.data.data.is_valid
|
||||
}
|
||||
|
||||
async attempt({ username, password, create_session }) {
|
||||
try {
|
||||
const result = await axios.post('/api/v1/auth/attempt', {
|
||||
username, password, create_session
|
||||
})
|
||||
|
||||
if ( result && result.data && result.data.data && result.data.data ) {
|
||||
return result.data.data
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return { success: false }
|
||||
}
|
||||
}
|
||||
|
||||
const auth_api = new AuthAPI()
|
||||
export { auth_api }
|
||||
13
app/assets/app/service/Location.service.js
Normal file
13
app/assets/app/service/Location.service.js
Normal file
@@ -0,0 +1,13 @@
|
||||
class LocationService {
|
||||
async redirect(to, delay = 0) {
|
||||
return new Promise(res => {
|
||||
setTimeout(() => {
|
||||
window.location = to
|
||||
res()
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const location_service = new LocationService()
|
||||
export { location_service }
|
||||
182
app/assets/less/form.less
Normal file
182
app/assets/less/form.less
Normal file
@@ -0,0 +1,182 @@
|
||||
@keyframes loading-bar {
|
||||
0% {
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
5% {
|
||||
left: 0;
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
10% {
|
||||
left: 0;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
15% {
|
||||
left: 0;
|
||||
width: 37.5%;
|
||||
}
|
||||
|
||||
20% {
|
||||
left: 0;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
25% {
|
||||
left: 0;
|
||||
width: 62.5%;
|
||||
}
|
||||
|
||||
30% {
|
||||
left: 0;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
35% {
|
||||
left: 0;
|
||||
width: 87.5%;
|
||||
}
|
||||
|
||||
40% {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
50% {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
55% {
|
||||
left: 12.5%;
|
||||
width: 87.5%;
|
||||
}
|
||||
|
||||
60% {
|
||||
left: 25%;
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
65% {
|
||||
left: 37.5%;
|
||||
width: 62.5%;
|
||||
}
|
||||
|
||||
70% {
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
75% {
|
||||
left: 62.5%;
|
||||
width: 37.5%;
|
||||
}
|
||||
|
||||
80% {
|
||||
left: 75%;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
85% {
|
||||
left: 87.5%;
|
||||
width: 12.5%;
|
||||
}
|
||||
|
||||
90% {
|
||||
left: 100%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: 0;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.coreid-form input {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
border-bottom: 2px solid #aaa;
|
||||
font-size: 1.2em;
|
||||
background: none;
|
||||
box-shadow: none;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
border-bottom: 2px solid #666;
|
||||
}
|
||||
}
|
||||
|
||||
.coreid-login-form {
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 7px;
|
||||
|
||||
.coreid-login-form-inner {
|
||||
padding: 30px;
|
||||
padding-top: 170px;
|
||||
padding-bottom: 160px;
|
||||
}
|
||||
|
||||
.coreid-login-form-header {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.coreid-login-form-message {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 40px;
|
||||
margin-bottom: 0;
|
||||
|
||||
.btn {
|
||||
background: #666;
|
||||
border-color: #444;
|
||||
|
||||
&:hover {
|
||||
background: #777;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background: #888;
|
||||
box-shadow: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coreid-loading-spinner {
|
||||
overflow: hidden;
|
||||
background-color: #ddd;
|
||||
height: 7px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: calc(100% + 30px);
|
||||
margin-left: -15px;
|
||||
border-radius: 0 0 5px 5px;
|
||||
|
||||
.inner {
|
||||
height: 7px;
|
||||
width: 50px;
|
||||
background-color: #bbb;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
border-radius: 0 0 5px 5px;
|
||||
animation-name: loading-bar;
|
||||
animation-duration: 1.5s;
|
||||
animation-fill-mode: both;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: darkred;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.other-message {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
}
|
||||
12
app/assets/less/public.less
Normal file
12
app/assets/less/public.less
Normal file
@@ -0,0 +1,12 @@
|
||||
.masthead {
|
||||
height: 100vh;
|
||||
min-height: 500px;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
background: #eee;
|
||||
color: #666;
|
||||
}
|
||||
9
app/assets/less/welcome.less
Normal file
9
app/assets/less/welcome.less
Normal file
@@ -0,0 +1,9 @@
|
||||
#about {
|
||||
background: #666;
|
||||
color: #eee;
|
||||
|
||||
a {
|
||||
color: white;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
3
app/assets/lib/axios/axios.min.js
vendored
Normal file
3
app/assets/lib/axios/axios.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
app/assets/lib/bootstrap/bootstrap-4.4.1.min.css
vendored
Normal file
7
app/assets/lib/bootstrap/bootstrap-4.4.1.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
app/assets/lib/bootstrap/bootstrap-4.4.1.min.js
vendored
Normal file
7
app/assets/lib/bootstrap/bootstrap-4.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
app/assets/lib/jquery/jquery-3.4.1.slim.min.js
vendored
Normal file
2
app/assets/lib/jquery/jquery-3.4.1.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
app/assets/lib/popper/popper-1.16.0.min.js
vendored
Normal file
5
app/assets/lib/popper/popper-1.16.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11965
app/assets/lib/vue/vue-2.6.11.js
Normal file
11965
app/assets/lib/vue/vue-2.6.11.js
Normal file
File diff suppressed because it is too large
Load Diff
45
app/assets/lib/vues6/vues6.js
Normal file
45
app/assets/lib/vues6/vues6.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export default class VuES6Loader {
|
||||
constructor(component_list) {
|
||||
this.components = component_list
|
||||
}
|
||||
|
||||
load() {
|
||||
for ( const ComponentClass of Object.values(this.components) ) {
|
||||
const method_properties = Object.getOwnPropertyNames(ComponentClass.prototype)
|
||||
|
||||
const watch = {}
|
||||
method_properties.filter(x => x.startsWith('watch_')).some(method_name => {
|
||||
const field_name = method_name.substr(6)
|
||||
const handler = function(...args) {
|
||||
return ComponentClass.prototype[method_name].bind(this)(...args)
|
||||
}
|
||||
watch[field_name] = handler
|
||||
})
|
||||
|
||||
const methods = {}
|
||||
method_properties.filter(x => !x.startsWith('watch_')).some(method_name => {
|
||||
const handler = function(...args) {
|
||||
return ComponentClass.prototype[method_name].bind(this)(...args)
|
||||
}
|
||||
methods[method_name] = handler
|
||||
})
|
||||
|
||||
Vue.component(ComponentClass.selector, {
|
||||
props: ComponentClass.props,
|
||||
data: () => {
|
||||
return new ComponentClass()
|
||||
},
|
||||
watch,
|
||||
methods,
|
||||
template: ComponentClass.template,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Component {
|
||||
static get selector() { return '' }
|
||||
static get template() { return '' }
|
||||
static get props() { return [] }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@ const Controller = require('libflitter/controller/Controller')
|
||||
* are used as handlers for routes specified in the route files.
|
||||
*/
|
||||
class Home extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'Vue']
|
||||
}
|
||||
|
||||
/*
|
||||
* Serve the main welcome page.
|
||||
@@ -18,7 +21,16 @@ class Home extends Controller {
|
||||
* The page() method is added by Flitter and passes some
|
||||
* helpful contextual data to the view as well.
|
||||
*/
|
||||
return res.page('welcome', {user: req.user})
|
||||
return res.page('welcome', {
|
||||
user: req.user,
|
||||
...this.Vue.data(),
|
||||
})
|
||||
}
|
||||
|
||||
async tmpl(req, res) {
|
||||
return res.page('tmpl', this.Vue.data({
|
||||
login_message: 'Please sign-in to continue.'
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
app/controllers/api/v1/Auth.controller.js
Normal file
67
app/controllers/api/v1/Auth.controller.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const { Controller } = require('libflitter')
|
||||
|
||||
class AuthController extends Controller {
|
||||
static get services() {
|
||||
return [...super.services, 'models', 'auth']
|
||||
}
|
||||
|
||||
async validate_username(req, res, next) {
|
||||
let is_valid = true
|
||||
|
||||
if ( !req.body.username ) is_valid = false
|
||||
|
||||
if ( is_valid ) {
|
||||
const User = this.models.get('auth:User')
|
||||
const user = await User.findOne({uid: req.body.username})
|
||||
if ( !user || !user.can_login ) is_valid = false
|
||||
}
|
||||
|
||||
return res.api({ is_valid })
|
||||
}
|
||||
|
||||
// TODO XSRF Token
|
||||
/*
|
||||
* Request Params:
|
||||
* - username
|
||||
* - password
|
||||
* - [create_session = false]
|
||||
*/
|
||||
async attempt(req, res, next) {
|
||||
const flitter = this.auth.get_provider('flitter')
|
||||
|
||||
const errors = await flitter.validate_login(req.body)
|
||||
if ( errors && errors.length > 0 )
|
||||
return res.status(400)
|
||||
.message(`Unable to complete authentication: one or more errors occurred`)
|
||||
.api({ errors })
|
||||
|
||||
const login_args = await flitter.get_login_args(req.body)
|
||||
const user = await flitter.login.apply(flitter, login_args)
|
||||
|
||||
if ( !user )
|
||||
return res.status(200)
|
||||
.message(`Invalid username or password.`)
|
||||
.api({
|
||||
message: `Invalid username or password.`,
|
||||
success: false,
|
||||
})
|
||||
|
||||
if ( req.body.create_session )
|
||||
await flitter.session(req, user)
|
||||
|
||||
let destination = this.configs.get('auth.default_login_route')
|
||||
if ( req?.session?.auth?.flow ) {
|
||||
destination = req.session.auth.flow
|
||||
req.session.auth.flow = false
|
||||
}
|
||||
|
||||
return res.api({
|
||||
success: true,
|
||||
session_created: !!req.body.create_session,
|
||||
next: destination,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = exports = AuthController
|
||||
@@ -6,7 +6,17 @@ const FormController = require('flitter-auth/controllers/Forms')
|
||||
* controller, however you can override them here as you need.
|
||||
*/
|
||||
class Forms extends FormController {
|
||||
static get services() {
|
||||
return [...super.services, 'Vue']
|
||||
}
|
||||
|
||||
async login_provider_get(req, res, next) {
|
||||
return res.page('auth:login', {
|
||||
...this.Vue.data({
|
||||
login_message: 'Please sign-in to continue.'
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = Forms
|
||||
|
||||
@@ -32,6 +32,11 @@ class User extends AuthUser {
|
||||
return this.find({ldap_visible: true})
|
||||
}
|
||||
|
||||
// TODO just in case we need this later
|
||||
get can_login() {
|
||||
return true
|
||||
}
|
||||
|
||||
// Prefer soft delete because of the active scope
|
||||
async delete() {
|
||||
this.active = false
|
||||
|
||||
18
app/routing/routers/api/v1/auth.routes.js
Normal file
18
app/routing/routers/api/v1/auth.routes.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const auth_routes = {
|
||||
prefix: '/api/v1/auth',
|
||||
|
||||
middleware: [
|
||||
|
||||
],
|
||||
|
||||
get: {
|
||||
|
||||
},
|
||||
|
||||
post: {
|
||||
'/validate/username': ['controller::api:v1:Auth.validate_username'],
|
||||
'/attempt': [ 'controller::api:v1:Auth.attempt' ],
|
||||
},
|
||||
}
|
||||
|
||||
module.exports = exports = auth_routes
|
||||
@@ -44,7 +44,9 @@ const index = {
|
||||
|
||||
// Placeholder for auth dashboard. You'd replace this with
|
||||
// your own route protected by 'middleware::auth:UserOnly'
|
||||
'/dash': [ 'controller::Home.welcome' ],
|
||||
'/dash': [ 'middleware::auth:UserOnly', 'controller::Home.welcome' ],
|
||||
|
||||
'/tmpl': [ 'controller::Home.tmpl' ],
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
19
app/services/Vue.service.js
Normal file
19
app/services/Vue.service.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const { Service } = require('flitter-di')
|
||||
|
||||
class VueService extends Service {
|
||||
static get services() {
|
||||
return [...super.services, 'configs']
|
||||
}
|
||||
|
||||
data(merge = {}) {
|
||||
return {
|
||||
vue_state: {
|
||||
app_name: this.configs.get('app.name'),
|
||||
app_url: this.configs.get('app.url'),
|
||||
...merge
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = exports = VueService
|
||||
@@ -1,5 +1,6 @@
|
||||
const Unit = require('libflitter/Unit')
|
||||
const LDAP = require('ldapjs')
|
||||
const Validator = require('email-validator')
|
||||
|
||||
class LDAPServerUnit extends Unit {
|
||||
static get name() {
|
||||
@@ -45,8 +46,7 @@ class LDAPServerUnit extends Unit {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
validate_email(email) {
|
||||
const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
return re.test(String(email).toLowerCase())
|
||||
return Validator.validate(email)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
extends ./form
|
||||
extends ../theme/public/base
|
||||
|
||||
block form
|
||||
.form-label-group
|
||||
input#inputUsername.form-control(type='text' name='username' value=(form_data ? form_data.username : '') required placeholder='Username' autofocus)
|
||||
label(for='inputUsername') Username
|
||||
.form-label-group
|
||||
input#inputPassword.form-control(type='password' name='password' required placeholder='Password')
|
||||
label(for='inputPassword') Password
|
||||
button.btn.btn-lg.btn-primary.btn-block.btn-login.text-uppercase.font-weight-bold.mb-2.form-submit-button(type='submit') Login
|
||||
|
||||
if registration_enabled
|
||||
.text-center
|
||||
span.small Need an account?
|
||||
a(href='./register') Register here.
|
||||
.text-center
|
||||
span.small(style="color: #999999;") Provider: #{provider_name}
|
||||
block append style
|
||||
link(rel='stylesheet' href='/style-asset/form.css')
|
||||
|
||||
block vue
|
||||
coreid-login-form(v-bind:app_name="app_name" v-bind:login_message="login_message")
|
||||
|
||||
23
app/views/theme/base.pug
Normal file
23
app/views/theme/base.pug
Normal file
@@ -0,0 +1,23 @@
|
||||
doctype html
|
||||
html(lang='en')
|
||||
head
|
||||
title #{title || (_app && _app.name) || 'CoreID'}
|
||||
|
||||
block meta
|
||||
meta(charset='utf-8')
|
||||
meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no')
|
||||
meta(name='description' content=(description || 'CoreID is a self-hosted, next-generation identity server.'))
|
||||
meta(name='author' content='Garrett Mills (garrett@glmdev.tech)')
|
||||
|
||||
block style
|
||||
link(rel='stylesheet' href='/assets/lib/bootstrap/bootstrap-4.4.1.min.css')
|
||||
body
|
||||
.app-container
|
||||
block app
|
||||
block script
|
||||
script(src='/assets/lib/axios/axios.min.js')
|
||||
script(src='/assets/lib/jquery/jquery-3.4.1.slim.min.js')
|
||||
script(src='/assets/lib/popper/popper-1.16.0.min.js')
|
||||
script(src='/assets/lib/bootstrap/bootstrap-4.4.1.min.js')
|
||||
script(src='/assets/lib/vue/vue-2.6.11.js')
|
||||
script(src='/assets/lib/vues6/vues6.js')
|
||||
27
app/views/theme/public/base.pug
Normal file
27
app/views/theme/public/base.pug
Normal file
@@ -0,0 +1,27 @@
|
||||
extends ../base
|
||||
|
||||
block append style
|
||||
link(rel='stylesheet' href='/style-asset/public.css')
|
||||
|
||||
block append script
|
||||
script(type='module').
|
||||
import { components } from '/assets/app/components.js'
|
||||
import VuES6Loader from '/assets/lib/vues6/vues6.js'
|
||||
|
||||
const loader = new VuES6Loader(components)
|
||||
loader.load()
|
||||
|
||||
const app = new Vue({
|
||||
el: '#vue-app-base',
|
||||
data: !{JSON.stringify(vue_state) || '\{\}'}
|
||||
})
|
||||
|
||||
block app
|
||||
block content
|
||||
header.masthead
|
||||
.container.h-100
|
||||
.row.h-100.align-items-center
|
||||
.col-12.text-center
|
||||
block masthead
|
||||
#vue-app-base
|
||||
block vue
|
||||
7
app/views/tmpl.pug
Normal file
7
app/views/tmpl.pug
Normal file
@@ -0,0 +1,7 @@
|
||||
extends ./theme/public/base
|
||||
|
||||
block append style
|
||||
link(rel='stylesheet' href='/style-asset/form.css')
|
||||
|
||||
block vue
|
||||
coreid-login-form(v-bind:app_name="app_name" v-bind:login_message="login_message")
|
||||
@@ -1,46 +1,19 @@
|
||||
html
|
||||
head
|
||||
title Flitter
|
||||
style(type="text/css").
|
||||
@import url('https://fonts.googleapis.com/css?family=Rajdhani');
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
background-color: #c7dbdf;
|
||||
}
|
||||
extends ./theme/public/base
|
||||
|
||||
.flitter-container {
|
||||
height: 60%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
block append style
|
||||
link(rel='stylesheet' href='/style-asset/welcome.css')
|
||||
|
||||
.flitter-image {
|
||||
height: 150px;
|
||||
}
|
||||
block masthead
|
||||
h1.font-weight-light #{_app && _app.name || 'Starship CoreID'}
|
||||
p.lead Centralized, self-hosted, modern identity services.
|
||||
|
||||
.flitter-name {
|
||||
font-family: "Rajdhani";
|
||||
font-size: 50pt;
|
||||
margin-left: 35px;
|
||||
color: #00323d;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.flitter-text {
|
||||
font-family: "Rajdhani";
|
||||
font-size: 24pt;
|
||||
color: #00323d;
|
||||
}
|
||||
body
|
||||
.flitter-container
|
||||
img.flitter-image(src="/assets/flitter.png")
|
||||
a.flitter-name(href="https://flitter.garrettmills.dev/" target="_blank") powered by flitter
|
||||
if user
|
||||
.flitter-container
|
||||
p.flitter-text Welcome, #{user.uid}! <a href="/auth/logout">Log out.</a>
|
||||
else
|
||||
.flitter-container
|
||||
p.flitter-text New to Flitter? <a href="https://flitter.garrettmills.dev/" target="_blank">Start here.</a>
|
||||
block append content
|
||||
section.py-5#about
|
||||
.container
|
||||
h2.font-weight-light What is #{_app && _app.name || 'Starship CoreID'}?
|
||||
p
|
||||
| #{_app && _app.name || 'CoreID'} is a self-hosted, open-source identity server designed for
|
||||
| people who self-host various applications. With its built-in OAuth2, LDAP, and SAML servers
|
||||
| and self-service password & admin panel, #{_app && _app.name || 'CoreID'} gives you the ability
|
||||
| to easily integrate a single-sign-on solution into your self-hosting infrastructure without
|
||||
| jumping through hoops to make it work. You can learn more <a href="#">here.</a>
|
||||
|
||||
Reference in New Issue
Block a user