Simplify: use OAuth2
This commit is contained in:
parent
c185c54aa6
commit
71e830d52f
25 changed files with 524 additions and 643 deletions
78
assets/templates/files/config.ovpn
Normal file
78
assets/templates/files/config.ovpn
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
{{ define "base" }}
|
||||||
|
client
|
||||||
|
dev tun
|
||||||
|
proto udp
|
||||||
|
remote 195.201.7.179 443
|
||||||
|
resolv-retry infinite
|
||||||
|
nobind
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
|
||||||
|
cipher AES-256-CBC
|
||||||
|
auth SHA512
|
||||||
|
ns-cert-type server
|
||||||
|
key-direction 1
|
||||||
|
tls-version-min 1.2
|
||||||
|
;comp-lzo
|
||||||
|
verb 3
|
||||||
|
|
||||||
|
route 172.31.1.100 255.255.255.255 net_gateway
|
||||||
|
|
||||||
|
<ca>
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDwDCCAqigAwIBAgIJAMvRC7FajlAOMA0GCSqGSIb3DQEBCwUAMHUxCzAJBgNV
|
||||||
|
BAYTAkRFMQ8wDQYDVQQIDAZCZXJsaW4xDzANBgNVBAcMBkJlcmxpbjETMBEGA1UE
|
||||||
|
CgwKT25lT2ZmVGVjaDEWMBQGA1UECwwNSVQgZGVwYXJ0bWVudDEXMBUGA1UEAwwO
|
||||||
|
Y2EtY2VydGlmaWNhdGUwHhcNMTgwMTI1MTM0NjI3WhcNMjMwMTI0MTM0NjI3WjB1
|
||||||
|
MQswCQYDVQQGEwJERTEPMA0GA1UECAwGQmVybGluMQ8wDQYDVQQHDAZCZXJsaW4x
|
||||||
|
EzARBgNVBAoMCk9uZU9mZlRlY2gxFjAUBgNVBAsMDUlUIGRlcGFydG1lbnQxFzAV
|
||||||
|
BgNVBAMMDmNhLWNlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
|
||||||
|
CgKCAQEA5LzVrHqz33L5YiFs1HOZWvLht9yQ6+AxK1+RDZsx8490UEYvPnguyU/c
|
||||||
|
8NtaZPtWOg5Qvnh+0tHpLHV+3WbWyIObkix6b3U5EgR6Hgdf1zuzX7y/S2o7uPT1
|
||||||
|
zkCgIi9EQfy0IDIhIErsO0dOWndFt/cAfrMaOx0LV/kzr9bKdgg7WLQoVzUgawZq
|
||||||
|
ROScZUogaElISxC/C77YaGg9V5sV9qTa3uZ9DxuESzXLGMDx3DJMjH+Yu+nhJjoc
|
||||||
|
isSxK5qEnfqWJhZgJFTAY2BRbcMFMieVz/+UGk2GDZf1tpMZOQKxwrNibe4HO8zo
|
||||||
|
lfhX+H+sb4QZCdn30eUGstK/jJdQrQIDAQABo1MwUTAdBgNVHQ4EFgQU9UASoCXR
|
||||||
|
ountXC2vQ4s9BT5qGRYwHwYDVR0jBBgwFoAU9UASoCXRountXC2vQ4s9BT5qGRYw
|
||||||
|
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEA2YgYuFKMzoblpPf+
|
||||||
|
VcyFKAXC9IoOJFeoA8FWMLBy38FedpCP+aFtlnG5eSLB/Xy7rdJK+7ASrdbAsFMD
|
||||||
|
U6P2guqUix4veIBZK0WLGTLfRKHQiOUqNP1zZpWsdrwUoUjGOEt4iqG9PCcaANSg
|
||||||
|
mOfl/BK+MtuRevF6Ry2JAZDArUXrXXjNdRXKB7iNc3Sd5icII53OGXXtn1ehzZXL
|
||||||
|
djbdz4MZa1kbA1ZlJVaYCRzOS/F9kU2aQceO17foxI5BvnOkpONLXDZHs61/KtYu
|
||||||
|
5z7hJoH49+4iyWZuRgWT/sq36qpvu+/f48JPxzqV94Jp77Z9BocTIjdfqHM++X9h
|
||||||
|
Yo95ZQ==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
</ca>
|
||||||
|
|
||||||
|
<cert>
|
||||||
|
{{ .Cert | html }}
|
||||||
|
</cert>
|
||||||
|
|
||||||
|
<key>
|
||||||
|
{{ .Key | html }}
|
||||||
|
</key>
|
||||||
|
|
||||||
|
<tls-auth>
|
||||||
|
#
|
||||||
|
# 2048 bit OpenVPN static key
|
||||||
|
#
|
||||||
|
-----BEGIN OpenVPN Static key V1-----
|
||||||
|
187be23c2b3b0a6a9d79bc5b5c95b70b
|
||||||
|
b43b6b303e7c00eb75121d68df470ea3
|
||||||
|
3cdd0ddedc273f5412f181709890ea32
|
||||||
|
7086cbc5b21bbaf3dd231d115b5ba986
|
||||||
|
1b1aee31bff9be5f6c6f6dd490d593a1
|
||||||
|
eec50bb866558c6b2c6fe62ebfe125d9
|
||||||
|
34b7115e72d94ce08cc9e1c4e8ccdbfe
|
||||||
|
a5ee19ac0aec60da63df881e3c2e7d4d
|
||||||
|
9c4f167ec1b46309f17c16c36683780b
|
||||||
|
bed7551ad7a3d526c19014567370122e
|
||||||
|
98ae0ae7fd83a8a6de09883fcc181b36
|
||||||
|
a8465c0deda7a345ec3d16a4daf3fbf5
|
||||||
|
23dc36a48e679c653b3cfc6dbaa150a7
|
||||||
|
7ace46081d2c3712ce655f4b8211f674
|
||||||
|
4d4688c2b3828f9208a80bf71e6e4554
|
||||||
|
ae09b91154a435995439ad576fcc72c1
|
||||||
|
-----END OpenVPN Static key V1-----
|
||||||
|
</tls-auth>
|
||||||
|
{{ end }}
|
|
@ -1,39 +0,0 @@
|
||||||
{{ 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" />
|
|
||||||
|
|
||||||
<title>Admin</title>
|
|
||||||
<link rel="stylesheet" href="{{ asset "css/admin-style" }}">
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body class="admin">
|
|
||||||
{{ template "header" . }}
|
|
||||||
|
|
||||||
<div id="flash-container">
|
|
||||||
{{range .flashes}}
|
|
||||||
<div class="{{ .Class }}"{{ .Message }}</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container main-container">
|
|
||||||
{{ template "content" . }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ template "footer" . }}
|
|
||||||
{{ template "sink" .}}
|
|
||||||
<script src="/public/assets/admin.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ define "header"}}{{end}}
|
|
||||||
{{ define "content"}}{{end}}
|
|
||||||
{{ define "footer"}}{{end}}
|
|
||||||
{{ define "sink"}}{{end}}
|
|
|
@ -28,14 +28,11 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ template "header" . }}
|
|
||||||
|
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
{{ template "content" . }}
|
{{ template "content" . }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ template "footer" . }}
|
|
||||||
{{ 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="{{ asset "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='{{ asset "js/main.js" }}' async></script>
|
<script src='{{ asset "js/main.js" }}' async></script>
|
||||||
|
@ -45,7 +42,4 @@
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ define "meta"}}{{end}}
|
{{ define "meta"}}{{end}}
|
||||||
{{ define "header"}}{{end}}
|
{{ define "content"}}{{end}}
|
||||||
{{ define "content"}}{{end}}
|
|
||||||
{{ define "footer"}}{{end}}
|
|
||||||
{{ define "sink"}}{{end}}
|
|
|
@ -1,13 +0,0 @@
|
||||||
{{ define "footer" }}
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="container">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column is-one-third">
|
|
||||||
<p>
|
|
||||||
© 2017 OneOffTech
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
{{ end }}
|
|
|
@ -1,37 +0,0 @@
|
||||||
{{ define "header" }}
|
|
||||||
<section class="hero is-link">
|
|
||||||
<div class="hero-body">
|
|
||||||
<div class="container">
|
|
||||||
<div class="columns is-vcentered">
|
|
||||||
<div class="column">
|
|
||||||
<p class="title">
|
|
||||||
Certificate management
|
|
||||||
</p>
|
|
||||||
<p class="subtitle">
|
|
||||||
Generate a <strong>VPN configuration</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
</section>
|
|
||||||
{{ end }}
|
|
|
@ -1,24 +0,0 @@
|
||||||
{{ define "meta" }}
|
|
||||||
<title>Log in</title>
|
|
||||||
{{ end}}
|
|
||||||
|
|
||||||
{{ define "content" }}
|
|
||||||
<section class="content">
|
|
||||||
<div class="section">
|
|
||||||
<div class="container">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<h1>Certificates</h1>
|
|
||||||
{{ if .Certificates }}
|
|
||||||
{{ range .Certificates }}
|
|
||||||
<li>{{ .User }}@{{ .Name }}</li>
|
|
||||||
{{ end }}
|
|
||||||
{{ else }}
|
|
||||||
<p>You don't have certificates yet!</p>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{ end}}
|
|
65
assets/templates/views/client_list.gohtml
Normal file
65
assets/templates/views/client_list.gohtml
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{{ define "meta" }}
|
||||||
|
<title>Log in</title>
|
||||||
|
{{ end}}
|
||||||
|
|
||||||
|
{{ define "content" }}
|
||||||
|
<section class="content">
|
||||||
|
<div class="section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
<h1>Certificates</h1>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<th>Device</th>
|
||||||
|
<th width="20%">Created</th>
|
||||||
|
<th width="20%" class="has-text-centered">Actions</th>
|
||||||
|
</thead>
|
||||||
|
<tfoot>
|
||||||
|
<form action="/certs/new" method="POST">
|
||||||
|
<th colspan="2">
|
||||||
|
<div class="field has-addons">
|
||||||
|
<p class="control is-marginless">
|
||||||
|
<a class="button is-static">
|
||||||
|
{{ $.username }}@
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p class="control is-marginless is-expanded">
|
||||||
|
<input name="certname" class="input" type="text" placeholder="Certificate name (e.g. Laptop)">
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>{{ .csrfField }}<input type="submit" class="button is-success is-fullwidth" value="Create"/></th>
|
||||||
|
</form>
|
||||||
|
</tfoot>
|
||||||
|
<tbody>
|
||||||
|
{{ range .Clients }}
|
||||||
|
<tr>
|
||||||
|
<td class="is-vcentered"><p>{{ $.username }}@{{ .Name }}</p></td>
|
||||||
|
<td><time title="{{ .CreatedAt.UTC }}">{{ .CreatedAt | humanDate }}</time></td>
|
||||||
|
<td>
|
||||||
|
<div class="field has-addons">
|
||||||
|
<p class="control is-marginless is-expanded">
|
||||||
|
<a href="/certs/download/{{ .Name }}" class="button is-primary is-fullwidth">Download</a>
|
||||||
|
</p>
|
||||||
|
<p class="control is-marginless">
|
||||||
|
<a class="button is-danger">
|
||||||
|
<span class="icon is-small">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{ end }}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{{ end}}
|
|
@ -1,17 +0,0 @@
|
||||||
{{ define "meta" }}
|
|
||||||
<title>Landing Page</title>
|
|
||||||
<meta name="description" content="Test boilerplate" />
|
|
||||||
{{ end}}
|
|
||||||
|
|
||||||
{{ define "content" }}
|
|
||||||
<section class="content">
|
|
||||||
<div class="container">
|
|
||||||
<div class="columns">
|
|
||||||
<div class="column">
|
|
||||||
<h1>Hello, World!</h1>
|
|
||||||
<p>Have some variables: {{.}}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{{ end}}
|
|
|
@ -1,24 +0,0 @@
|
||||||
{{ 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,32 +0,0 @@
|
||||||
{{ 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>
|
|
||||||
{{ .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 }}
|
|
215
handlers/auth.go
215
handlers/auth.go
|
@ -1,179 +1,86 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"os"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/views"
|
"git.klink.asia/paul/certman/views"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
"github.com/go-chi/chi"
|
|
||||||
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/services"
|
"git.klink.asia/paul/certman/services"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/models"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func RegisterHandler(p *services.Provider) http.HandlerFunc {
|
var GitlabConfig = &oauth2.Config{
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
ClientID: os.Getenv("OAUTH2_CLIENT_ID"),
|
||||||
// Get parameters
|
ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"),
|
||||||
email := req.Form.Get("email")
|
Scopes: []string{"read_user"},
|
||||||
|
RedirectURL: os.Getenv("HOST") + "/login/oauth2/redirect",
|
||||||
user := models.User{}
|
Endpoint: oauth2.Endpoint{
|
||||||
user.Email = email
|
AuthURL: os.Getenv("OAUTH2_AUTH_URL"),
|
||||||
|
TokenURL: os.Getenv("OAUTH2_TOKEN_URL"),
|
||||||
// don't set a password, user will get password reset request via mail
|
},
|
||||||
user.HashedPassword = []byte{}
|
|
||||||
|
|
||||||
err := p.DB.CreateUser(&user)
|
|
||||||
if err != nil {
|
|
||||||
panic(err.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := createPasswordReset(p, &user); err != nil {
|
|
||||||
p.Sessions.Flash(w, req,
|
|
||||||
services.Flash{
|
|
||||||
Type: "danger",
|
|
||||||
Message: "The registration email could not be generated.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
http.Redirect(w, req, "/register", http.StatusFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Sessions.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(p *services.Provider) http.HandlerFunc {
|
func OAuth2Endpoint(p *services.Provider) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
// Get parameters
|
|
||||||
email := req.Form.Get("email")
|
|
||||||
password := req.Form.Get("password")
|
|
||||||
|
|
||||||
user, err := p.DB.GetUserByEmail(email)
|
|
||||||
if err != nil {
|
|
||||||
// could not find user
|
|
||||||
p.Sessions.Flash(
|
|
||||||
w, req, services.Flash{
|
|
||||||
Type: "warning", Message: "Invalid Email or Password.",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
http.Redirect(w, req, "/login", http.StatusFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !user.EmailValid {
|
|
||||||
p.Sessions.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
|
|
||||||
p.Sessions.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
|
|
||||||
p.Sessions.SetUserEmail(w, req, email)
|
|
||||||
|
|
||||||
http.Redirect(w, req, "/certs", http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConfirmEmailHandler(p *services.Provider) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
v := views.NewWithSession(req, p.Sessions)
|
v := views.NewWithSession(req, p.Sessions)
|
||||||
|
|
||||||
switch req.Method {
|
code := req.FormValue("code")
|
||||||
case "GET":
|
|
||||||
token := chi.URLParam(req, "token")
|
|
||||||
pwr, err := p.DB.GetPasswordResetByToken(token)
|
|
||||||
_ = pwr
|
|
||||||
if err != nil {
|
|
||||||
v.RenderError(w, 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
v.Render(w, "email-set-password")
|
|
||||||
case "POST":
|
|
||||||
password := req.Form.Get("password")
|
|
||||||
token := req.Form.Get("token")
|
|
||||||
pwr, err := p.DB.GetPasswordResetByToken(token)
|
|
||||||
if err != nil {
|
|
||||||
v.RenderError(w, 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := p.DB.GetUserByID(pwr.UserID)
|
// exchange code for token
|
||||||
if err != nil {
|
accessToken, err := GitlabConfig.Exchange(oauth2.NoContext, code)
|
||||||
v.RenderError(w, 500)
|
if err != nil {
|
||||||
return
|
fmt.Println(err)
|
||||||
}
|
http.NotFound(w, req)
|
||||||
|
return
|
||||||
user.SetPassword(password)
|
|
||||||
|
|
||||||
//err := p.DB.UpdateUser(user.ID, &user)
|
|
||||||
if err != nil {
|
|
||||||
v.RenderError(w, 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = p.DB.DeletePasswordResetsByUserID(pwr.UserID)
|
|
||||||
|
|
||||||
default:
|
|
||||||
v.RenderError(w, 405)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to get post params
|
if accessToken.Valid() {
|
||||||
|
// generate a client using the access token
|
||||||
|
httpClient := GitlabConfig.Client(oauth2.NoContext, accessToken)
|
||||||
|
|
||||||
fmt.Fprintln(w, "Okay.")
|
apiRequest, err := http.NewRequest("GET", "https://git.klink.asia/api/v4/user", nil)
|
||||||
|
if err != nil {
|
||||||
|
v.RenderError(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpClient.Do(apiRequest)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
v.RenderError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&user)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
v.RenderError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Username != "" {
|
||||||
|
p.Sessions.SetUsername(w, req, user.Username)
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
v.RenderError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createPasswordReset(p *services.Provider, user *models.User) error {
|
func GetLoginHandler(p *services.Provider) http.HandlerFunc {
|
||||||
// create the reset request
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
pwr := models.PasswordReset{
|
authURL := GitlabConfig.AuthCodeURL("", oauth2.AccessTypeOnline)
|
||||||
UserID: user.ID,
|
http.Redirect(w, req, authURL, http.StatusFound)
|
||||||
Token: string(securecookie.GenerateRandomKey(32)),
|
|
||||||
ValidUntil: time.Now().Add(6 * time.Hour),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.DB.CreatePasswordReset(&pwr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var subject string
|
|
||||||
var text *bytes.Buffer
|
|
||||||
|
|
||||||
if user.EmailValid {
|
|
||||||
subject = "Password reset"
|
|
||||||
text.WriteString("Somebody (hopefully you) has requested a password reset.\nClick below to reset your password:\n")
|
|
||||||
} else {
|
|
||||||
// If the user email has not been confirmed yet, send out
|
|
||||||
// "mail confirmation"-mail instead
|
|
||||||
subject = "Email confirmation"
|
|
||||||
text.WriteString("Hello, thanks you for signing up!\nClick below to verify this email address\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.Email.Send(user.Email, subject, text.String(), "")
|
|
||||||
}
|
}
|
||||||
|
|
103
handlers/cert.go
103
handlers/cert.go
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
|
@ -15,25 +16,38 @@ import (
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/models"
|
"git.klink.asia/paul/certman/models"
|
||||||
"git.klink.asia/paul/certman/services"
|
"git.klink.asia/paul/certman/services"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/views"
|
"git.klink.asia/paul/certman/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ListCertHandler(p *services.Provider) http.HandlerFunc {
|
func ListClientsHandler(p *services.Provider) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
v := views.NewWithSession(req, p.Sessions)
|
v := views.NewWithSession(req, p.Sessions)
|
||||||
v.Render(w, "cert_list")
|
|
||||||
|
username := p.Sessions.GetUsername(req)
|
||||||
|
|
||||||
|
clients, _ := p.DB.ListClientsForUser(username, 100, 0)
|
||||||
|
|
||||||
|
v.Vars["Clients"] = clients
|
||||||
|
v.Render(w, "client_list")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
email := p.Sessions.GetUserEmail(req)
|
username := p.Sessions.GetUsername(req)
|
||||||
certname := req.FormValue("certname")
|
certname := req.FormValue("certname")
|
||||||
|
|
||||||
user, err := p.DB.GetUserByEmail(email)
|
if !IsByteLength(certname, 2, 64) || !IsAlphanumeric(certname) {
|
||||||
if err != nil {
|
p.Sessions.Flash(w, req,
|
||||||
fmt.Printf("Could not fetch user for mail %s\n", email)
|
services.Flash{
|
||||||
|
Type: "danger",
|
||||||
|
Message: "The certificate name can only contain letters and numbers",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load CA master certificate
|
// Load CA master certificate
|
||||||
|
@ -47,29 +61,45 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
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)
|
||||||
|
p.Sessions.Flash(w, req,
|
||||||
|
services.Flash{
|
||||||
|
Type: "danger",
|
||||||
|
Message: "The certificate key could not be generated",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate Certificate
|
// Generate Certificate
|
||||||
derBytes, err := CreateCertificate(key, caCert, caKey)
|
commonName := fmt.Sprintf("%s@%s", username, certname)
|
||||||
|
derBytes, err := CreateCertificate(commonName, key, caCert, caKey)
|
||||||
|
|
||||||
// Initialize new client config
|
// Initialize new client config
|
||||||
client := models.Client{
|
client := models.Client{
|
||||||
Name: certname,
|
Name: certname,
|
||||||
PrivateKey: x509.MarshalPKCS1PrivateKey(key),
|
PrivateKey: x509.MarshalPKCS1PrivateKey(key),
|
||||||
Cert: derBytes,
|
Cert: derBytes,
|
||||||
UserID: user.ID,
|
User: username,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert client into database
|
// Insert client into database
|
||||||
_ = client
|
if err := p.DB.CreateClient(&client); err != nil {
|
||||||
//if err := p.DB.Create(&client).Error; err != nil {
|
log.Println(err.Error())
|
||||||
// panic(err.Error())
|
p.Sessions.Flash(w, req,
|
||||||
//}
|
services.Flash{
|
||||||
|
Type: "danger",
|
||||||
|
Message: "The certificate could not be added to the database",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
p.Sessions.Flash(w, req,
|
p.Sessions.Flash(w, req,
|
||||||
services.Flash{
|
services.Flash{
|
||||||
Type: "success",
|
Type: "success",
|
||||||
Message: "The certificate was created successfully.",
|
Message: "The certificate was created successfully",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -79,13 +109,42 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
|
|
||||||
func DownloadCertHandler(p *services.Provider) http.HandlerFunc {
|
func DownloadCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
//v := views.New(req)
|
v := views.New(req)
|
||||||
//
|
// detemine own username
|
||||||
//derBytes, err := CreateCertificate(key, caCert, caKey)
|
username := p.Sessions.GetUsername(req)
|
||||||
//pem.Encode(w, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
|
name := chi.URLParam(req, "name")
|
||||||
//
|
|
||||||
//pkBytes := x509.MarshalPKCS1PrivateKey(key)
|
client, err := p.DB.GetClientByNameUser(name, username)
|
||||||
//pem.Encode(w, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: pkBytes})
|
if err != nil {
|
||||||
|
v.RenderError(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// cbuf and kbuf are buffers in which the PEM certificates are
|
||||||
|
// rendered into
|
||||||
|
var cbuf = new(bytes.Buffer)
|
||||||
|
var kbuf = new(bytes.Buffer)
|
||||||
|
|
||||||
|
pem.Encode(cbuf, &pem.Block{Type: "CERTIFICATE", Bytes: client.Cert})
|
||||||
|
pem.Encode(kbuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: client.PrivateKey})
|
||||||
|
|
||||||
|
vars := map[string]string{
|
||||||
|
"Cert": cbuf.String(),
|
||||||
|
"Key": kbuf.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := views.GetTemplate("config.ovpn")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error loading certificate template: %s", err)
|
||||||
|
v.RenderError(w, http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/x-openvpn-profile")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\"config.ovpn\"")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
log.Println(vars)
|
||||||
|
t.Execute(w, vars)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -117,10 +176,10 @@ func loadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateK
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateCertificate creates a CA-signed certificate
|
// CreateCertificate creates a CA-signed certificate
|
||||||
func CreateCertificate(key interface{}, caCert *x509.Certificate, caKey interface{}) ([]byte, error) {
|
func CreateCertificate(commonName string, key interface{}, caCert *x509.Certificate, caKey interface{}) ([]byte, error) {
|
||||||
subj := caCert.Subject
|
subj := caCert.Subject
|
||||||
// .. except for the common name
|
// .. except for the common name
|
||||||
subj.CommonName = "clientName"
|
subj.CommonName = commonName
|
||||||
|
|
||||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
|
64
handlers/converters.go
Normal file
64
handlers/converters.go
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToString convert the input to a string.
|
||||||
|
func ToString(obj interface{}) string {
|
||||||
|
res := fmt.Sprintf("%v", obj)
|
||||||
|
return string(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToJSON convert the input to a valid JSON string
|
||||||
|
func ToJSON(obj interface{}) (string, error) {
|
||||||
|
res, err := json.Marshal(obj)
|
||||||
|
if err != nil {
|
||||||
|
res = []byte("")
|
||||||
|
}
|
||||||
|
return string(res), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFloat convert the input string to a float, or 0.0 if the input is not a float.
|
||||||
|
func ToFloat(str string) (float64, error) {
|
||||||
|
res, err := strconv.ParseFloat(str, 64)
|
||||||
|
if err != nil {
|
||||||
|
res = 0.0
|
||||||
|
}
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToInt convert the input string or any int type to an integer type 64, or 0 if the input is not an integer.
|
||||||
|
func ToInt(value interface{}) (res int64, err error) {
|
||||||
|
val := reflect.ValueOf(value)
|
||||||
|
|
||||||
|
switch value.(type) {
|
||||||
|
case int, int8, int16, int32, int64:
|
||||||
|
res = val.Int()
|
||||||
|
case uint, uint8, uint16, uint32, uint64:
|
||||||
|
res = int64(val.Uint())
|
||||||
|
case string:
|
||||||
|
if IsInt(val.String()) {
|
||||||
|
res, err = strconv.ParseInt(val.String(), 0, 64)
|
||||||
|
if err != nil {
|
||||||
|
res = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("math: square root of negative number %g", value)
|
||||||
|
res = 0
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("math: square root of negative number %g", value)
|
||||||
|
res = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToBoolean convert the input string to a boolean.
|
||||||
|
func ToBoolean(str string) (bool, error) {
|
||||||
|
return strconv.ParseBool(str)
|
||||||
|
}
|
120
handlers/validators.go
Normal file
120
handlers/validators.go
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
package handlers
|
||||||
|
|
||||||
|
import "regexp"
|
||||||
|
|
||||||
|
const (
|
||||||
|
Email string = "^(((([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+(\\.([a-zA-Z]|\\d|[!#\\$%&'\\*\\+\\-\\/=\\?\\^_`{\\|}~]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])+)*)|((\\x22)((((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(([\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]|\\x21|[\\x23-\\x5b]|[\\x5d-\\x7e]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(\\([\\x01-\\x09\\x0b\\x0c\\x0d-\\x7f]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}]))))*(((\\x20|\\x09)*(\\x0d\\x0a))?(\\x20|\\x09)+)?(\\x22)))@((([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|\\.|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|\\d|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.)+(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])|(([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])([a-zA-Z]|\\d|-|_|~|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])*([a-zA-Z]|[\\x{00A0}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFEF}])))\\.?$"
|
||||||
|
CreditCard string = "^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\\d{11})$"
|
||||||
|
ISBN10 string = "^(?:[0-9]{9}X|[0-9]{10})$"
|
||||||
|
ISBN13 string = "^(?:[0-9]{13})$"
|
||||||
|
UUID3 string = "^[0-9a-f]{8}-[0-9a-f]{4}-3[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||||
|
UUID4 string = "^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||||
|
UUID5 string = "^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$"
|
||||||
|
UUID string = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
|
||||||
|
Alpha string = "^[a-zA-Z]+$"
|
||||||
|
Alphanumeric string = "^[a-zA-Z0-9]+$"
|
||||||
|
Numeric string = "^[0-9]+$"
|
||||||
|
Int string = "^(?:[-+]?(?:0|[1-9][0-9]*))$"
|
||||||
|
Float string = "^(?:[-+]?(?:[0-9]+))?(?:\\.[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$"
|
||||||
|
Hexadecimal string = "^[0-9a-fA-F]+$"
|
||||||
|
Hexcolor string = "^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$"
|
||||||
|
RGBcolor string = "^rgb\\(\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*,\\s*(0|[1-9]\\d?|1\\d\\d?|2[0-4]\\d|25[0-5])\\s*\\)$"
|
||||||
|
ASCII string = "^[\x00-\x7F]+$"
|
||||||
|
Multibyte string = "[^\x00-\x7F]"
|
||||||
|
FullWidth string = "[^\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||||
|
HalfWidth string = "[\u0020-\u007E\uFF61-\uFF9F\uFFA0-\uFFDC\uFFE8-\uFFEE0-9a-zA-Z]"
|
||||||
|
Base64 string = "^(?:[A-Za-z0-9+\\/]{4})*(?:[A-Za-z0-9+\\/]{2}==|[A-Za-z0-9+\\/]{3}=|[A-Za-z0-9+\\/]{4})$"
|
||||||
|
PrintableASCII string = "^[\x20-\x7E]+$"
|
||||||
|
DataURI string = "^data:.+\\/(.+);base64$"
|
||||||
|
Latitude string = "^[-+]?([1-8]?\\d(\\.\\d+)?|90(\\.0+)?)$"
|
||||||
|
Longitude string = "^[-+]?(180(\\.0+)?|((1[0-7]\\d)|([1-9]?\\d))(\\.\\d+)?)$"
|
||||||
|
DNSName string = `^([a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62}){1}(\.[a-zA-Z0-9_]{1}[a-zA-Z0-9_-]{0,62})*[\._]?$`
|
||||||
|
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
|
||||||
|
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
|
||||||
|
URLUsername string = `(\S+(:\S*)?@)`
|
||||||
|
URLPath string = `((\/|\?|#)[^\s]*)`
|
||||||
|
URLPort string = `(:(\d{1,5}))`
|
||||||
|
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3])(\.(1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-4]))`
|
||||||
|
URLSubdomain string = `((www\.)|([a-zA-Z0-9]([-\.][-\._a-zA-Z0-9]+)*))`
|
||||||
|
URL string = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
|
||||||
|
SSN string = `^\d{3}[- ]?\d{2}[- ]?\d{4}$`
|
||||||
|
WinPath string = `^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$`
|
||||||
|
UnixPath string = `^(/[^/\x00]*)+/?$`
|
||||||
|
Semver string = "^v?(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)\\.(?:0|[1-9]\\d*)(-(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(\\.(0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\\+[0-9a-zA-Z-]+(\\.[0-9a-zA-Z-]+)*)?$"
|
||||||
|
tagName string = "valid"
|
||||||
|
hasLowerCase string = ".*[[:lower:]]"
|
||||||
|
hasUpperCase string = ".*[[:upper:]]"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rxEmail = regexp.MustCompile(Email)
|
||||||
|
rxCreditCard = regexp.MustCompile(CreditCard)
|
||||||
|
rxISBN10 = regexp.MustCompile(ISBN10)
|
||||||
|
rxISBN13 = regexp.MustCompile(ISBN13)
|
||||||
|
rxUUID3 = regexp.MustCompile(UUID3)
|
||||||
|
rxUUID4 = regexp.MustCompile(UUID4)
|
||||||
|
rxUUID5 = regexp.MustCompile(UUID5)
|
||||||
|
rxUUID = regexp.MustCompile(UUID)
|
||||||
|
rxAlpha = regexp.MustCompile(Alpha)
|
||||||
|
rxAlphanumeric = regexp.MustCompile(Alphanumeric)
|
||||||
|
rxNumeric = regexp.MustCompile(Numeric)
|
||||||
|
rxInt = regexp.MustCompile(Int)
|
||||||
|
rxFloat = regexp.MustCompile(Float)
|
||||||
|
rxHexadecimal = regexp.MustCompile(Hexadecimal)
|
||||||
|
rxHexcolor = regexp.MustCompile(Hexcolor)
|
||||||
|
rxRGBcolor = regexp.MustCompile(RGBcolor)
|
||||||
|
rxASCII = regexp.MustCompile(ASCII)
|
||||||
|
rxPrintableASCII = regexp.MustCompile(PrintableASCII)
|
||||||
|
rxMultibyte = regexp.MustCompile(Multibyte)
|
||||||
|
rxFullWidth = regexp.MustCompile(FullWidth)
|
||||||
|
rxHalfWidth = regexp.MustCompile(HalfWidth)
|
||||||
|
rxBase64 = regexp.MustCompile(Base64)
|
||||||
|
rxDataURI = regexp.MustCompile(DataURI)
|
||||||
|
rxLatitude = regexp.MustCompile(Latitude)
|
||||||
|
rxLongitude = regexp.MustCompile(Longitude)
|
||||||
|
rxDNSName = regexp.MustCompile(DNSName)
|
||||||
|
rxURL = regexp.MustCompile(URL)
|
||||||
|
rxSSN = regexp.MustCompile(SSN)
|
||||||
|
rxWinPath = regexp.MustCompile(WinPath)
|
||||||
|
rxUnixPath = regexp.MustCompile(UnixPath)
|
||||||
|
rxSemver = regexp.MustCompile(Semver)
|
||||||
|
rxHasLowerCase = regexp.MustCompile(hasLowerCase)
|
||||||
|
rxHasUpperCase = regexp.MustCompile(hasUpperCase)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsAlphanumeric check if the string contains only letters and numbers. Empty string is valid.
|
||||||
|
func IsAlphanumeric(str string) bool {
|
||||||
|
if IsNull(str) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return rxAlphanumeric.MatchString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ByteLength check string's length
|
||||||
|
func ByteLength(str string, params ...string) bool {
|
||||||
|
if len(params) == 2 {
|
||||||
|
min, _ := ToInt(params[0])
|
||||||
|
max, _ := ToInt(params[1])
|
||||||
|
return len(str) >= int(min) && len(str) <= int(max)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsInt check if the string is an integer. Empty string is valid.
|
||||||
|
func IsInt(str string) bool {
|
||||||
|
if IsNull(str) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return rxInt.MatchString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNull check if the string is null.
|
||||||
|
func IsNull(str string) bool {
|
||||||
|
return len(str) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsByteLength check if the string's length (in bytes) falls in a range.
|
||||||
|
func IsByteLength(str string, min, max int) bool {
|
||||||
|
return len(str) >= min && len(str) <= max
|
||||||
|
}
|
13
main.go
13
main.go
|
@ -29,23 +29,10 @@ func main() {
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Lifetime: 24 * time.Hour,
|
Lifetime: 24 * time.Hour,
|
||||||
},
|
},
|
||||||
Email: &services.EmailConfig{
|
|
||||||
SMTPEnabled: false,
|
|
||||||
SMTPServer: "example.com",
|
|
||||||
SMTPPort: 25,
|
|
||||||
SMTPUsername: "test",
|
|
||||||
SMTPPassword: "password",
|
|
||||||
SMTPTimeout: 5 * time.Second,
|
|
||||||
From: "Mailtest <test@example.com>",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serviceProvider := services.NewProvider(&c)
|
serviceProvider := services.NewProvider(&c)
|
||||||
|
|
||||||
// Start the mail daemon, which re-uses connections to send mails to the
|
|
||||||
// SMTP server
|
|
||||||
go serviceProvider.Email.Daemon()
|
|
||||||
|
|
||||||
// load and parse template files
|
// load and parse template files
|
||||||
views.LoadTemplates()
|
views.LoadTemplates()
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
func RequireLogin(sessions *services.Sessions) func(http.Handler) http.Handler {
|
func RequireLogin(sessions *services.Sessions) func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, req *http.Request) {
|
fn := func(w http.ResponseWriter, req *http.Request) {
|
||||||
if username := sessions.GetUserEmail(req); username == "" {
|
if username := sessions.GetUsername(req); username == "" {
|
||||||
http.Redirect(w, req, "/login", http.StatusFound)
|
http.Redirect(w, req, "/login", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,17 +23,17 @@ type Model struct {
|
||||||
// Client represent the OpenVPN client configuration
|
// Client represent the OpenVPN client configuration
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Model
|
Model
|
||||||
Name string
|
Name string `gorm:"index;unique_index:idx_name_user"`
|
||||||
User User
|
User string `gorm:"index;unique_index:idx_name_user"`
|
||||||
UserID uint
|
|
||||||
Cert []byte
|
Cert []byte
|
||||||
PrivateKey []byte
|
PrivateKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientProvider interface {
|
type ClientProvider interface {
|
||||||
CountClients() (uint, error)
|
CountClients() (uint, error)
|
||||||
CreateClient(*User) (*User, error)
|
CreateClient(*Client) (*Client, error)
|
||||||
ListClients(count, offset int) ([]*User, error)
|
ListClients(count, offset int) ([]*Client, error)
|
||||||
GetClientByID(id uint) (*User, error)
|
ListClientsForUser(user string, count, offset int) ([]*Client, error)
|
||||||
|
GetClientByID(id uint) (*Client, error)
|
||||||
DeleteClient(id uint) error
|
DeleteClient(id uint) error
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
package models
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
|
||||||
|
|
||||||
// User represents a User of the system which is able to log in
|
|
||||||
type User struct {
|
|
||||||
Model
|
|
||||||
Email string
|
|
||||||
EmailValid bool
|
|
||||||
DisplayName string
|
|
||||||
HashedPassword []byte
|
|
||||||
IsAdmin bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPassword sets the password of an user struct, but does not save it yet
|
|
||||||
func (u *User) SetPassword(password string) error {
|
|
||||||
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
|
|
||||||
// stored password hash, returns error=nil on success.
|
|
||||||
func (u *User) CheckPassword(password string) error {
|
|
||||||
return bcrypt.CompareHashAndPassword(u.HashedPassword, []byte(password))
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserProvider interface {
|
|
||||||
CountUsers() (uint, error)
|
|
||||||
CreateUser(*User) error
|
|
||||||
ListUsers(count, offset int) ([]*User, error)
|
|
||||||
GetUserByID(id uint) (*User, error)
|
|
||||||
GetUserByEmail(email string) (*User, error)
|
|
||||||
DeleteUser(id uint) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type PasswordReset struct {
|
|
||||||
Model
|
|
||||||
User *User
|
|
||||||
UserID uint
|
|
||||||
Token string
|
|
||||||
ValidUntil time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type PasswordResetProvider interface {
|
|
||||||
CreatePasswordReset(*PasswordReset) error
|
|
||||||
GetPasswordResetByToken(token string) (*PasswordReset, error)
|
|
||||||
}
|
|
|
@ -51,34 +51,18 @@ func HandleRoutes(provider *services.Provider) http.Handler {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
r.HandleFunc("/", v("debug"))
|
r.HandleFunc("/", http.RedirectHandler("certs", http.StatusFound).ServeHTTP)
|
||||||
|
|
||||||
r.Route("/register", func(r chi.Router) {
|
|
||||||
r.Get("/", v("register"))
|
|
||||||
r.Post("/", handlers.RegisterHandler(provider))
|
|
||||||
})
|
|
||||||
|
|
||||||
r.Route("/login", func(r chi.Router) {
|
r.Route("/login", func(r chi.Router) {
|
||||||
r.Get("/", v("login"))
|
r.Get("/", handlers.GetLoginHandler(provider))
|
||||||
r.Post("/", handlers.LoginHandler(provider))
|
r.Get("/oauth2/redirect", handlers.OAuth2Endpoint(provider))
|
||||||
})
|
|
||||||
|
|
||||||
r.Post("/confirm-email/{token}", handlers.ConfirmEmailHandler(provider))
|
|
||||||
|
|
||||||
r.Route("/forgot-password", func(r chi.Router) {
|
|
||||||
r.Get("/", v("forgot-password"))
|
|
||||||
r.Post("/", handlers.LoginHandler(provider))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/certs", func(r chi.Router) {
|
r.Route("/certs", func(r chi.Router) {
|
||||||
r.Use(mw.RequireLogin(provider.Sessions))
|
r.Use(mw.RequireLogin(provider.Sessions))
|
||||||
r.Get("/", handlers.ListCertHandler(provider))
|
r.Get("/", handlers.ListClientsHandler(provider))
|
||||||
r.Post("/new", handlers.CreateCertHandler(provider))
|
r.Post("/new", handlers.CreateCertHandler(provider))
|
||||||
r.HandleFunc("/download/{ID}", handlers.DownloadCertHandler(provider))
|
r.HandleFunc("/download/{name}", handlers.DownloadCertHandler(provider))
|
||||||
})
|
|
||||||
|
|
||||||
r.HandleFunc("/500", func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
panic("500")
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -34,7 +34,7 @@ func NewDB(conf *DBConfig) *DB {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate models
|
// Migrate models
|
||||||
db.AutoMigrate(models.User{}, models.Client{})
|
db.AutoMigrate(models.Client{})
|
||||||
db.LogMode(conf.Log)
|
db.LogMode(conf.Log)
|
||||||
|
|
||||||
return &DB{
|
return &DB{
|
||||||
|
@ -43,64 +43,53 @@ func NewDB(conf *DBConfig) *DB {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountUsers returns the number of Users in the datastore
|
// CountClients returns the number of clients in the datastore
|
||||||
func (db *DB) CountUsers() (uint, error) {
|
func (db *DB) CountClients() (uint, error) {
|
||||||
var count uint
|
var count uint
|
||||||
err := db.gorm.Find(&models.User{}).Count(&count).Error
|
err := db.gorm.Find(&models.Client{}).Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser inserts a user into the datastore
|
// CreateClient inserts a client into the datastore
|
||||||
func (db *DB) CreateUser(user *models.User) error {
|
func (db *DB) CreateClient(client *models.Client) error {
|
||||||
err := db.gorm.Create(&user).Error
|
err := db.gorm.Create(&client).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListUsers returns a slice of 'count' users, starting at 'offset'
|
// ListClients returns a slice of 'count' client, starting at 'offset'
|
||||||
func (db *DB) ListUsers(count, offset int) ([]*models.User, error) {
|
func (db *DB) ListClients(count, offset int) ([]*models.Client, error) {
|
||||||
var users = make([]*models.User, 0)
|
var clients = make([]*models.Client, 0)
|
||||||
|
|
||||||
err := db.gorm.Find(&users).Limit(count).Offset(offset).Error
|
err := db.gorm.Find(&clients).Limit(count).Offset(offset).Error
|
||||||
|
|
||||||
return users, err
|
return clients, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByID returns a single user by ID
|
// ListClientsForUser returns a slice of 'count' client for user 'user', starting at 'offset'
|
||||||
func (db *DB) GetUserByID(id uint) (*models.User, error) {
|
func (db *DB) ListClientsForUser(user string, count, offset int) ([]*models.Client, error) {
|
||||||
var user models.User
|
var clients = make([]*models.Client, 0)
|
||||||
err := db.gorm.Where("id = ?", id).First(&user).Error
|
|
||||||
return &user, err
|
err := db.gorm.Find(&clients).Where("user = ?", user).Limit(count).Offset(offset).Error
|
||||||
|
|
||||||
|
return clients, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByEmail returns a single user by email
|
// GetClientByID returns a single client by ID
|
||||||
func (db *DB) GetUserByEmail(email string) (*models.User, error) {
|
func (db *DB) GetClientByID(id uint) (*models.Client, error) {
|
||||||
var user models.User
|
var client models.Client
|
||||||
err := db.gorm.Where("email = ?", email).First(&user).Error
|
err := db.gorm.Where("id = ?", id).First(&client).Error
|
||||||
return &user, err
|
return &client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser removes a user from the datastore
|
// GetClientByNameUser returns a single client by ID
|
||||||
func (db *DB) DeleteUser(id uint) error {
|
func (db *DB) GetClientByNameUser(name, user string) (*models.Client, error) {
|
||||||
var user models.User
|
var client models.Client
|
||||||
err := db.gorm.Where("id = ?", id).Delete(&user).Error
|
err := db.gorm.Where("name = ?", name).Where("user = ?", user).First(&client).Error
|
||||||
return err
|
return &client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreatePasswordReset creates a new password reset token
|
// DeleteClient removes a client from the datastore
|
||||||
func (db *DB) CreatePasswordReset(pwReset *models.PasswordReset) error {
|
func (db *DB) DeleteClient(id uint) error {
|
||||||
err := db.gorm.Create(&pwReset).Error
|
err := db.gorm.Where("id = ?", id).Delete(&models.Client{}).Error
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPasswordResetByToken retrieves a PasswordReset by token
|
|
||||||
func (db *DB) GetPasswordResetByToken(token string) (*models.PasswordReset, error) {
|
|
||||||
var pwReset models.PasswordReset
|
|
||||||
err := db.gorm.Where("token = ?", token).First(&pwReset).Error
|
|
||||||
return &pwReset, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePasswordResetsByUserID deletes all pending password resets for a user
|
|
||||||
func (db *DB) DeletePasswordResetsByUserID(uid uint) error {
|
|
||||||
err := db.gorm.Where("user_id = ?", uid).Delete(&models.PasswordReset{}).Error
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,121 +0,0 @@
|
||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-mail/mail"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrMailUninitializedConfig = errors.New("Mail: uninitialized config")
|
|
||||||
)
|
|
||||||
|
|
||||||
type EmailConfig struct {
|
|
||||||
From string
|
|
||||||
SMTPEnabled bool
|
|
||||||
SMTPServer string
|
|
||||||
SMTPPort int
|
|
||||||
SMTPUsername string
|
|
||||||
SMTPPassword string
|
|
||||||
SMTPTimeout time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type Email struct {
|
|
||||||
config *EmailConfig
|
|
||||||
|
|
||||||
mailChan chan *mail.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewEmail(conf *EmailConfig) *Email {
|
|
||||||
if conf == nil {
|
|
||||||
log.Println(ErrMailUninitializedConfig)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Email{
|
|
||||||
config: conf,
|
|
||||||
mailChan: make(chan *mail.Message, 4),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send sends an email to the receiver
|
|
||||||
func (email *Email) Send(to, subject, text, html string) error {
|
|
||||||
if email.config == nil {
|
|
||||||
log.Print("Error: trying to send mail with uninitialized config.")
|
|
||||||
return ErrMailUninitializedConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
if !email.config.SMTPEnabled {
|
|
||||||
log.Printf("SMTP is disabled in config, printing out email text instead:\nTo: %s\n%s", to, text)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := mail.NewMessage()
|
|
||||||
m.SetHeader("From", email.config.From)
|
|
||||||
m.SetHeader("To", to)
|
|
||||||
m.SetHeader("Subject", subject)
|
|
||||||
m.SetBody("text/plain", text)
|
|
||||||
|
|
||||||
if len(html) > 0 {
|
|
||||||
m.AddAlternative("text/html", html)
|
|
||||||
}
|
|
||||||
|
|
||||||
// put email in chan
|
|
||||||
email.mailChan <- m
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Daemon is a function that takes Mail and sends it without blocking.
|
|
||||||
// WIP
|
|
||||||
func (email *Email) Daemon() {
|
|
||||||
if email.config == nil {
|
|
||||||
log.Print("Error: trying to set up mail deamon with uninitialized config.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !email.config.SMTPEnabled {
|
|
||||||
log.Print("SMTP is disabled in config, emails will be printed instead.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Print("Running mail sending routine")
|
|
||||||
|
|
||||||
d := mail.NewDialer(
|
|
||||||
email.config.SMTPServer,
|
|
||||||
email.config.SMTPPort,
|
|
||||||
email.config.SMTPUsername,
|
|
||||||
email.config.SMTPPassword)
|
|
||||||
|
|
||||||
var s mail.SendCloser
|
|
||||||
var err error
|
|
||||||
open := false
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case m, ok := <-email.mailChan:
|
|
||||||
if !ok {
|
|
||||||
// channel is closed
|
|
||||||
log.Print("Channel closed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !open {
|
|
||||||
if s, err = d.Dial(); err != nil {
|
|
||||||
log.Print(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
open = true
|
|
||||||
}
|
|
||||||
log.Printf("Trying to send mail")
|
|
||||||
if err := mail.Send(s, m); err != nil {
|
|
||||||
log.Printf("Mail: %s", err)
|
|
||||||
}
|
|
||||||
// Close the connection if no email was sent in the last X seconds.
|
|
||||||
case <-time.After(email.config.SMTPTimeout):
|
|
||||||
if open {
|
|
||||||
if err := s.Close(); err != nil {
|
|
||||||
log.Printf("Mail: Failed to close connection: %s", err)
|
|
||||||
}
|
|
||||||
open = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,13 +3,11 @@ package services
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DB *DBConfig
|
DB *DBConfig
|
||||||
Sessions *SessionsConfig
|
Sessions *SessionsConfig
|
||||||
Email *EmailConfig
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
DB *DB
|
DB *DB
|
||||||
Sessions *Sessions
|
Sessions *Sessions
|
||||||
Email *Email
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewProvider returns the ServiceProvider
|
// NewProvider returns the ServiceProvider
|
||||||
|
@ -18,7 +16,6 @@ func NewProvider(conf *Config) *Provider {
|
||||||
|
|
||||||
provider.DB = NewDB(conf.DB)
|
provider.DB = NewDB(conf.DB)
|
||||||
provider.Sessions = NewSessions(conf.Sessions)
|
provider.Sessions = NewSessions(conf.Sessions)
|
||||||
provider.Email = NewEmail(conf.Email)
|
|
||||||
|
|
||||||
return provider
|
return provider
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,7 +49,7 @@ func NewSessions(conf *SessionsConfig) *Sessions {
|
||||||
return &Sessions{store}
|
return &Sessions{store}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Sessions) GetUserEmail(req *http.Request) string {
|
func (store *Sessions) GetUsername(req *http.Request) string {
|
||||||
if store == nil {
|
if store == nil {
|
||||||
// if store was not initialized, all requests fail
|
// if store was not initialized, all requests fail
|
||||||
log.Println("Zero pointer when checking session for username")
|
log.Println("Zero pointer when checking session for username")
|
||||||
|
@ -69,7 +69,7 @@ func (store *Sessions) GetUserEmail(req *http.Request) string {
|
||||||
return email
|
return email
|
||||||
}
|
}
|
||||||
|
|
||||||
func (store *Sessions) SetUserEmail(w http.ResponseWriter, req *http.Request, email string) {
|
func (store *Sessions) SetUsername(w http.ResponseWriter, req *http.Request, username string) {
|
||||||
if store == nil {
|
if store == nil {
|
||||||
// if store was not initialized, do nothing
|
// if store was not initialized, do nothing
|
||||||
return
|
return
|
||||||
|
@ -80,7 +80,7 @@ func (store *Sessions) SetUserEmail(w http.ResponseWriter, req *http.Request, em
|
||||||
// renew token to avoid session pinning/fixation attack
|
// renew token to avoid session pinning/fixation attack
|
||||||
sess.RenewToken(w)
|
sess.RenewToken(w)
|
||||||
|
|
||||||
sess.PutString(w, UserEmailKey, email)
|
sess.PutString(w, UserEmailKey, username)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,11 @@ func LoadTemplates() {
|
||||||
"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"),
|
"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"),
|
"client_list": newTemplate("layouts/application.gohtml", "views/client_list.gohtml"),
|
||||||
"cert_list": newTemplate("layouts/application.gohtml", "shared/header.gohtml", "shared/footer.gohtml", "views/cert_list.gohtml"),
|
|
||||||
|
"config.ovpn": newTemplate("files/config.ovpn"),
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ func NewWithSession(req *http.Request, sessionStore *services.Sessions) *View {
|
||||||
"Env": "develop",
|
"Env": "develop",
|
||||||
},
|
},
|
||||||
"flashes": []services.Flash{},
|
"flashes": []services.Flash{},
|
||||||
"username": sessionStore.GetUserEmail(req),
|
"username": sessionStore.GetUsername(req),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue