Add User management
This commit is contained in:
parent
cad6e94368
commit
e5cd6e0be3
24 changed files with 721 additions and 164 deletions
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
59
assets/static/img/logo.svg
Normal file
59
assets/static/img/logo.svg
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height="119.99999"
|
||||||
|
width="540"
|
||||||
|
xml:space="preserve"
|
||||||
|
viewBox="0 0 540 119.99999"
|
||||||
|
y="0px"
|
||||||
|
x="0px"
|
||||||
|
id="Ebene_1"
|
||||||
|
version="1.1"><metadata
|
||||||
|
id="metadata22"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title>ONEOFF</dc:title></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs20" /><style
|
||||||
|
id="style2"
|
||||||
|
type="text/css">
|
||||||
|
.st0{fill:#143C78;}
|
||||||
|
</style><g
|
||||||
|
transform="matrix(4,0,0,4,-365.2,-639.99996)"
|
||||||
|
id="XMLID_14_" /><title
|
||||||
|
id="title5">ONEOFF</title><desc
|
||||||
|
id="desc7">Created with Sketch.</desc><g
|
||||||
|
transform="matrix(1.1233618,0,0,1.0968921,-102.56293,-88.409503)"
|
||||||
|
id="XMLID_2_"><path
|
||||||
|
style="fill:#f9f9f9"
|
||||||
|
d="m 492.9,95.3 h -44 c -4.8,0 -8.7,3.9 -8.7,8.7 v 61.9 c 0,4.8 3.9,8.6 8.7,8.6 4.8,0 8.7,-3.8 8.7,-8.6 v -22.5 h 30.2 c 4.4,0 7.9,-3.5 7.9,-7.9 0,-4.4 -3.5,-7.9 -7.9,-7.9 H 457.6 V 111 h 35.2 c 4.4,0 7.9,-3.5 7.9,-7.9 0,-4.3 -3.5,-7.8 -7.8,-7.8 z"
|
||||||
|
class="st0"
|
||||||
|
id="XMLID_13_" /><path
|
||||||
|
style="fill:#f9f9f9"
|
||||||
|
d="m 564.1,111 c 4.4,0 7.9,-3.5 7.9,-7.9 0,-4.4 -3.5,-7.9 -7.9,-7.9 h -44 c -4.8,0 -8.4,3.9 -8.4,8.7 v 61.9 c 0,4.8 3.7,8.6 8.5,8.6 4.8,0 8.5,-3.8 8.5,-8.6 V 143.3 H 559 c 4.4,0 7.9,-3.5 7.9,-7.9 0,-4.4 -3.5,-7.9 -7.9,-7.9 H 528.7 V 111 Z"
|
||||||
|
class="st0"
|
||||||
|
id="XMLID_12_" /><path
|
||||||
|
style="fill:#f9f9f9"
|
||||||
|
d="m 386.6,94 c -21.6,0 -38.1,14.6 -41.3,33.6 h -14.9 c -3.9,0.1 -7,3.3 -7,7.2 0,3.9 3.1,7.1 7,7.2 h 14.9 c 3.3,18.9 19.5,33.1 41.1,33.1 24.2,0 41.9,-18.3 41.9,-40.6 0,-22.4 -17.5,-40.5 -41.7,-40.5 z m 23.4,40.8 c 0,13.5 -9.6,24.5 -23.5,24.5 -11.2,0 -19.8,-7.3 -22.7,-17.2 h 22.7 c 4,0 7.3,-3.1 7.3,-7.2 0,-4 -3.3,-7.3 -7.3,-7.3 h -22.9 c 2.6,-10.2 11.2,-17.7 22.7,-17.7 13.9,0 23.7,11.2 23.7,24.7 z"
|
||||||
|
class="st0"
|
||||||
|
id="XMLID_9_" /><path
|
||||||
|
style="fill:#f9f9f9"
|
||||||
|
d="m 342.5,80.7 -10.8,16 c -11.7,0 -49.1,-0.1 -49.3,-0.1 -3.8,0.2 -6.8,3.4 -6.8,7.2 0,4 3.2,7.2 7.2,7.2 0.1,0 29.7,0 39.1,0 l -11.1,16.4 c -8.6,0 -28.1,0 -28.2,0 h -0.2 v -0.1 c -3.8,0.1 -7,3.3 -7,7.2 0,3.9 3.1,7.1 7,7.2 H 301 l -11.7,17.4 h -6.5 c -4,0 -7.3,3.2 -7.3,7.2 0,3 1.9,5.6 4.5,6.7 l -11.4,17 h 1 L 343.3,80.6 h -0.8 z"
|
||||||
|
class="st0"
|
||||||
|
id="XMLID_8_" /><path
|
||||||
|
style="fill:#f9f9f9"
|
||||||
|
d="m 133.1,94 c -24.2,0 -41.8,18.3 -41.8,40.6 v 0.2 c 0,22.3 17.4,40.4 41.6,40.4 24.2,0 41.8,-18.3 41.8,-40.6 v -0.2 C 174.7,112.1 157.3,94 133.1,94 Z m 23.7,40.8 c 0,13.5 -9.8,24.5 -23.7,24.5 -13.9,0 -23.9,-11.2 -23.9,-24.7 v -0.2 c 0,-13.5 9.8,-24.5 23.7,-24.5 13.9,0 23.9,11.2 23.9,24.7 z"
|
||||||
|
class="st0"
|
||||||
|
id="XMLID_5_" /><circle
|
||||||
|
style="fill:#f9f9f9"
|
||||||
|
r="7.4000001"
|
||||||
|
cy="134.60001"
|
||||||
|
cx="132.5"
|
||||||
|
class="st0"
|
||||||
|
id="XMLID_4_" /><path
|
||||||
|
style="fill:#f9f9f9"
|
||||||
|
d="m 250.8,94.7 c -4.7,0 -8.5,3.8 -8.5,8.5 v 40.5 L 209.1,99.9 c -2.4,-3 -4.7,-5.1 -8.9,-5.1 h -1.8 c -4.8,0 -8.8,3.9 -8.8,8.7 V 166 c 0,4.7 4,8.5 8.7,8.5 4.7,0 8.7,-3.8 8.7,-8.5 v -42 l 34.5,45.4 c 2.4,3 4.7,5.1 8.9,5.1 h 0.6 c 4.8,0 8.5,-3.9 8.5,-8.8 v -62.5 c -0.1,-4.7 -3.9,-8.5 -8.7,-8.5 z"
|
||||||
|
class="st0"
|
||||||
|
id="XMLID_3_" /></g></svg>
|
After Width: | Height: | Size: 3.4 KiB |
21
assets/templates/errors/403.gohtml
Normal file
21
assets/templates/errors/403.gohtml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{{ define "meta" }}
|
||||||
|
<title>Forbidden</title>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="hero is-warning is-fullheight">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column is-8-desktop is-offset-2-desktop">
|
||||||
|
<h1 class="title is-2 is-spaced">Forbidden</h1>
|
||||||
|
<h2 class="subtitle is-4">
|
||||||
|
The received the request was not processed.
|
||||||
|
For example, the CSRF validation may have failed.
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end }}
|
|
@ -6,11 +6,11 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="csrf-token" content="{{ .CSRF_TOKEN }}" />
|
<meta name="csrf-token" content="{{ .csrfToken }}" />
|
||||||
<meta name="csrf-param" content="csrf_token" />
|
<meta name="csrf-param" content="csrf_token" />
|
||||||
|
|
||||||
<title>Admin</title>
|
<title>Admin</title>
|
||||||
<link rel="stylesheet" href="{{ assetURL "css/admin-style" }}">
|
<link rel="stylesheet" href="{{ asset "css/admin-style" }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="admin">
|
<body class="admin">
|
||||||
|
|
|
@ -6,24 +6,29 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta name="csrf-token" content="{{ .CSRF_TOKEN }}" />
|
<meta name="csrf-token" content="{{ .csrfToken }}" />
|
||||||
<meta name="csrf-param" content="csrf_token" />
|
<meta name="csrf-param" content="csrf_token" />
|
||||||
|
|
||||||
<link rel="stylesheet" href='{{ assetURL "vendor/bulma.css" }}'>
|
<link rel="stylesheet" href='{{ asset "vendor/bulma.css" }}'>
|
||||||
<link rel="stylesheet" href='{{ assetURL "css/style.css" }}'>
|
<link rel="stylesheet" href='{{ asset "css/style.css" }}'>
|
||||||
{{ template "meta" . }}
|
{{ template "meta" . }}
|
||||||
{{ if eq .Meta.Env "production" }}
|
{{ if eq .Meta.Env "production" }}
|
||||||
<!-- Add meta tags specific to production environment (analytics etc) -->
|
<!-- Add meta tags specific to production environment (analytics etc) -->
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"
|
||||||
|
integrity="sha384-0AJY8UERsBUKdWcyF3o2kisLKeIo6G4Tbd8Y6fbyw6qYmn4WBuqcvxokp8m2UzSD"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{{ template "header" . }}
|
{{ if .flashes}}
|
||||||
<div id="flash-container">
|
<div id="flash-container">
|
||||||
{{range .flashes}}
|
{{range .flashes}}
|
||||||
<div class="{{ .Class }}">{{ .Message }}</div>
|
{{ .Render }}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ template "header" . }}
|
||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
{{ template "content" . }}
|
{{ template "content" . }}
|
||||||
|
@ -32,8 +37,8 @@
|
||||||
{{ template "footer" . }}
|
{{ template "footer" . }}
|
||||||
{{ template "sink" .}}
|
{{ template "sink" .}}
|
||||||
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
||||||
<script>window.jQuery || document.write('<script src="{{ assetURL "vendor/jquery-3.2.1.min.js" }}"><\/script>')</script>
|
<script>window.jQuery || document.write('<script src="{{ asset "vendor/jquery-3.2.1.min.js" }}"><\/script>')</script>
|
||||||
<script src='{{ assetURL "js/main.js" }}' async></script>
|
<script src='{{ asset "js/main.js" }}' async></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
56
assets/templates/layouts/auth.gohtml
Normal file
56
assets/templates/layouts/auth.gohtml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{{ define "base" }}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="csrf-token" content="{{ .csrfToken }}" />
|
||||||
|
<meta name="csrf-param" content="csrf_token" />
|
||||||
|
|
||||||
|
<link rel="stylesheet" href='{{ asset "vendor/bulma.css" }}'>
|
||||||
|
<link rel="stylesheet" href='{{ asset "css/style.css" }}'>
|
||||||
|
{{ template "meta" . }}
|
||||||
|
{{ if eq .Meta.Env "production" }}
|
||||||
|
<!-- Add meta tags specific to production environment (analytics etc) -->
|
||||||
|
{{ end }}
|
||||||
|
<script defer src="https://use.fontawesome.com/releases/v5.0.6/js/all.js"
|
||||||
|
integrity="sha384-0AJY8UERsBUKdWcyF3o2kisLKeIo6G4Tbd8Y6fbyw6qYmn4WBuqcvxokp8m2UzSD"
|
||||||
|
crossorigin="anonymous"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<section class="hero is-info is-bold is-fullheight">
|
||||||
|
{{ if .flashes}}
|
||||||
|
<div class="hero-head" id="flash-container">
|
||||||
|
{{range .flashes}}
|
||||||
|
{{ .Render }}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-one-third">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-half is-half-mobile is-offset-one-quarter-mobile">
|
||||||
|
<img src="{{ asset "img/logo.svg" }}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ template "content" . }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js" integrity="sha256-hwg4gsxgFZhOsEEamdOYGBf13FyQuiTwlAQgxVSNgt4=" crossorigin="anonymous"></script>
|
||||||
|
<script>window.jQuery || document.write('<script src="{{ asset "vendor/jquery-3.2.1.min.js" }}"><\/script>')</script>
|
||||||
|
<script src='{{ asset "js/main.js" }}' async></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ define "meta"}}{{end}}
|
|
@ -1,25 +1,37 @@
|
||||||
{{ define "header" }}
|
{{ define "header" }}
|
||||||
<nav class="navbar container" role="navigation" aria-label="main navigation">
|
<section class="hero is-link">
|
||||||
<div class="navbar-brand">
|
<div class="hero-body">
|
||||||
<a class="navbar-item" href="#">
|
<div class="container">
|
||||||
<img src="{{ assetURL "img/logo.png" }}" alt="Logo" width="112" height="28">
|
<div class="columns is-vcentered">
|
||||||
</a>
|
<div class="column">
|
||||||
|
<p class="title">
|
||||||
|
Certificate management
|
||||||
|
</p>
|
||||||
|
<p class="subtitle">
|
||||||
|
Generate a <strong>VPN configuration</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="button navbar-burger">
|
|
||||||
<!-- Burger patties -->
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
<span></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-menu">
|
|
||||||
<div class="navbar-start">
|
|
||||||
<a class="navbar-item">Certificates</a>
|
|
||||||
<a class="navbar-item">Change Password</a>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="navbar-end">
|
|
||||||
</div>
|
</div>
|
||||||
<!-- navbar start, navbar end -->
|
|
||||||
|
<div class="hero-foot">
|
||||||
|
<div class="container">
|
||||||
|
<nav class="tabs is-boxed">
|
||||||
|
<ul>
|
||||||
|
<li class="is-active">
|
||||||
|
<a href="{{ url "" }}">Home</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url "certs" }}">Certificates</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ url "users" }}">Users</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav></div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
|
||||||
|
</section>
|
||||||
{{ end }}
|
{{ end }}
|
|
@ -1,6 +1,5 @@
|
||||||
{{ define "meta" }}
|
{{ define "meta" }}
|
||||||
<title>Log in</title>
|
<title>Log in</title>
|
||||||
<meta name="description" content="Test boilerplate" />
|
|
||||||
{{ end}}
|
{{ end}}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
|
24
assets/templates/views/forgot-password.gohtml
Normal file
24
assets/templates/views/forgot-password.gohtml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{{ define "meta" }}<title>Forgot Password</title>{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="box is-shadowless">
|
||||||
|
<div class="content has-text-centered" style="padding: 0 40px;">
|
||||||
|
<h1 class="title has-text-dark">Reset Password</h1>
|
||||||
|
<form action="" method="POST" class="control">
|
||||||
|
<div class="field">
|
||||||
|
<label for="email" class="label is-hidden">Email Address</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input class="input is-medium is-shadowless" id="email" name="email" spellcheck="false" label="false" type="email" placeholder="Email Address" value="" autofocus />
|
||||||
|
<span class="icon is-medium is-left">
|
||||||
|
<i class="fas fa-at"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ .csrfField }}
|
||||||
|
<div class="field is-grouped-right">
|
||||||
|
<input class="button is-success is-fullwidth is-medium" type="submit" value="Reset my Password">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
|
@ -1,24 +1,41 @@
|
||||||
{{ define "meta" }}
|
{{ define "meta" }}<title>Log In</title>{{ end }}
|
||||||
<title>Log in</title>
|
|
||||||
<meta name="description" content="Test boilerplate" />
|
|
||||||
{{ end}}
|
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<section class="content">
|
<div class="box is-shadowless">
|
||||||
<div class="section">
|
<div class="content has-text-centered" style="padding: 0 40px;">
|
||||||
<div class="container">
|
<h1 class="title has-text-dark">Log In</h1>
|
||||||
<div class="columns">
|
<form action="" method="POST" class="control">
|
||||||
<div class="column">
|
<div class="field">
|
||||||
<h1>Hello, World!</h1>
|
<label for="email" class="label is-hidden">Email Address</label>
|
||||||
<form action="" method="POST">
|
<div class="control has-icons-left">
|
||||||
<input name="username" type="text" value=""/>
|
<input class="input is-medium is-shadowless" id="email" name="email" spellcheck="false" label="false" type="" placeholder="Email Address" value="" autofocus />
|
||||||
<input name="password" type="text" value=""/>
|
<span class="icon is-medium is-left">
|
||||||
|
<i class="fas fa-at"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password" class="label is-hidden">Password</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input class="input is-medium is-shadowless" id="password" name="password" type="password" placeholder="Password" value=""/>
|
||||||
|
<span class="icon is-medium is-left">
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ .csrfField }}
|
{{ .csrfField }}
|
||||||
<input class="button is-primary" type="submit" value="Log in">
|
<div class="field is-grouped-right">
|
||||||
|
<input class="button is-success is-fullwidth is-medium" type="submit" value="Log In">
|
||||||
|
</div>
|
||||||
|
<div class="field has-text-centered">
|
||||||
|
<p><a href="/forgot-password">Forgot Password?</a></p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<p class="has-text-white has-text-centered">
|
||||||
|
Don't have an account? <a class="has-text-weight-bold" href="/register">Sign Up</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{ end }}
|
{{ end }}
|
41
assets/templates/views/register.gohtml
Normal file
41
assets/templates/views/register.gohtml
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{{ define "meta" }}<title>Log In</title>{{ end }}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<div class="box is-shadowless">
|
||||||
|
<div class="content has-text-centered" style="padding: 0 40px;">
|
||||||
|
<h1 class="title has-text-dark">Sign Up</h1>
|
||||||
|
<form action="" method="POST" class="control">
|
||||||
|
<div class="field">
|
||||||
|
<label for="email" class="label is-hidden">Email</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input class="input is-medium is-shadowless" id="email" name="email" type="email" placeholder="Email Address" value=""/>
|
||||||
|
<span class="icon is-medium is-left">
|
||||||
|
<i class="fas fa-at"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="password" class="label is-hidden">Password</label>
|
||||||
|
<div class="control has-icons-left">
|
||||||
|
<input class="input is-medium is-shadowless" id="password" name="password" type="password" placeholder="Password" value=""/>
|
||||||
|
<span class="icon is-medium is-left">
|
||||||
|
<i class="fas fa-key"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ .csrfField }}
|
||||||
|
<div class="field is-grouped-right">
|
||||||
|
<input class="button is-success is-fullwidth is-medium" type="submit" value="Sign Up">
|
||||||
|
</div>
|
||||||
|
<div class="field has-text-centered">
|
||||||
|
<p>By signing up you agree to the <a href="{{ url "legal/tos" }}" class="has-text-weight-bold">Terms of Service</a></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<p class="has-text-white has-text-centered">
|
||||||
|
Already have an account? <a class="has-text-weight-bold" href="/login">Log In</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
80
handlers/auth.go
Normal file
80
handlers/auth.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/services"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// Get parameters
|
||||||
|
email := req.Form.Get("email")
|
||||||
|
password := req.Form.Get("password")
|
||||||
|
|
||||||
|
user := models.User{}
|
||||||
|
user.Email = email
|
||||||
|
user.SetPassword(password)
|
||||||
|
|
||||||
|
err := services.Database.Create(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
panic(err.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
services.SessionStore.Flash(w, req,
|
||||||
|
services.Flash{
|
||||||
|
Type: "success",
|
||||||
|
Message: "The user was created. Check your inbox for the confirmation email.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoginHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
// Get parameters
|
||||||
|
email := req.Form.Get("email")
|
||||||
|
password := req.Form.Get("password")
|
||||||
|
|
||||||
|
user := models.User{}
|
||||||
|
|
||||||
|
err := services.Database.Where(&models.User{Email: email}).Find(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
// could not find user
|
||||||
|
services.SessionStore.Flash(
|
||||||
|
w, req, services.Flash{
|
||||||
|
Type: "warning", Message: "Invalid Email or Password.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.EmailValid {
|
||||||
|
services.SessionStore.Flash(
|
||||||
|
w, req, services.Flash{
|
||||||
|
Type: "warning", Message: "You need to confirm your email before logging in.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := user.CheckPassword(password); err != nil {
|
||||||
|
// wrong password
|
||||||
|
services.SessionStore.Flash(
|
||||||
|
w, req, services.Flash{
|
||||||
|
Type: "warning", Message: "Invalid Email or Password.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// user is logged in, set cookie
|
||||||
|
services.SessionStore.SetUserEmail(w, req, email)
|
||||||
|
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusSeeOther)
|
||||||
|
}
|
|
@ -13,40 +13,75 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/views"
|
"git.klink.asia/paul/certman/models"
|
||||||
|
"git.klink.asia/paul/certman/services"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"git.klink.asia/paul/certman/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListCertHandler(db *gorm.DB) http.HandlerFunc {
|
func ListCertHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
v := views.New(req)
|
v := views.New(req)
|
||||||
v.Render(w, "cert_list")
|
v.Render(w, "cert_list")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CreateCertHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
email := services.SessionStore.GetUserEmail(req)
|
||||||
|
certname := req.FormValue("certname")
|
||||||
|
|
||||||
|
user := models.User{}
|
||||||
|
err := services.Database.Where(&models.User{Email: email}).Find(&user).Error
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Could not fetch user for mail %s\n", email)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GenCertHandler(db *gorm.DB) http.HandlerFunc {
|
// Load CA master certificate
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
caCert, caKey, err := loadX509KeyPair("ca.crt", "ca.key")
|
||||||
v := views.New(req)
|
if err != nil {
|
||||||
|
log.Fatalf("error loading ca keyfiles: %s", err)
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate Keypair
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Could not generate keypair: %s", err)
|
log.Fatalf("Could not generate keypair: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
caCert, caKey, err := loadX509KeyPair("ca.crt", "ca.key")
|
// Generate Certificate
|
||||||
if err != nil {
|
|
||||||
v.Render(w, "500")
|
|
||||||
log.Fatalf("error loading ca keyfiles: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
derBytes, err := CreateCertificate(key, caCert, caKey)
|
derBytes, err := CreateCertificate(key, caCert, caKey)
|
||||||
pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
|
||||||
|
|
||||||
pkBytes := x509.MarshalPKCS1PrivateKey(key)
|
// Initialize new client config
|
||||||
pem.Encode(w, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkBytes})
|
client := models.Client{
|
||||||
return
|
Name: certname,
|
||||||
|
PrivateKey: x509.MarshalPKCS1PrivateKey(key),
|
||||||
|
Cert: derBytes,
|
||||||
|
UserID: user.ID,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert client into database
|
||||||
|
if err := services.Database.Create(&client).Error; err != nil {
|
||||||
|
panic(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
services.SessionStore.Flash(w, req,
|
||||||
|
services.Flash{
|
||||||
|
Type: "success",
|
||||||
|
Message: "The certificate was created successfully.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DownloadCertHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
//v := views.New(req)
|
||||||
|
//
|
||||||
|
//derBytes, err := CreateCertificate(key, caCert, caKey)
|
||||||
|
//pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
||||||
|
//
|
||||||
|
//pkBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||||
|
//pem.Encode(w, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkBytes})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateKey, error) {
|
func loadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateKey, error) {
|
|
@ -15,3 +15,8 @@ func ErrorHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
view := views.New(req)
|
view := views.New(req)
|
||||||
view.RenderError(w, http.StatusInternalServerError)
|
view.RenderError(w, http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CSRFErrorHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
|
view := views.New(req)
|
||||||
|
view.RenderError(w, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
package handlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/models"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func LoginHandler(db *gorm.DB) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
// Get parameters
|
|
||||||
username := req.Form.Get("username")
|
|
||||||
password := req.Form.Get("password")
|
|
||||||
|
|
||||||
user := models.User{}
|
|
||||||
|
|
||||||
err := db.Where(&models.User{Username: username}).Find(&user).Error
|
|
||||||
if err != nil {
|
|
||||||
// could not find user
|
|
||||||
http.Redirect(w, req, "/login", http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.CheckPassword(password); err != nil {
|
|
||||||
// wrong password
|
|
||||||
http.Redirect(w, req, "/login", http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
// user is logged in
|
|
||||||
// set cookie
|
|
||||||
http.Redirect(w, req, "/certs", http.StatusFound)
|
|
||||||
}
|
|
||||||
}
|
|
8
main.go
8
main.go
|
@ -18,6 +18,14 @@ func main() {
|
||||||
// Connect to the database
|
// Connect to the database
|
||||||
db := services.InitDB()
|
db := services.InitDB()
|
||||||
|
|
||||||
|
services.InitSession()
|
||||||
|
|
||||||
|
//user := models.User{}
|
||||||
|
//user.Username = "test"
|
||||||
|
//user.SetPassword("test")
|
||||||
|
//fmt.Println(user.HashedPassword)
|
||||||
|
//fmt.Println(db.Create(&user).Error)
|
||||||
|
|
||||||
// load and parse template files
|
// load and parse template files
|
||||||
views.LoadTemplates()
|
views.LoadTemplates()
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/handlers"
|
"git.klink.asia/paul/certman/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RequireLogin is a middleware that checks for a username in the active
|
||||||
|
// session, and redirects to `/login` if no username was found.
|
||||||
func RequireLogin(next http.Handler) http.Handler {
|
func RequireLogin(next http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, req *http.Request) {
|
||||||
defer func() {
|
if username := services.SessionStore.GetUserEmail(req); username == "" {
|
||||||
if rvr := recover(); rvr != nil {
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
log.Println(rvr)
|
|
||||||
log.Println(string(debug.Stack()))
|
|
||||||
handlers.ErrorHandler(w, r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, req)
|
||||||
|
}
|
||||||
return http.HandlerFunc(fn)
|
return http.HandlerFunc(fn)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,9 @@ package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jinzhu/gorm"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -12,31 +13,64 @@ var (
|
||||||
ErrNotImplemented = errors.New("Not implemented")
|
ErrNotImplemented = errors.New("Not implemented")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Model is a base model definition, including helpful fields for dealing with
|
||||||
|
// models in a database
|
||||||
|
type Model struct {
|
||||||
|
ID uint `gorm:"primary_key"`
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
DeletedAt *time.Time `sql:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
// User represents a User of the system which is able to log in
|
// User represents a User of the system which is able to log in
|
||||||
type User struct {
|
type User struct {
|
||||||
gorm.Model
|
Model
|
||||||
Username string
|
Email string
|
||||||
|
EmailValid bool
|
||||||
|
DisplayName string
|
||||||
HashedPassword []byte
|
HashedPassword []byte
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetPassword sets the password of an user struct, but does not save it yet
|
// SetPassword sets the password of an user struct, but does not save it yet
|
||||||
func (u *User) SetPassword(password string) error {
|
func (u *User) SetPassword(password string) error {
|
||||||
return ErrNotImplemented
|
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.HashedPassword = bytes
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckPassword compares a supplied plain text password with the internally
|
// CheckPassword compares a supplied plain text password with the internally
|
||||||
// stored password hash, returns error=nil on success.
|
// stored password hash, returns error=nil on success.
|
||||||
func (u *User) CheckPassword(password string) error {
|
func (u *User) CheckPassword(password string) error {
|
||||||
return ErrNotImplemented
|
return bcrypt.CompareHashAndPassword(u.HashedPassword, []byte(password))
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientConf represent the OpenVPN client configuration
|
type UserProvider interface {
|
||||||
type ClientConf struct {
|
CountUsers() (uint, error)
|
||||||
gorm.Model
|
CreateUser(*User) (*User, error)
|
||||||
|
ListUsers(count, offset int) ([]*User, error)
|
||||||
|
GetUserByID(id uint) (*User, error)
|
||||||
|
GetUserByEmail(email string) (*User, error)
|
||||||
|
DeleteUser(id uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client represent the OpenVPN client configuration
|
||||||
|
type Client struct {
|
||||||
|
Model
|
||||||
Name string
|
Name string
|
||||||
User User
|
User User
|
||||||
|
UserID uint
|
||||||
Cert []byte
|
Cert []byte
|
||||||
PublicKey []byte
|
|
||||||
PrivateKey []byte
|
PrivateKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ClientProvider interface {
|
||||||
|
CountClients() (uint, error)
|
||||||
|
CreateClient(*User) (*User, error)
|
||||||
|
ListClients(count, offset int) ([]*User, error)
|
||||||
|
GetClientByID(id uint) (*User, error)
|
||||||
|
DeleteClient(id uint) error
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/services"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/assets"
|
"git.klink.asia/paul/certman/assets"
|
||||||
"git.klink.asia/paul/certman/handlers"
|
"git.klink.asia/paul/certman/handlers"
|
||||||
"git.klink.asia/paul/certman/views"
|
"git.klink.asia/paul/certman/views"
|
||||||
|
@ -28,12 +30,15 @@ func HandleRoutes(db *gorm.DB) http.Handler {
|
||||||
mux := chi.NewMux()
|
mux := chi.NewMux()
|
||||||
|
|
||||||
//mux.Use(middleware.RequestID)
|
//mux.Use(middleware.RequestID)
|
||||||
mux.Use(middleware.Logger)
|
mux.Use(middleware.Logger) // log requests
|
||||||
mux.Use(middleware.RealIP)
|
mux.Use(middleware.RealIP) // use proxy headers
|
||||||
mux.Use(middleware.RedirectSlashes)
|
mux.Use(middleware.RedirectSlashes) // redirect trailing slashes
|
||||||
mux.Use(mw.Recoverer)
|
mux.Use(mw.Recoverer) // recover on panic
|
||||||
|
mux.Use(services.SessionStore.Use) // use session storage
|
||||||
|
|
||||||
// we are serving the static files directly from the assets package
|
// we are serving the static files directly from the assets package
|
||||||
|
// this either means we use the embedded files, or live-load
|
||||||
|
// from the file system (if `--tags="dev"` is used).
|
||||||
fileServer(mux, "/static", assets.Assets)
|
fileServer(mux, "/static", assets.Assets)
|
||||||
|
|
||||||
mux.Route("/", func(r chi.Router) {
|
mux.Route("/", func(r chi.Router) {
|
||||||
|
@ -43,21 +48,35 @@ func HandleRoutes(db *gorm.DB) http.Handler {
|
||||||
csrf.Secure(false),
|
csrf.Secure(false),
|
||||||
csrf.CookieName(csrfCookieName),
|
csrf.CookieName(csrfCookieName),
|
||||||
csrf.FieldName(csrfFieldName),
|
csrf.FieldName(csrfFieldName),
|
||||||
|
csrf.ErrorHandler(http.HandlerFunc(handlers.CSRFErrorHandler)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
|
r.HandleFunc("/", v("debug"))
|
||||||
view := views.New(req)
|
|
||||||
view.Render(w, "debug")
|
r.Route("/register", func(r chi.Router) {
|
||||||
|
r.Get("/", v("register"))
|
||||||
|
r.Post("/", handlers.RegisterHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("/login", func(w http.ResponseWriter, req *http.Request) {
|
r.Route("/login", func(r chi.Router) {
|
||||||
view := views.New(req)
|
r.Get("/", v("login"))
|
||||||
view.Render(w, "login")
|
r.Post("/", handlers.LoginHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("/certs", handlers.ListCertHandler(db))
|
//r.Post("/confirm-email/{token}", handlers.ConfirmEmailHandler(db))
|
||||||
r.HandleFunc("/certs/new", handlers.GenCertHandler(db))
|
|
||||||
|
r.Route("/forgot-password", func(r chi.Router) {
|
||||||
|
r.Get("/", v("forgot-password"))
|
||||||
|
r.Post("/", handlers.LoginHandler)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Route("/certs", func(r chi.Router) {
|
||||||
|
r.Use(mw.RequireLogin)
|
||||||
|
r.Get("/", handlers.ListCertHandler)
|
||||||
|
r.Post("/new", handlers.CreateCertHandler)
|
||||||
|
r.HandleFunc("/download/{ID}", handlers.DownloadCertHandler)
|
||||||
|
})
|
||||||
|
|
||||||
r.HandleFunc("/500", func(w http.ResponseWriter, req *http.Request) {
|
r.HandleFunc("/500", func(w http.ResponseWriter, req *http.Request) {
|
||||||
panic("500")
|
panic("500")
|
||||||
|
@ -70,6 +89,14 @@ func HandleRoutes(db *gorm.DB) http.Handler {
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v is a helper function for quickly displaying a view
|
||||||
|
func v(template string) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
view := views.New(req)
|
||||||
|
view.Render(w, template)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// fileServer sets up a http.FileServer handler to serve
|
// fileServer sets up a http.FileServer handler to serve
|
||||||
// static files from a http.FileSystem.
|
// static files from a http.FileSystem.
|
||||||
func fileServer(r chi.Router, path string, root http.FileSystem) {
|
func fileServer(r chi.Router, path string, root http.FileSystem) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/models"
|
"git.klink.asia/paul/certman/models"
|
||||||
|
@ -8,7 +9,17 @@ import (
|
||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DB *gorm.DB
|
// Error Definitions
|
||||||
|
var (
|
||||||
|
ErrNotImplemented = errors.New("Not implemented")
|
||||||
|
)
|
||||||
|
|
||||||
|
var Database *gorm.DB
|
||||||
|
|
||||||
|
// DB is a wrapper around gorm.DB to provide custom methods
|
||||||
|
type DB struct {
|
||||||
|
*gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
func InitDB() *gorm.DB {
|
func InitDB() *gorm.DB {
|
||||||
dsn := settings.Get("DATABASE_URL", "db.sqlite3")
|
dsn := settings.Get("DATABASE_URL", "db.sqlite3")
|
||||||
|
@ -20,7 +31,41 @@ func InitDB() *gorm.DB {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate models
|
// Migrate models
|
||||||
db.AutoMigrate(models.User{}, models.ClientConf{})
|
db.AutoMigrate(models.User{}, models.Client{})
|
||||||
|
db.LogMode(true)
|
||||||
|
|
||||||
|
Database = db
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CountUsers returns the number of Users in the datastore
|
||||||
|
func (db *DB) CountUsers() (uint, error) {
|
||||||
|
return 0, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser inserts a user into the datastore
|
||||||
|
func (db *DB) CreateUser(*models.User) (*models.User, error) {
|
||||||
|
return nil, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers returns a slice of 'count' users, starting at 'offset'
|
||||||
|
func (db *DB) ListUsers(count, offset int) ([]*models.User, error) {
|
||||||
|
var users = make([]*models.User, 0)
|
||||||
|
|
||||||
|
return users, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByID returns a single user by ID
|
||||||
|
func (db *DB) GetUserByID(id uint) (*models.User, error) {
|
||||||
|
return nil, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserByEmail returns a single user by email
|
||||||
|
func (db *DB) GetUserByEmail(email string) (*models.User, error) {
|
||||||
|
return nil, ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser removes a user from the datastore
|
||||||
|
func (db *DB) DeleteUser(id uint) error {
|
||||||
|
return ErrNotImplemented
|
||||||
|
}
|
||||||
|
|
|
@ -1,21 +1,126 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/settings"
|
"git.klink.asia/paul/certman/settings"
|
||||||
|
"github.com/alexedwards/scs"
|
||||||
"github.com/gorilla/securecookie"
|
"github.com/gorilla/securecookie"
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var Sessions sessions.Store
|
var (
|
||||||
|
// SessionName is the name of the session cookie
|
||||||
func InitSession() {
|
SessionName = "session"
|
||||||
store := sessions.NewCookieStore(
|
// CookieKey is the key the cookies are encrypted and signed with
|
||||||
securecookie.GenerateRandomKey(32), // signing key
|
CookieKey = string(securecookie.GenerateRandomKey(32))
|
||||||
securecookie.GenerateRandomKey(32), // encryption key
|
// FlashesKey is the key used for the flashes in the cookie
|
||||||
|
FlashesKey = "_flashes"
|
||||||
|
// UserEmailKey is the key used to reference usernames
|
||||||
|
UserEmailKey = "_user_email"
|
||||||
)
|
)
|
||||||
store.Options.HttpOnly = true
|
|
||||||
store.Options.MaxAge = 7 * 24 * 60 * 60 // 1 Week
|
|
||||||
store.Options.Secure = settings.Get("ENVIRONMENT", "") == "production"
|
|
||||||
|
|
||||||
Sessions = store
|
func init() {
|
||||||
|
// Register the Flash message type, so gob can serialize it
|
||||||
|
gob.Register(Flash{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionStore is a globally accessible sessions store for the application
|
||||||
|
var SessionStore *Store
|
||||||
|
|
||||||
|
// Store is a wrapped scs.Store in order to implement custom
|
||||||
|
// logic
|
||||||
|
type Store struct {
|
||||||
|
*scs.Manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitSession populates the default sessions Store
|
||||||
|
func InitSession() {
|
||||||
|
store := scs.NewCookieManager(
|
||||||
|
CookieKey,
|
||||||
|
)
|
||||||
|
store.HttpOnly(true)
|
||||||
|
store.Lifetime(24 * time.Hour)
|
||||||
|
|
||||||
|
// Use secure cookies (HTTPS only) in production
|
||||||
|
store.Secure(settings.Get("ENVIRONMENT", "") == "production")
|
||||||
|
|
||||||
|
SessionStore = &Store{store}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *Store) GetUserEmail(req *http.Request) string {
|
||||||
|
if store == nil {
|
||||||
|
// if store was not initialized, all requests fail
|
||||||
|
log.Println("Zero pointer when checking session for username")
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := store.Load(req)
|
||||||
|
|
||||||
|
email, err := sess.GetString(UserEmailKey)
|
||||||
|
if err != nil {
|
||||||
|
// Username found
|
||||||
|
return ""
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// User is logged in
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|
||||||
|
func (store *Store) SetUserEmail(w http.ResponseWriter, req *http.Request, email string) {
|
||||||
|
if store == nil {
|
||||||
|
// if store was not initialized, do nothing
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sess := store.Load(req)
|
||||||
|
|
||||||
|
// renew token to avoid session pinning/fixation attack
|
||||||
|
sess.RenewToken(w)
|
||||||
|
|
||||||
|
sess.PutString(w, UserEmailKey, email)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type Flash struct {
|
||||||
|
Message template.HTML
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render renders the flash message as a notification box
|
||||||
|
func (flash Flash) Render() template.HTML {
|
||||||
|
return template.HTML(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"<div class=\"notification is-radiusless is-%s\"><div class=\"container has-text-centered\">%s</div></div>",
|
||||||
|
flash.Type, flash.Message,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash add flash message to session data
|
||||||
|
func (store *Store) Flash(w http.ResponseWriter, req *http.Request, flash Flash) error {
|
||||||
|
var flashes []Flash
|
||||||
|
|
||||||
|
sess := store.Load(req)
|
||||||
|
|
||||||
|
if err := sess.GetObject(FlashesKey, &flashes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
flashes = append(flashes, flash)
|
||||||
|
|
||||||
|
return sess.PutObject(w, FlashesKey, flashes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flashes returns a slice of flash messages from session data
|
||||||
|
func (store *Store) Flashes(w http.ResponseWriter, req *http.Request) []Flash {
|
||||||
|
var flashes []Flash
|
||||||
|
sess := store.Load(req)
|
||||||
|
sess.PopObject(w, FlashesKey, &flashes)
|
||||||
|
return flashes
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var funcs = template.FuncMap{
|
var funcs = template.FuncMap{
|
||||||
"assetURL": assetURLFn,
|
"asset": assetURLFn,
|
||||||
|
"url": relURLFn,
|
||||||
"lower": lower,
|
"lower": lower,
|
||||||
"upper": upper,
|
"upper": upper,
|
||||||
"date": dateFn,
|
"date": dateFn,
|
||||||
"humanDate": readableDateFn,
|
"humanDate": readableDateFn,
|
||||||
|
"t": translateFn,
|
||||||
}
|
}
|
||||||
|
|
||||||
func lower(input string) string {
|
func lower(input string) string {
|
||||||
|
@ -28,6 +30,11 @@ func assetURLFn(input string) string {
|
||||||
return fmt.Sprintf("%s%s", url, input)
|
return fmt.Sprintf("%s%s", url, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func relURLFn(input string) string {
|
||||||
|
url := "/" //os.Getenv("ASSET_URL")
|
||||||
|
return fmt.Sprintf("%s%s", url, input)
|
||||||
|
}
|
||||||
|
|
||||||
func dateFn(format string, input interface{}) string {
|
func dateFn(format string, input interface{}) string {
|
||||||
var t time.Time
|
var t time.Time
|
||||||
switch date := input.(type) {
|
switch date := input.(type) {
|
||||||
|
@ -40,6 +47,10 @@ func dateFn(format string, input interface{}) string {
|
||||||
return t.Format(format)
|
return t.Format(format)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func translateFn(language string, text string) string {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
func readableDateFn(t time.Time) string {
|
func readableDateFn(t time.Time) string {
|
||||||
if time.Now().Before(t) {
|
if time.Now().Before(t) {
|
||||||
return "in the future"
|
return "in the future"
|
||||||
|
|
|
@ -18,11 +18,15 @@ var templates map[string]*template.Template
|
||||||
func LoadTemplates() {
|
func LoadTemplates() {
|
||||||
templates = map[string]*template.Template{
|
templates = map[string]*template.Template{
|
||||||
"401": newTemplate("layouts/application.gohtml", "errors/401.gohtml"),
|
"401": newTemplate("layouts/application.gohtml", "errors/401.gohtml"),
|
||||||
|
"403": newTemplate("layouts/application.gohtml", "errors/403.gohtml"),
|
||||||
"404": newTemplate("layouts/application.gohtml", "errors/404.gohtml"),
|
"404": newTemplate("layouts/application.gohtml", "errors/404.gohtml"),
|
||||||
"500": newTemplate("layouts/application.gohtml", "errors/500.gohtml"),
|
"500": newTemplate("layouts/application.gohtml", "errors/500.gohtml"),
|
||||||
|
|
||||||
|
"login": newTemplate("layouts/auth.gohtml", "views/login.gohtml"),
|
||||||
|
"register": newTemplate("layouts/auth.gohtml", "views/register.gohtml"),
|
||||||
|
"forgot-password": newTemplate("layouts/auth.gohtml", "views/forgot-password.gohtml"),
|
||||||
|
|
||||||
"debug": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/debug.gohtml"),
|
"debug": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/debug.gohtml"),
|
||||||
"login": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/login.gohtml"),
|
|
||||||
"cert_list": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/cert_list.gohtml"),
|
"cert_list": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/cert_list.gohtml"),
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
|
@ -7,6 +7,8 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/services"
|
||||||
|
|
||||||
"github.com/gorilla/csrf"
|
"github.com/gorilla/csrf"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -21,10 +23,12 @@ func New(req *http.Request) *View {
|
||||||
Vars: map[string]interface{}{
|
Vars: map[string]interface{}{
|
||||||
"CSRF_TOKEN": csrf.Token(req),
|
"CSRF_TOKEN": csrf.Token(req),
|
||||||
"csrfField": csrf.TemplateField(req),
|
"csrfField": csrf.TemplateField(req),
|
||||||
|
"username": services.SessionStore.GetUserEmail(req),
|
||||||
"Meta": map[string]interface{}{
|
"Meta": map[string]interface{}{
|
||||||
"Path": req.URL.Path,
|
"Path": req.URL.Path,
|
||||||
"Env": "develop",
|
"Env": "develop",
|
||||||
},
|
},
|
||||||
|
"flashes": []services.Flash{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +43,9 @@ func (view View) Render(w http.ResponseWriter, name string) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add flashes to template
|
||||||
|
view.Vars["flashes"] = services.SessionStore.Flashes(w, view.Request)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
t.Execute(w, view.Vars)
|
t.Execute(w, view.Vars)
|
||||||
|
@ -49,12 +56,12 @@ func (view View) RenderError(w http.ResponseWriter, status int) {
|
||||||
var name string
|
var name string
|
||||||
|
|
||||||
switch status {
|
switch status {
|
||||||
case http.StatusNotFound:
|
|
||||||
name = "404"
|
|
||||||
case http.StatusUnauthorized:
|
case http.StatusUnauthorized:
|
||||||
name = "401"
|
name = "401"
|
||||||
case http.StatusForbidden:
|
case http.StatusForbidden:
|
||||||
name = "403"
|
name = "403"
|
||||||
|
case http.StatusNotFound:
|
||||||
|
name = "404"
|
||||||
default:
|
default:
|
||||||
name = "500"
|
name = "500"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue