simple api
This commit is contained in:
parent
963fd34724
commit
6cf998997e
106
admin.go
106
admin.go
@ -3,9 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
@ -13,24 +10,11 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AdminConfig struct {
|
||||
Users []*User `json:"users"`
|
||||
Secrets *SecretsConfig `json:"secrets"`
|
||||
Addr string `json:"addr"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type SecretsConfig struct {
|
||||
PasswordPepper string `json:"password_pepper"`
|
||||
ContextKey string `json:"context_key"`
|
||||
ContextExpiration int `json:"context_expiration"`
|
||||
ScryptN int `json:"scrypt_n"`
|
||||
ScryptR int `json:"scrypt_r"`
|
||||
ScryptP int `json:"scrypt_p"`
|
||||
Addr string `json:"addr"`
|
||||
}
|
||||
|
||||
//go:embed assets
|
||||
@ -41,44 +25,12 @@ func NewAdmin() *AdminConfig {
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
a := &AdminConfig{
|
||||
Addr: "0.0.0.0:8080",
|
||||
Secrets: NewSecrets(),
|
||||
Users: make([]*User, 0),
|
||||
URL: "https://backup.example.com/",
|
||||
Addr: "0.0.0.0:8080",
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func NewSecrets() *SecretsConfig {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
pepper, _ := password.Generate(20, 5, 0, false, false)
|
||||
ctx, _ := password.Generate(20, 5, 0, false, false)
|
||||
|
||||
return &SecretsConfig{
|
||||
PasswordPepper: pepper,
|
||||
ContextKey: ctx,
|
||||
ContextExpiration: 3600,
|
||||
ScryptN: 32768,
|
||||
ScryptR: 8,
|
||||
ScryptP: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AdminConfig) NewAdminUser() {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
p, _ := password.Generate(20, 5, 0, false, false)
|
||||
u, _ := NewUser("admin", p)
|
||||
|
||||
a.Users = append(a.Users, u)
|
||||
|
||||
log.WithFields(log.Fields{}).Warnf("Admin user password : %s", p)
|
||||
}
|
||||
|
||||
func (a *AdminConfig) Run() {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
@ -93,46 +45,12 @@ func (a *AdminConfig) Run() {
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
if t, err := template.ParseFS(assets, "assets/templates/*.html"); err != nil {
|
||||
log.WithFields(log.Fields{"call": "template.ParseFS", "error": err}).Errorf("")
|
||||
return
|
||||
} else {
|
||||
r.SetHTMLTemplate(t)
|
||||
}
|
||||
r.GET("/run", ApiRun)
|
||||
r.GET("/run/:app", ApiRunApp)
|
||||
|
||||
r.GET("/", HttpAnyIndex)
|
||||
r.POST("/", HttpAnyIndex)
|
||||
r.GET("/save", ApiSave)
|
||||
|
||||
r.GET("/run", func(c *gin.Context) {
|
||||
cfg.Run()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "done",
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/save", func(c *gin.Context) {
|
||||
if err := cfg.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": "error",
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "done",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
fsys, _ := fs.Sub(assets, "assets/static")
|
||||
r.StaticFS("/assets", http.FS(fsys))
|
||||
|
||||
protected := r.Group("p", HttpAuth())
|
||||
protected.GET("test", HttpAnyHome)
|
||||
protected.GET("home", HttpAnyHome)
|
||||
|
||||
unprotected := r.Group("u", HttpNoAuth())
|
||||
unprotected.GET("signin", HttpGetSignIn)
|
||||
unprotected.POST("submit", HttpPostSubmit)
|
||||
unprotected.GET("recover", HttpGetRecover)
|
||||
r.GET("/config", ApiConfig)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: a.Addr,
|
||||
@ -168,15 +86,3 @@ func (a *AdminConfig) Run() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func FindUserID(user string) (uint64, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
for _, v := range cfg.Admin.Users {
|
||||
if v.Username == user {
|
||||
return v.ID, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no user")
|
||||
}
|
||||
|
50
api.go
Normal file
50
api.go
Normal file
@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ApiRun(c *gin.Context) {
|
||||
cfg.Run()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "done",
|
||||
})
|
||||
}
|
||||
|
||||
func ApiRunApp(c *gin.Context) {
|
||||
if _, ok := cfg.apps[c.Param("app")]; ok {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "done",
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": "error",
|
||||
"error": "no app found",
|
||||
})
|
||||
}
|
||||
|
||||
func ApiSave(c *gin.Context) {
|
||||
if err := cfg.Save(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": "error",
|
||||
"error": err,
|
||||
})
|
||||
} else {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "done",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func ApiConfig(c *gin.Context) {
|
||||
if b, err := cfg.Pretty(); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"message": "error",
|
||||
"error": err,
|
||||
})
|
||||
} else {
|
||||
c.Data(http.StatusOK, "application/json", b)
|
||||
}
|
||||
}
|
5002
assets/static/css/bootstrap-grid.css
vendored
5002
assets/static/css/bootstrap-grid.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.min.css
vendored
7
assets/static/css/bootstrap-grid.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
426
assets/static/css/bootstrap-reboot.css
vendored
426
assets/static/css/bootstrap-reboot.css
vendored
@ -1,426 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr /* rtl:ignore */;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
File diff suppressed because one or more lines are too long
8
assets/static/css/bootstrap-reboot.min.css
vendored
8
assets/static/css/bootstrap-reboot.min.css
vendored
@ -1,8 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
File diff suppressed because one or more lines are too long
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
@ -1,423 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr ;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
File diff suppressed because one or more lines are too long
@ -1,8 +0,0 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
|
File diff suppressed because one or more lines are too long
4752
assets/static/css/bootstrap-utilities.css
vendored
4752
assets/static/css/bootstrap-utilities.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10837
assets/static/css/bootstrap.css
vendored
10837
assets/static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.min.css
vendored
7
assets/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10813
assets/static/css/bootstrap.rtl.css
vendored
10813
assets/static/css/bootstrap.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.rtl.min.css
vendored
7
assets/static/css/bootstrap.rtl.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6748
assets/static/js/bootstrap.bundle.js
vendored
6748
assets/static/js/bootstrap.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.bundle.min.js
vendored
7
assets/static/js/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4967
assets/static/js/bootstrap.esm.js
vendored
4967
assets/static/js/bootstrap.esm.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.esm.min.js
vendored
7
assets/static/js/bootstrap.esm.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5016
assets/static/js/bootstrap.js
vendored
5016
assets/static/js/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.min.js
vendored
7
assets/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,17 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<title>Home :: zBackup</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="container">
|
||||
<h1 class="h3 mb-3 font-weight-normal">Logged in.</h1>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,24 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<title>Recover :: zBackup</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="container">
|
||||
<form class="form-signin" method="POST" action="/u/submit">
|
||||
<h1 class="h3 mb-3 font-weight-normal">Recover password</h1>
|
||||
<div class="form-group">
|
||||
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" >Recover</button>
|
||||
<p class="mt-5 mb-3 text-muted">© 2023-2024</p>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,29 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<title>Login :: zBackup</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="container">
|
||||
<form class="form-signin" method="POST" action="/u/submit">
|
||||
<h1 class="h3 mb-3 font-weight-normal">Sign in</h1>
|
||||
{{if (ne .Error "")}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
|
||||
<div class="form-group">
|
||||
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" id="password" placeholder="Password" name="password" required>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" >Sign in</button>
|
||||
<p class="mt-5 mb-3 text-muted">© 2023-2024</p>
|
||||
</form>
|
||||
<a href="/u/recover">Lost password</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -49,6 +49,7 @@ func main() {
|
||||
} else if c, err := LoadConfigFile("backup.json"); err == nil {
|
||||
cfg = c
|
||||
} else {
|
||||
log.Debugf("loading default config")
|
||||
cfg, _ = LoadConfigByte(sampleCfg)
|
||||
}
|
||||
|
||||
@ -56,12 +57,6 @@ func main() {
|
||||
if cfg.Admin == nil {
|
||||
cfg.Admin = NewAdmin()
|
||||
}
|
||||
if cfg.Admin.Secrets == nil {
|
||||
cfg.Admin.Secrets = NewSecrets()
|
||||
}
|
||||
if len(cfg.Admin.Users) == 0 {
|
||||
cfg.Admin.NewAdminUser()
|
||||
}
|
||||
cfg.Admin.Run()
|
||||
} else {
|
||||
cfg.Run()
|
||||
|
57
config.go
57
config.go
@ -52,6 +52,13 @@ type AppConfig struct {
|
||||
Active bool `json:"active,omitempty"`
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
Active bool `json:"active"`
|
||||
SmtpHost string `json:"smtp,omitempty"`
|
||||
FromEmail string `json:"email_from,omitempty"`
|
||||
ToEmail []string `json:"email_to,omitempty"`
|
||||
}
|
||||
|
||||
// Load config from file
|
||||
func LoadConfigFile(path string) (*Config, error) {
|
||||
log.WithFields(log.Fields{"path": path}).Debugf("starting")
|
||||
@ -96,22 +103,24 @@ func LoadConfigByte(conf []byte) (*Config, error) {
|
||||
}
|
||||
|
||||
if c.Email != nil {
|
||||
if len(c.Email.SmtpHost) == 0 {
|
||||
err := fmt.Errorf("no smtp")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
if c.Email.Active {
|
||||
if len(c.Email.SmtpHost) == 0 {
|
||||
err := fmt.Errorf("no smtp")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Email.FromEmail) == 0 {
|
||||
err := fmt.Errorf("no email from")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
if len(c.Email.FromEmail) == 0 {
|
||||
err := fmt.Errorf("no email from")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Email.ToEmail) == 0 {
|
||||
err := fmt.Errorf("no email to")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
if len(c.Email.ToEmail) == 0 {
|
||||
err := fmt.Errorf("no email to")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -180,8 +189,8 @@ func LoadConfigByte(conf []byte) (*Config, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Save config
|
||||
func (c *Config) Save() error {
|
||||
// Pretty config
|
||||
func (c *Config) Pretty() ([]byte, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
@ -191,10 +200,22 @@ func (c *Config) Save() error {
|
||||
b, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err, "call": "json.Marshal"}).Errorf("")
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := pretty.PrettyOptions(b, &pretty.Options{Indent: " "})
|
||||
return pretty.PrettyOptions(b, &pretty.Options{Indent: " "}), nil
|
||||
}
|
||||
|
||||
// Save config
|
||||
func (c *Config) Save() error {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
r, err := cfg.Pretty()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"error": err, "call": "cfg.Pretty"}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Create(*cfgFile)
|
||||
if err != nil {
|
||||
|
11
email.go
11
email.go
@ -16,12 +16,6 @@ type Email struct {
|
||||
items []string
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
SmtpHost string `json:"smtp"`
|
||||
FromEmail string `json:"email_from"`
|
||||
ToEmail []string `json:"email_to"`
|
||||
}
|
||||
|
||||
func NewEmail(now time.Time) *Email {
|
||||
log.WithFields(log.Fields{"now": now}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"now": now}).Debugf("done")
|
||||
@ -32,8 +26,9 @@ func NewEmail(now time.Time) *Email {
|
||||
func (e *Email) AddItem(item string) {
|
||||
log.WithFields(log.Fields{"item": item}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"item": item}).Debugf("done")
|
||||
|
||||
e.items = append(e.items, item)
|
||||
if cfg.Email.Active {
|
||||
e.items = append(e.items, item)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Email) Send(addr, from string, to []string) error {
|
||||
|
148
http.go
148
http.go
@ -1,148 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func HttpAuth() gin.HandlerFunc {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
return func(c *gin.Context) {
|
||||
// Search signed-in userID
|
||||
userID := 0
|
||||
if userID == 0 {
|
||||
// Return 404 and abort handlers chain.
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HttpNoAuth() gin.HandlerFunc {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
return func(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func HttpPostSubmit(c *gin.Context) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
t, err := GetCSRFToken(c)
|
||||
|
||||
if err != nil {
|
||||
c.String(http.StatusBadRequest, "bad token")
|
||||
return
|
||||
}
|
||||
|
||||
if !t.Valid() {
|
||||
c.String(http.StatusBadRequest, "expired token")
|
||||
return
|
||||
}
|
||||
|
||||
switch t.GetPath() {
|
||||
case "signin":
|
||||
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("submit signin")
|
||||
HttpSubmitSignIn(c)
|
||||
HttpAnyIndex(c)
|
||||
default:
|
||||
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("submit %s", t.GetPath())
|
||||
c.String(http.StatusBadRequest, "")
|
||||
}
|
||||
|
||||
if GetWebSessionUserID(c) > 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
|
||||
} else {
|
||||
SetCSRFToken(c)
|
||||
warning, _ := c.Cookie("warning")
|
||||
c.SetCookie("warning", "", -1, "/", cfg.Admin.Addr, false, true)
|
||||
c.HTML(http.StatusOK, "page-signin.html", gin.H{
|
||||
"Error": warning,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func HttpGetRecover(c *gin.Context) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
SetCSRFToken(c)
|
||||
c.HTML(http.StatusOK, "page-recover.html", gin.H{})
|
||||
}
|
||||
|
||||
func HttpGetSignIn(c *gin.Context) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
SetCSRFToken(c)
|
||||
c.HTML(http.StatusOK, "page-signin.html", gin.H{})
|
||||
}
|
||||
|
||||
func HttpAnyIndex(c *gin.Context) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if GetWebSessionUserID(c) > 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
|
||||
}
|
||||
}
|
||||
|
||||
func HttpAnyHome(c *gin.Context) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if GetWebSessionUserID(c) == 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
|
||||
} else {
|
||||
SetCSRFToken(c)
|
||||
c.HTML(http.StatusOK, "page-home.html", gin.H{})
|
||||
}
|
||||
}
|
||||
|
||||
func GetWebSessionUserID(c *gin.Context) uint64 {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func HttpSubmitSignIn(c *gin.Context) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
c.SetCookie("warning", "Unable to parse form", 0, "/", cfg.Admin.URL, false, true)
|
||||
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("")
|
||||
return
|
||||
}
|
||||
|
||||
username := c.Request.FormValue("username")
|
||||
password := c.Request.FormValue("password")
|
||||
|
||||
userID, err := FindUserID(username)
|
||||
if err != nil {
|
||||
c.SetCookie("warning", "Invalid user or password", 0, "/", cfg.Admin.URL, false, true)
|
||||
log.WithFields(log.Fields{"call": "FindUserID", "attr": username, "err": err}).Debugf("")
|
||||
return
|
||||
}
|
||||
|
||||
if !VerifyUserPassword(userID, password) {
|
||||
c.SetCookie("warning", "Invalid user or password", 0, "/", cfg.Admin.URL, false, true)
|
||||
log.WithFields(log.Fields{"call": "VerifyUserPassword", "attr": "***"}).Debugf("auth not ok")
|
||||
return
|
||||
}
|
||||
|
||||
t := NewSessionToken(userID)
|
||||
|
||||
c.SetCookie("session", t.Encode(), 9999999999, "/", cfg.Admin.URL, false, true)
|
||||
c.SetCookie("warning", "", -1, "/", cfg.Admin.URL, false, true)
|
||||
}
|
298
http_tokens.go
298
http_tokens.go
@ -1,298 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var sessionMap map[string]uint64
|
||||
|
||||
type SessionToken struct {
|
||||
Identifier [32]byte
|
||||
Verifier [32]byte
|
||||
}
|
||||
|
||||
type CSRFToken struct {
|
||||
Seed [4]byte
|
||||
Path [16]byte
|
||||
Time int64
|
||||
Checksum uint32
|
||||
}
|
||||
|
||||
func NewSessionToken(userID uint64) *SessionToken {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
identifier := make([]byte, 32)
|
||||
_, err := rand.Read(identifier)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"call": "rand.Read", "attr": "identifier", "err": err}).Debugf("")
|
||||
return &SessionToken{}
|
||||
}
|
||||
|
||||
verifier := make([]byte, 32)
|
||||
_, err = rand.Read(verifier)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"call": "rand.Read", "attr": "verifier", "err": err}).Debugf("")
|
||||
return &SessionToken{}
|
||||
}
|
||||
|
||||
hash := hmac.New(sha256.New, verifier)
|
||||
hash.Write(verifier)
|
||||
hashVerifier := hash.Sum(nil)
|
||||
|
||||
t := SessionToken{}
|
||||
|
||||
copy(t.Verifier[:], verifier[:32])
|
||||
copy(t.Identifier[:], hashVerifier[:32])
|
||||
|
||||
return &t
|
||||
|
||||
}
|
||||
|
||||
func (t *SessionToken) Bytes() []byte {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_ = binary.Write(buf, binary.LittleEndian, t)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (t *SessionToken) Load(b []byte) error {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if len(b) != binary.Size(t) {
|
||||
return fmt.Errorf("wrong size")
|
||||
}
|
||||
|
||||
r := bytes.NewReader(b)
|
||||
|
||||
err := binary.Read(r, binary.LittleEndian, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *SessionToken) GetSessionID() uint64 {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if sessionID, ok := sessionMap[string(t.Identifier[:])]; !ok {
|
||||
return 0
|
||||
} else {
|
||||
return sessionID
|
||||
}
|
||||
}
|
||||
|
||||
func GetSessionTokenParam(c *gin.Context) (*SessionToken, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
err := c.Request.ParseForm()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
session := c.Param("key")
|
||||
|
||||
return DecodeSessionToken(session)
|
||||
}
|
||||
|
||||
func GetSessionTokenCookie(c *gin.Context) (*SessionToken, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if session, err := c.Cookie("session"); err != nil {
|
||||
log.WithFields(log.Fields{"call": "Context.Cookie", "attr": "session", "err": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
return DecodeSessionToken(session)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SessionToken) Encode() string {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
return base64.StdEncoding.EncodeToString(t.Bytes())
|
||||
}
|
||||
|
||||
func DecodeSessionToken(s string) (*SessionToken, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"call": "base64.StdEncoding.DecodeString", "attr": "session", "err": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := SessionToken{}
|
||||
err = t.Load(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (t *CSRFToken) GetPath() string {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
for _, b := range t.Path {
|
||||
if b > 0 {
|
||||
buf.WriteByte(b)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (t *CSRFToken) Bytes() []byte {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_ = binary.Write(buf, binary.LittleEndian, t)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (t *CSRFToken) Load(b []byte) error {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if len(b) != binary.Size(t) {
|
||||
return fmt.Errorf("wrong size")
|
||||
}
|
||||
r := bytes.NewReader(b)
|
||||
err := binary.Read(r, binary.LittleEndian, t)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"call": "binary.Read", "attr": "b", "err": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
chk := t.Checksum
|
||||
t.Checksum = 0
|
||||
if crc32.ChecksumIEEE(t.Bytes()) != chk {
|
||||
return fmt.Errorf("wrong checksum")
|
||||
}
|
||||
t.Checksum = chk
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewCSRFToken(c *gin.Context) *CSRFToken {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
t := CSRFToken{}
|
||||
|
||||
copy(t.Path[:], c.Request.URL.Path[0:])
|
||||
|
||||
t.Time = time.Now().UTC().Unix()
|
||||
|
||||
_, err := rand.Read(t.Seed[:])
|
||||
log.WithFields(log.Fields{"call": "rand.Read", "attr": "seed", "err": err}).Errorf("")
|
||||
|
||||
t.Checksum = crc32.ChecksumIEEE(t.Bytes())
|
||||
|
||||
return &t
|
||||
}
|
||||
|
||||
func GetCSRFToken(c *gin.Context) (*CSRFToken, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if context, err := c.Cookie("context"); err != nil {
|
||||
return nil, fmt.Errorf("no context param")
|
||||
} else {
|
||||
return DecodeCSRFToken(context)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CSRFToken) Encode() string {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(cfg.Admin.Secrets.ContextKey))
|
||||
key := hash.Sum(nil)
|
||||
|
||||
block, _ := aes.NewCipher(key)
|
||||
ciphertext := make([]byte, 32) // size of CSRFToken
|
||||
if binary.Size(t) != 32 {
|
||||
log.WithFields(log.Fields{"err": fmt.Errorf("size is wrong")}).Fatalf("")
|
||||
}
|
||||
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
mode.CryptBlocks(ciphertext[:], t.Bytes())
|
||||
|
||||
return base64.StdEncoding.EncodeToString(ciphertext)
|
||||
}
|
||||
|
||||
func DecodeCSRFToken(s string) (*CSRFToken, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(b) != binary.Size(CSRFToken{}) {
|
||||
return nil, fmt.Errorf("wrong size")
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(cfg.Admin.Secrets.ContextKey))
|
||||
key := hash.Sum(nil)
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
|
||||
iv := make([]byte, aes.BlockSize)
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
mode.CryptBlocks(b, b)
|
||||
|
||||
t := CSRFToken{}
|
||||
err = t.Load(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (t *CSRFToken) Valid() bool {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if time.Now().UTC().Sub(time.Unix(t.Time, 0)) > time.Duration(cfg.Admin.Secrets.ContextExpiration)*time.Second {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func SetCSRFToken(c *gin.Context) {
|
||||
c.SetCookie("context", NewCSRFToken(c).Encode(), cfg.Admin.Secrets.ContextExpiration, "/", cfg.Admin.URL, false, true)
|
||||
return
|
||||
}
|
102
user.go
102
user.go
@ -1,102 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Salt string `json:"salt"`
|
||||
Password string `json:"passwd"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
func NewUser(name, passwd string) (*User, error) {
|
||||
log.WithFields(log.Fields{"name": name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": name}).Debugf("done")
|
||||
|
||||
userID := uint64(1)
|
||||
for _, v := range cfg.Admin.Users {
|
||||
userID = max(v.ID+1, userID)
|
||||
if v.Username == name {
|
||||
err := errors.New("user already exists")
|
||||
log.WithFields(log.Fields{"name": name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
u := &User{
|
||||
ID: userID,
|
||||
Username: name,
|
||||
}
|
||||
|
||||
salt := make([]byte, 32)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "call": "rand.Read", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
u.Salt = hex.EncodeToString(salt)
|
||||
|
||||
if pass, err := u.HashPassword(passwd); err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "call": "HashPassword", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
u.Password = hex.EncodeToString(pass)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (u *User) HashPassword(passwd string) ([]byte, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
//peppering the pass
|
||||
hash := hmac.New(sha256.New, []byte(cfg.Admin.Secrets.PasswordPepper))
|
||||
hash.Write([]byte(passwd))
|
||||
hashPass := hash.Sum(nil)
|
||||
|
||||
//salting the hash
|
||||
salt := make([]byte, 32)
|
||||
if _, err := hex.Decode(salt, []byte(u.Salt)); err != nil {
|
||||
log.WithFields(log.Fields{"call": "hex.Decode", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h, err := scrypt.Key(hashPass, salt, cfg.Admin.Secrets.ScryptN, cfg.Admin.Secrets.ScryptR, cfg.Admin.Secrets.ScryptP, 32); err != nil {
|
||||
log.WithFields(log.Fields{"call": "scrypt.Key", "error": err}).Errorf("")
|
||||
return h, err
|
||||
} else {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func VerifyUserPassword(userID uint64, clearPass string) bool {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
var u *User
|
||||
|
||||
for _, v := range cfg.Admin.Users {
|
||||
if v.ID == userID {
|
||||
u = v
|
||||
}
|
||||
}
|
||||
|
||||
hashPass, err := u.HashPassword(clearPass)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"call": "user.HashPassword", "attr": "***", "error": err}).Errorf("")
|
||||
return false
|
||||
}
|
||||
|
||||
return hex.EncodeToString(hashPass) == u.Password
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user