Refactor code, delete old references
This commit is contained in:
parent
71e830d52f
commit
9ef61b19bb
16 changed files with 435 additions and 223 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,7 @@
|
||||||
*_vfsdata.go
|
*_vfsdata.go
|
||||||
certman
|
certman
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
*.crt
|
||||||
|
*.key
|
||||||
|
clients.json
|
||||||
|
.env
|
||||||
|
|
|
@ -45,15 +45,15 @@ compile:
|
||||||
|
|
||||||
# build binaries -- list of supported plattforms is here:
|
# build binaries -- list of supported plattforms is here:
|
||||||
# https://stackoverflow.com/a/20728862
|
# https://stackoverflow.com/a/20728862
|
||||||
- GOOS=linux GOARCH=amd64 go build -o $CI_PROJECT_DIR/certman
|
- GOOS=linux GOARCH=amd64 go build -tags "netgo" -o $CI_PROJECT_DIR/certman
|
||||||
#- GOOS=linux GOARCH=arm GOARM=6 go build -o $CI_PROJECT_DIR/certman.arm
|
- GOOS=linux GOARCH=arm GOARM=6 go build -tags "netgo" -o $CI_PROJECT_DIR/certman.arm
|
||||||
#- GOOS=windows GOARCH=amd64 go build -o $CI_PROJECT_DIR/certman.exe
|
- GOOS=windows GOARCH=amd64 go build -tags "netgo" -o $CI_PROJECT_DIR/certman.exe
|
||||||
artifacts:
|
artifacts:
|
||||||
expire_in: "8 hrs"
|
expire_in: "8 hrs"
|
||||||
paths:
|
paths:
|
||||||
- certman
|
- certman
|
||||||
# - certman.arm
|
- certman.arm
|
||||||
# - certman.exe
|
- certman.exe
|
||||||
|
|
||||||
minify:
|
minify:
|
||||||
stage: release
|
stage: release
|
||||||
|
@ -65,11 +65,23 @@ minify:
|
||||||
name: znly/upx:latest
|
name: znly/upx:latest
|
||||||
entrypoint: ["/bin/sh", "-c"]
|
entrypoint: ["/bin/sh", "-c"]
|
||||||
script:
|
script:
|
||||||
- upx --best --brute $CI_PROJECT_DIR/certman certman.arm certman.exe
|
- upx --best --brute $CI_PROJECT_DIR/certman $CI_PROJECT_DIR/certman.arm $CI_PROJECT_DIR/certman.exe
|
||||||
artifacts:
|
artifacts:
|
||||||
paths:
|
paths:
|
||||||
- certman
|
- certman
|
||||||
#- certman.arm
|
- certman.arm
|
||||||
#- certman.exe
|
- certman.exe
|
||||||
|
only:
|
||||||
|
- tags
|
||||||
|
|
||||||
|
build_image:
|
||||||
|
stage: release
|
||||||
|
tags:
|
||||||
|
- dind
|
||||||
|
image: "docker:latest"
|
||||||
|
script:
|
||||||
|
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||||
|
- docker build -t $CI_REGISTRY_IMAGE:${CI_COMMIT_REF_NAME#v} .
|
||||||
|
- docker push $CI_REGISTRY_IMAGE:${CI_COMMIT_REF_NAME#v}
|
||||||
only:
|
only:
|
||||||
- tags
|
- tags
|
21
Dockerfile
Normal file
21
Dockerfile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
FROM golang:1.9
|
||||||
|
|
||||||
|
WORKDIR /go/src/git.klink.asia/paul/certman
|
||||||
|
ADD . .
|
||||||
|
RUN \
|
||||||
|
go get github.com/shurcooL/vfsgen/cmd/vfsgendev && \
|
||||||
|
go generate git.klink.asia/paul/certman/assets && \
|
||||||
|
go get -v git.klink.asia/paul/certman && \
|
||||||
|
go build -tags netgo
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
ENV \
|
||||||
|
OAUTH2_CLIENT_ID="" \
|
||||||
|
OAUTH2_CLIENT_SECRET="" \
|
||||||
|
APP_KEY="" \
|
||||||
|
OAUTH2_AUTH_URL="https://gitlab.example.com/oauth/authorize" \
|
||||||
|
OAUTH2_TOKEN_URL="https://gitlab.example.com/oauth/token" \
|
||||||
|
USER_ENDPOINT="https://gitlab.example.com/api/v4/user" \
|
||||||
|
OAUTH2_REDIRECT_URL="https://certman.example.com/login/oauth2/redirect"
|
||||||
|
COPY --from=0 /go/src/git.klink.asia/paul/certman/certman /
|
||||||
|
ENTRYPOINT ["/certman"]
|
|
@ -1,4 +1,4 @@
|
||||||
{{ define "base" }}
|
{{ define "base" }}# Client configuration for {{ .User }}@{{ .Name }}
|
||||||
client
|
client
|
||||||
dev tun
|
dev tun
|
||||||
proto udp
|
proto udp
|
||||||
|
@ -45,12 +45,10 @@ Yo95ZQ==
|
||||||
</ca>
|
</ca>
|
||||||
|
|
||||||
<cert>
|
<cert>
|
||||||
{{ .Cert | html }}
|
{{ .Cert | html }}</cert>
|
||||||
</cert>
|
|
||||||
|
|
||||||
<key>
|
<key>
|
||||||
{{ .Key | html }}
|
{{ .Key | html }}</key>
|
||||||
</key>
|
|
||||||
|
|
||||||
<tls-auth>
|
<tls-auth>
|
||||||
#
|
#
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{{ define "meta" }}
|
{{ define "meta" }}
|
||||||
<title>Log in</title>
|
<title>Certificate List</title>
|
||||||
{{ end}}
|
{{ end}}
|
||||||
|
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h1>Certificates</h1>
|
<h1 class="title">Certificates for {{ .username }}:</h1>
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -36,20 +36,23 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{{ range .Clients }}
|
{{ range .Clients }}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="is-vcentered"><p>{{ $.username }}@{{ .Name }}</p></td>
|
<td class="is-vcentered"><p>{{ .User }}@{{ .Name }}</p></td>
|
||||||
<td><time title="{{ .CreatedAt.UTC }}">{{ .CreatedAt | humanDate }}</time></td>
|
<td><time title="{{ .CreatedAt.UTC }}">{{ .CreatedAt | humanDate }}</time></td>
|
||||||
<td>
|
<td>
|
||||||
<div class="field has-addons">
|
<div class="field has-addons">
|
||||||
<p class="control is-marginless is-expanded">
|
<p class="control is-marginless is-expanded">
|
||||||
<a href="/certs/download/{{ .Name }}" class="button is-primary is-fullwidth">Download</a>
|
<a href="/certs/download/{{ .Name }}" class="button is-primary is-fullwidth">Download</a>
|
||||||
</p>
|
</p>
|
||||||
<p class="control is-marginless">
|
<div class="control is-marginless">
|
||||||
<a class="button is-danger">
|
<form action="/certs/delete/{{ .Name }}" method="POST">
|
||||||
|
{{ $.csrfField }}
|
||||||
|
<button class="button is-danger" type="submit">
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</button>
|
||||||
</p>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
37
handlers/README.md
Normal file
37
handlers/README.md
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Certman
|
||||||
|
Certman is a simple certificate manager web service for OpenVPN.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
### Binary
|
||||||
|
There are prebuilt binary files for this application. They are statically
|
||||||
|
linked and have no additional dependencies. Supported plattforms are:
|
||||||
|
* Windows (XP and up)
|
||||||
|
* Linux (2.6.16 and up)
|
||||||
|
* Linux ARM (for raspberry pi, 3.0 and up)
|
||||||
|
Simply download them from the "artifacts" section of this project.
|
||||||
|
### Docker
|
||||||
|
A prebuilt docker image (10MB) is available:
|
||||||
|
```bash
|
||||||
|
docker pull docker.klink.asia/paul/certman
|
||||||
|
```
|
||||||
|
### From Source-Docker
|
||||||
|
You can easily build your own docker image from source
|
||||||
|
```bash
|
||||||
|
docker build -t docker.klink.asia/paul/certman .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
Certman assumes the root certificates of the VPN CA are located in the same
|
||||||
|
directory as the binary, If that is not the case you need to copy over the
|
||||||
|
`ca.crt` and `ca.key` files before you are able to generate certificates
|
||||||
|
with this tool.
|
||||||
|
|
||||||
|
Additionally, the project is configured by the following environment
|
||||||
|
variables:
|
||||||
|
* `OAUTH2_CLIENT_ID` the Client ID, assigned during client registration
|
||||||
|
* `OAUTH2_CLIENT_SECRET` the Client secret, assigned during client registration
|
||||||
|
* `OAUTH2_AUTH_URL` the URL to the "/authorize" endpoint of the identity provider
|
||||||
|
* `OAUTH2_TOKEN_URL` the URL to the "/token" endpoint of the identity provider
|
||||||
|
* `OAUTH2_REDIRECT_URL` the redirect URL used by the app, usually the hostname suffixed by "/login/oauth2/redirect"
|
||||||
|
* `USER_ENDPOINT` the URL to the Identity provider user endpoint, for gitlab this is "/api/v4/user". The "username" attribute of the returned JSON will used for authentication.
|
||||||
|
* `APP_KEY` random ASCII string, 32 characters in length. Used for cookie generation.
|
|
@ -12,25 +12,14 @@ import (
|
||||||
"git.klink.asia/paul/certman/services"
|
"git.klink.asia/paul/certman/services"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GitlabConfig = &oauth2.Config{
|
func OAuth2Endpoint(p *services.Provider, config *oauth2.Config) http.HandlerFunc {
|
||||||
ClientID: os.Getenv("OAUTH2_CLIENT_ID"),
|
|
||||||
ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"),
|
|
||||||
Scopes: []string{"read_user"},
|
|
||||||
RedirectURL: os.Getenv("HOST") + "/login/oauth2/redirect",
|
|
||||||
Endpoint: oauth2.Endpoint{
|
|
||||||
AuthURL: os.Getenv("OAUTH2_AUTH_URL"),
|
|
||||||
TokenURL: os.Getenv("OAUTH2_TOKEN_URL"),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func OAuth2Endpoint(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)
|
||||||
|
|
||||||
code := req.FormValue("code")
|
code := req.FormValue("code")
|
||||||
|
|
||||||
// exchange code for token
|
// exchange code for token
|
||||||
accessToken, err := GitlabConfig.Exchange(oauth2.NoContext, code)
|
accessToken, err := config.Exchange(oauth2.NoContext, code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
http.NotFound(w, req)
|
http.NotFound(w, req)
|
||||||
|
@ -39,9 +28,9 @@ func OAuth2Endpoint(p *services.Provider) http.HandlerFunc {
|
||||||
|
|
||||||
if accessToken.Valid() {
|
if accessToken.Valid() {
|
||||||
// generate a client using the access token
|
// generate a client using the access token
|
||||||
httpClient := GitlabConfig.Client(oauth2.NoContext, accessToken)
|
httpClient := config.Client(oauth2.NoContext, accessToken)
|
||||||
|
|
||||||
apiRequest, err := http.NewRequest("GET", "https://git.klink.asia/api/v4/user", nil)
|
apiRequest, err := http.NewRequest("GET", os.Getenv("USER_ENDPOINT"), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.RenderError(w, http.StatusNotFound)
|
v.RenderError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -78,9 +67,9 @@ func OAuth2Endpoint(p *services.Provider) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLoginHandler(p *services.Provider) http.HandlerFunc {
|
func GetLoginHandler(p *services.Provider, config *oauth2.Config) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
authURL := GitlabConfig.AuthCodeURL("", oauth2.AccessTypeOnline)
|
authURL := config.AuthCodeURL("", oauth2.AccessTypeOnline)
|
||||||
http.Redirect(w, req, authURL, http.StatusFound)
|
http.Redirect(w, req, authURL, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,10 +8,12 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/models"
|
"git.klink.asia/paul/certman/models"
|
||||||
|
@ -27,7 +29,7 @@ func ListClientsHandler(p *services.Provider) http.HandlerFunc {
|
||||||
|
|
||||||
username := p.Sessions.GetUsername(req)
|
username := p.Sessions.GetUsername(req)
|
||||||
|
|
||||||
clients, _ := p.DB.ListClientsForUser(username, 100, 0)
|
clients, _ := p.ClientCollection.ListClientsForUser(username)
|
||||||
|
|
||||||
v.Vars["Clients"] = clients
|
v.Vars["Clients"] = clients
|
||||||
v.Render(w, "client_list")
|
v.Render(w, "client_list")
|
||||||
|
@ -39,7 +41,8 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
username := p.Sessions.GetUsername(req)
|
username := p.Sessions.GetUsername(req)
|
||||||
certname := req.FormValue("certname")
|
certname := req.FormValue("certname")
|
||||||
|
|
||||||
if !IsByteLength(certname, 2, 64) || !IsAlphanumeric(certname) {
|
// Validate certificate Name
|
||||||
|
if !IsByteLength(certname, 2, 64) || !IsDNSName(certname) {
|
||||||
p.Sessions.Flash(w, req,
|
p.Sessions.Flash(w, req,
|
||||||
services.Flash{
|
services.Flash{
|
||||||
Type: "danger",
|
Type: "danger",
|
||||||
|
@ -50,6 +53,10 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lowercase the certificate name, to avoid problems with the case
|
||||||
|
// insensitive matching inside OpenVPN
|
||||||
|
certname = strings.ToLower(certname)
|
||||||
|
|
||||||
// Load CA master certificate
|
// Load CA master certificate
|
||||||
caCert, caKey, err := loadX509KeyPair("ca.crt", "ca.key")
|
caCert, caKey, err := loadX509KeyPair("ca.crt", "ca.key")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -78,13 +85,14 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
// Initialize new client config
|
// Initialize new client config
|
||||||
client := models.Client{
|
client := models.Client{
|
||||||
Name: certname,
|
Name: certname,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
PrivateKey: x509.MarshalPKCS1PrivateKey(key),
|
PrivateKey: x509.MarshalPKCS1PrivateKey(key),
|
||||||
Cert: derBytes,
|
Cert: derBytes,
|
||||||
User: username,
|
User: username,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert client into database
|
// Insert client into database
|
||||||
if err := p.DB.CreateClient(&client); err != nil {
|
if err := p.ClientCollection.CreateClient(&client); err != nil {
|
||||||
log.Println(err.Error())
|
log.Println(err.Error())
|
||||||
p.Sessions.Flash(w, req,
|
p.Sessions.Flash(w, req,
|
||||||
services.Flash{
|
services.Flash{
|
||||||
|
@ -107,6 +115,40 @@ func CreateCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
|
v := views.New(req)
|
||||||
|
// detemine own username
|
||||||
|
username := p.Sessions.GetUsername(req)
|
||||||
|
name := chi.URLParam(req, "name")
|
||||||
|
|
||||||
|
client, err := p.ClientCollection.GetClientByNameUser(name, username)
|
||||||
|
if err != nil {
|
||||||
|
v.RenderError(w, http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.ClientCollection.DeleteClient(client.ID)
|
||||||
|
if err != nil {
|
||||||
|
p.Sessions.Flash(w, req,
|
||||||
|
services.Flash{
|
||||||
|
Type: "danger",
|
||||||
|
Message: "Failed to delete certificate",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Sessions.Flash(w, req,
|
||||||
|
services.Flash{
|
||||||
|
Type: "success",
|
||||||
|
Message: template.HTML(fmt.Sprintf("Successfully deleted client <strong>%s</strong>", client.Name)),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
http.Redirect(w, req, "/certs", http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
@ -114,7 +156,7 @@ func DownloadCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
username := p.Sessions.GetUsername(req)
|
username := p.Sessions.GetUsername(req)
|
||||||
name := chi.URLParam(req, "name")
|
name := chi.URLParam(req, "name")
|
||||||
|
|
||||||
client, err := p.DB.GetClientByNameUser(name, username)
|
client, err := p.ClientCollection.GetClientByNameUser(name, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
v.RenderError(w, http.StatusNotFound)
|
v.RenderError(w, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
|
@ -143,7 +185,6 @@ func DownloadCertHandler(p *services.Provider) http.HandlerFunc {
|
||||||
w.Header().Set("Content-Type", "application/x-openvpn-profile")
|
w.Header().Set("Content-Type", "application/x-openvpn-profile")
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename=\"config.ovpn\"")
|
w.Header().Set("Content-Disposition", "attachment; filename=\"config.ovpn\"")
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
log.Println(vars)
|
|
||||||
t.Execute(w, vars)
|
t.Execute(w, vars)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -159,10 +200,8 @@ func loadX509KeyPair(certFile, keyFile string) (*x509.Certificate, *rsa.PrivateK
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
cpb, cr := pem.Decode(cf)
|
cpb, _ := pem.Decode(cf)
|
||||||
fmt.Println(string(cr))
|
kpb, _ := pem.Decode(kf)
|
||||||
kpb, kr := pem.Decode(kf)
|
|
||||||
fmt.Println(string(kr))
|
|
||||||
crt, err := x509.ParseCertificate(cpb.Bytes)
|
crt, err := x509.ParseCertificate(cpb.Bytes)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -191,11 +230,10 @@ func CreateCertificate(commonName string, key interface{}, caCert *x509.Certific
|
||||||
SerialNumber: serialNumber,
|
SerialNumber: serialNumber,
|
||||||
Subject: subj,
|
Subject: subj,
|
||||||
|
|
||||||
NotBefore: time.Now(),
|
NotBefore: time.Now().Add(-5 * time.Minute), // account for clock shift
|
||||||
NotAfter: time.Now().Add(24 * time.Hour * 356 * 5),
|
NotAfter: time.Now().Add(24 * time.Hour * 356 * 5), // 5 years ought to be enough!
|
||||||
|
|
||||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||||
//KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageDataEncipherment,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
|
||||||
BasicConstraintsValid: true,
|
BasicConstraintsValid: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import "regexp"
|
import (
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
const (
|
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}])))\\.?$"
|
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}])))\\.?$"
|
||||||
|
@ -118,3 +122,17 @@ func IsNull(str string) bool {
|
||||||
func IsByteLength(str string, min, max int) bool {
|
func IsByteLength(str string, min, max int) bool {
|
||||||
return len(str) >= min && len(str) <= max
|
return len(str) >= min && len(str) <= max
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsDNSName will validate the given string as a DNS name
|
||||||
|
func IsDNSName(str string) bool {
|
||||||
|
if str == "" || len(strings.Replace(str, ".", "", -1)) > 255 {
|
||||||
|
// constraints already violated
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !IsIP(str) && rxDNSName.MatchString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIP checks if a string is either IP version 4 or 6.
|
||||||
|
func IsIP(str string) bool {
|
||||||
|
return net.ParseIP(str) != nil
|
||||||
|
}
|
||||||
|
|
32
main.go
32
main.go
|
@ -1,43 +1,53 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/services"
|
"git.klink.asia/paul/certman/services"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/router"
|
"git.klink.asia/paul/certman/router"
|
||||||
"git.klink.asia/paul/certman/views"
|
"git.klink.asia/paul/certman/views"
|
||||||
|
|
||||||
// import sqlite3 driver
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
log.Println("Initializing certman")
|
||||||
|
if err := checkCAFilesExist(); err != nil {
|
||||||
|
log.Fatalf("Could not read CA files: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
c := services.Config{
|
c := services.Config{
|
||||||
DB: &services.DBConfig{
|
CollectionPath: "./clients.json",
|
||||||
Type: "sqlite3",
|
|
||||||
DSN: "db.sqlite3",
|
|
||||||
Log: true,
|
|
||||||
},
|
|
||||||
Sessions: &services.SessionsConfig{
|
Sessions: &services.SessionsConfig{
|
||||||
SessionName: "_session",
|
SessionName: "_session",
|
||||||
CookieKey: string(securecookie.GenerateRandomKey(32)),
|
CookieKey: os.Getenv("APP_KEY"),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Lifetime: 24 * time.Hour,
|
Lifetime: 24 * time.Hour,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Println(".. services")
|
||||||
serviceProvider := services.NewProvider(&c)
|
serviceProvider := services.NewProvider(&c)
|
||||||
|
|
||||||
// load and parse template files
|
// load and parse template files
|
||||||
|
log.Println(".. templates")
|
||||||
views.LoadTemplates()
|
views.LoadTemplates()
|
||||||
|
|
||||||
mux := router.HandleRoutes(serviceProvider)
|
mux := router.HandleRoutes(serviceProvider)
|
||||||
|
|
||||||
|
log.Println(".. server")
|
||||||
err := http.ListenAndServe(":8000", mux)
|
err := http.ListenAndServe(":8000", mux)
|
||||||
log.Fatalf(err.Error())
|
log.Fatalf(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkCAFilesExist() error {
|
||||||
|
for _, filename := range []string{"ca.crt", "ca.key"} {
|
||||||
|
if _, err := os.Stat(filename); os.IsNotExist(err) {
|
||||||
|
return errors.New(filename + " not readable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -11,20 +11,12 @@ 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client represent the OpenVPN client configuration
|
// Client represent the OpenVPN client configuration
|
||||||
type Client struct {
|
type Client struct {
|
||||||
Model
|
ID uint
|
||||||
Name string `gorm:"index;unique_index:idx_name_user"`
|
CreatedAt time.Time
|
||||||
User string `gorm:"index;unique_index:idx_name_user"`
|
Name string
|
||||||
|
User string
|
||||||
Cert []byte
|
Cert []byte
|
||||||
PrivateKey []byte
|
PrivateKey []byte
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/services"
|
"git.klink.asia/paul/certman/services"
|
||||||
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/assets"
|
"git.klink.asia/paul/certman/assets"
|
||||||
"git.klink.asia/paul/certman/handlers"
|
"git.klink.asia/paul/certman/handlers"
|
||||||
|
@ -35,6 +36,18 @@ func HandleRoutes(provider *services.Provider) http.Handler {
|
||||||
mux.Use(mw.Recoverer) // recover on panic
|
mux.Use(mw.Recoverer) // recover on panic
|
||||||
mux.Use(provider.Sessions.Manager.Use) // use session storage
|
mux.Use(provider.Sessions.Manager.Use) // use session storage
|
||||||
|
|
||||||
|
// TODO: move this code away from here
|
||||||
|
oauth2Config := &oauth2.Config{
|
||||||
|
ClientID: os.Getenv("OAUTH2_CLIENT_ID"),
|
||||||
|
ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"),
|
||||||
|
Scopes: []string{"read_user"},
|
||||||
|
RedirectURL: os.Getenv("OAUTH2_REDIRECT_URL"),
|
||||||
|
Endpoint: oauth2.Endpoint{
|
||||||
|
AuthURL: os.Getenv("OAUTH2_AUTH_URL"),
|
||||||
|
TokenURL: os.Getenv("OAUTH2_TOKEN_URL"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
// this either means we use the embedded files, or live-load
|
||||||
// from the file system (if `--tags="dev"` is used).
|
// from the file system (if `--tags="dev"` is used).
|
||||||
|
@ -54,8 +67,8 @@ func HandleRoutes(provider *services.Provider) http.Handler {
|
||||||
r.HandleFunc("/", http.RedirectHandler("certs", http.StatusFound).ServeHTTP)
|
r.HandleFunc("/", http.RedirectHandler("certs", http.StatusFound).ServeHTTP)
|
||||||
|
|
||||||
r.Route("/login", func(r chi.Router) {
|
r.Route("/login", func(r chi.Router) {
|
||||||
r.Get("/", handlers.GetLoginHandler(provider))
|
r.Get("/", handlers.GetLoginHandler(provider, oauth2Config))
|
||||||
r.Get("/oauth2/redirect", handlers.OAuth2Endpoint(provider))
|
r.Get("/oauth2/redirect", handlers.OAuth2Endpoint(provider, oauth2Config))
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Route("/certs", func(r chi.Router) {
|
r.Route("/certs", func(r chi.Router) {
|
||||||
|
@ -63,7 +76,10 @@ func HandleRoutes(provider *services.Provider) http.Handler {
|
||||||
r.Get("/", handlers.ListClientsHandler(provider))
|
r.Get("/", handlers.ListClientsHandler(provider))
|
||||||
r.Post("/new", handlers.CreateCertHandler(provider))
|
r.Post("/new", handlers.CreateCertHandler(provider))
|
||||||
r.HandleFunc("/download/{name}", handlers.DownloadCertHandler(provider))
|
r.HandleFunc("/download/{name}", handlers.DownloadCertHandler(provider))
|
||||||
|
r.Post("/delete/{name}", handlers.DeleteCertHandler(provider))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
r.Get("/unconfigured-backend", handlers.NotFoundHandler)
|
||||||
})
|
})
|
||||||
|
|
||||||
// what should happen if no route matches
|
// what should happen if no route matches
|
||||||
|
|
169
services/clientstore.go
Normal file
169
services/clientstore.go
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.klink.asia/paul/certman/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrNilCertificate = errors.New("Trying to store nil certificate")
|
||||||
|
ErrDuplicate = errors.New("Client with that name already exists")
|
||||||
|
ErrUserNotExists = errors.New("User does not exist")
|
||||||
|
ErrClientNotExists = errors.New("Client does not exist")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientCollection struct {
|
||||||
|
sync.RWMutex
|
||||||
|
path string
|
||||||
|
|
||||||
|
Clients map[uint]*models.Client
|
||||||
|
UserIndex map[string]map[string]uint
|
||||||
|
LastID uint
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientCollection(path string) *ClientCollection {
|
||||||
|
// empty collection
|
||||||
|
var clientCollection = ClientCollection{
|
||||||
|
path: path,
|
||||||
|
Clients: make(map[uint]*models.Client),
|
||||||
|
UserIndex: make(map[string]map[string]uint),
|
||||||
|
LastID: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := ioutil.ReadFile(path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return &clientCollection
|
||||||
|
} else if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return &clientCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(raw, &clientCollection); err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &clientCollection
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateClient inserts a client into the datastore
|
||||||
|
func (db *ClientCollection) CreateClient(client *models.Client) error {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
return ErrNilCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
db.LastID++ // increment Id
|
||||||
|
client.ID = db.LastID
|
||||||
|
|
||||||
|
userIndex, exists := db.UserIndex[client.User]
|
||||||
|
if !exists {
|
||||||
|
// create user index if not exists
|
||||||
|
db.UserIndex[client.User] = make(map[string]uint)
|
||||||
|
userIndex = db.UserIndex[client.User]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists = userIndex[client.Name]; exists {
|
||||||
|
return ErrDuplicate
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all went well, add client and set the index
|
||||||
|
db.Clients[client.ID] = client
|
||||||
|
userIndex[client.Name] = client.ID
|
||||||
|
db.UserIndex[client.User] = userIndex
|
||||||
|
|
||||||
|
return db.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListClientsForUser returns a slice of 'count' client for user 'user', starting at 'offset'
|
||||||
|
func (db *ClientCollection) ListClientsForUser(user string) ([]*models.Client, error) {
|
||||||
|
db.RLock()
|
||||||
|
defer db.RUnlock()
|
||||||
|
|
||||||
|
var clients = make([]*models.Client, 0)
|
||||||
|
|
||||||
|
userIndex, exists := db.UserIndex[user]
|
||||||
|
if !exists {
|
||||||
|
return nil, errors.New("user does not exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, clientID := range userIndex {
|
||||||
|
clients = append(clients, db.Clients[clientID])
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientByID returns a single client by ID
|
||||||
|
func (db *ClientCollection) GetClientByID(id uint) (*models.Client, error) {
|
||||||
|
|
||||||
|
client, exists := db.Clients[id]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrClientNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClientByNameUser returns a single client by ID
|
||||||
|
func (db *ClientCollection) GetClientByNameUser(name, user string) (*models.Client, error) {
|
||||||
|
db.RLock()
|
||||||
|
defer db.RUnlock()
|
||||||
|
|
||||||
|
userIndex, exists := db.UserIndex[user]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrUserNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
clientID, exists := userIndex[name]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrClientNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
client, exists := db.Clients[clientID]
|
||||||
|
if !exists {
|
||||||
|
return nil, ErrClientNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteClient removes a client from the datastore
|
||||||
|
func (db *ClientCollection) DeleteClient(id uint) error {
|
||||||
|
db.Lock()
|
||||||
|
defer db.Unlock()
|
||||||
|
|
||||||
|
client, exists := db.Clients[id]
|
||||||
|
if !exists {
|
||||||
|
return nil // nothing to delete
|
||||||
|
}
|
||||||
|
|
||||||
|
userIndex, exists := db.UserIndex[client.User]
|
||||||
|
if !exists {
|
||||||
|
return ErrUserNotExists
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(userIndex, client.Name) // delete client index
|
||||||
|
|
||||||
|
// if index is now empty, delete the user entry
|
||||||
|
if len(userIndex) == 0 {
|
||||||
|
delete(db.UserIndex, client.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally delete the client
|
||||||
|
delete(db.Clients, id)
|
||||||
|
|
||||||
|
return db.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientCollection) save() error {
|
||||||
|
collectionJSON, _ := json.Marshal(c)
|
||||||
|
return ioutil.WriteFile(c.path, collectionJSON, 0600)
|
||||||
|
}
|
|
@ -1,95 +0,0 @@
|
||||||
package services
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
|
|
||||||
"git.klink.asia/paul/certman/models"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Error Definitions
|
|
||||||
var (
|
|
||||||
ErrNotImplemented = errors.New("Not implemented")
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBConfig struct {
|
|
||||||
Type string
|
|
||||||
DSN string
|
|
||||||
Log bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DB is a wrapper around gorm.DB to provide custom methods
|
|
||||||
type DB struct {
|
|
||||||
gorm *gorm.DB
|
|
||||||
|
|
||||||
conf *DBConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewDB(conf *DBConfig) *DB {
|
|
||||||
// Establish connection
|
|
||||||
db, err := gorm.Open(conf.Type, conf.DSN)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Could not open database: %s", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate models
|
|
||||||
db.AutoMigrate(models.Client{})
|
|
||||||
db.LogMode(conf.Log)
|
|
||||||
|
|
||||||
return &DB{
|
|
||||||
gorm: db,
|
|
||||||
conf: conf,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountClients returns the number of clients in the datastore
|
|
||||||
func (db *DB) CountClients() (uint, error) {
|
|
||||||
var count uint
|
|
||||||
err := db.gorm.Find(&models.Client{}).Count(&count).Error
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateClient inserts a client into the datastore
|
|
||||||
func (db *DB) CreateClient(client *models.Client) error {
|
|
||||||
err := db.gorm.Create(&client).Error
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListClients returns a slice of 'count' client, starting at 'offset'
|
|
||||||
func (db *DB) ListClients(count, offset int) ([]*models.Client, error) {
|
|
||||||
var clients = make([]*models.Client, 0)
|
|
||||||
|
|
||||||
err := db.gorm.Find(&clients).Limit(count).Offset(offset).Error
|
|
||||||
|
|
||||||
return clients, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListClientsForUser returns a slice of 'count' client for user 'user', starting at 'offset'
|
|
||||||
func (db *DB) ListClientsForUser(user string, count, offset int) ([]*models.Client, error) {
|
|
||||||
var clients = make([]*models.Client, 0)
|
|
||||||
|
|
||||||
err := db.gorm.Find(&clients).Where("user = ?", user).Limit(count).Offset(offset).Error
|
|
||||||
|
|
||||||
return clients, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClientByID returns a single client by ID
|
|
||||||
func (db *DB) GetClientByID(id uint) (*models.Client, error) {
|
|
||||||
var client models.Client
|
|
||||||
err := db.gorm.Where("id = ?", id).First(&client).Error
|
|
||||||
return &client, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClientByNameUser returns a single client by ID
|
|
||||||
func (db *DB) GetClientByNameUser(name, user string) (*models.Client, error) {
|
|
||||||
var client models.Client
|
|
||||||
err := db.gorm.Where("name = ?", name).Where("user = ?", user).First(&client).Error
|
|
||||||
return &client, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteClient removes a client from the datastore
|
|
||||||
func (db *DB) DeleteClient(id uint) error {
|
|
||||||
err := db.gorm.Where("id = ?", id).Delete(&models.Client{}).Error
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
package services
|
package services
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DB *DBConfig
|
CollectionPath string
|
||||||
Sessions *SessionsConfig
|
Sessions *SessionsConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type Provider struct {
|
type Provider struct {
|
||||||
DB *DB
|
ClientCollection *ClientCollection
|
||||||
Sessions *Sessions
|
Sessions *Sessions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ type Provider struct {
|
||||||
func NewProvider(conf *Config) *Provider {
|
func NewProvider(conf *Config) *Provider {
|
||||||
var provider = &Provider{}
|
var provider = &Provider{}
|
||||||
|
|
||||||
provider.DB = NewDB(conf.DB)
|
provider.ClientCollection = NewClientCollection(conf.CollectionPath)
|
||||||
provider.Sessions = NewSessions(conf.Sessions)
|
provider.Sessions = NewSessions(conf.Sessions)
|
||||||
|
|
||||||
return provider
|
return provider
|
||||||
|
|
|
@ -52,7 +52,7 @@ func NewSessions(conf *SessionsConfig) *Sessions {
|
||||||
func (store *Sessions) GetUsername(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("Nil pointer when checking session for username")
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue