db changes

pull/429/head
Hunter Long 2020-03-06 01:33:46 -08:00
parent 66ec613953
commit 7dc285295b
36 changed files with 221 additions and 126 deletions

View File

@ -45,6 +45,12 @@ stop:
logs:
docker logs statping --follow
db-up:
docker-compose -f dev/docker-compose.db.yml up -d --remove-orphans
db-down:
docker-compose -f dev/docker-compose.full.yml down --remove-orphans
console:
docker exec -t -i statping /bin/sh

View File

@ -104,17 +104,22 @@ type Database interface {
Since(time.Time) Database
Between(time.Time, time.Time) Database
SelectByTime(string) string
SelectByTime(time.Duration) string
MultipleSelects(args ...string) Database
FormatTime(t time.Time) string
ParseTime(t string) (time.Time, error)
DbType() string
}
func DB() Database {
return database
}
func (it *Db) DbType() string {
return it.Database.Dialect().GetName()
}
func Close() error {
if database == nil {
return nil
@ -145,11 +150,21 @@ func Available() bool {
return true
}
func AmountGreaterThan1000(db *gorm.DB) *gorm.DB {
return db.Where("service = ?", 1000)
}
func (it *Db) MultipleSelects(args ...string) Database {
joined := strings.Join(args, ", ")
return it.Select(joined)
}
func OrderStatus(status []string) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Scopes(AmountGreaterThan1000).Where("status IN (?)", status)
}
}
type Db struct {
Database *gorm.DB
Type string

View File

@ -28,7 +28,7 @@ func (b By) String() string {
type GroupQuery struct {
Start time.Time
End time.Time
Group string
Group time.Duration
Order string
Limit int
Offset int
@ -47,8 +47,15 @@ func (b GroupQuery) Database() Database {
var (
ByCount = By("COUNT(id) as amount")
ByAverage = func(column string) By {
return By(fmt.Sprintf("AVG(%s) as amount", column))
ByAverage = func(column string, multiplier int) By {
switch database.DbType() {
case "mysql":
return By(fmt.Sprintf("CAST(AVG(%s)*%d as UNSIGNED) as amount", column, multiplier))
case "postgres":
return By(fmt.Sprintf("cast(AVG(%s)*%d as int) as amount", column, multiplier))
default:
return By(fmt.Sprintf("cast(AVG(%s)*%d as int) as amount", column, multiplier))
}
}
)
@ -76,7 +83,7 @@ func (g *GroupQuery) toFloatRows() []*TimeValue {
fmt.Println("float rows: ", timeframe, amount)
newTs := types.FixedTime(timeframe, g.duration())
newTs := types.FixedTime(timeframe, g.Group)
data = append(data, &TimeValue{
Timeframe: newTs,
Amount: amount,
@ -91,7 +98,7 @@ func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
dbQuery := g.db.MultipleSelects(
g.db.SelectByTime(g.Group),
by.String(),
).Group("timeframe")
).Group("timeframe").Debug()
g.db = dbQuery
@ -119,7 +126,7 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
log.Error(err, timeframe)
}
trueTime, _ := g.db.ParseTime(timeframe)
newTs := types.FixedTime(trueTime, g.duration())
newTs := types.FixedTime(trueTime, g.Group)
data = append(data, &TimeValue{
Timeframe: newTs,
Amount: amount,
@ -131,12 +138,12 @@ func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
timeMap := make(map[string]float64)
var validSet []*TimeValue
dur := t.g.duration()
dur := t.g.Group
for _, v := range t.data {
timeMap[v.Timeframe] = v.Amount
}
currentStr := types.FixedTime(current, t.g.duration())
currentStr := types.FixedTime(current, t.g.Group)
for {
var amount float64
@ -151,31 +158,12 @@ func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
break
}
current = current.Add(dur)
currentStr = types.FixedTime(current, t.g.duration())
currentStr = types.FixedTime(current, t.g.Group)
}
return validSet, nil
}
func (g *GroupQuery) duration() time.Duration {
switch g.Group {
case "second":
return types.Second
case "minute":
return types.Minute
case "hour":
return types.Hour
case "day":
return types.Day
case "month":
return types.Month
case "year":
return types.Year
default:
return types.Hour
}
}
type isObject interface {
Db() Database
}
@ -183,9 +171,6 @@ type isObject interface {
func ParseQueries(r *http.Request, o isObject) *GroupQuery {
fields := parseGet(r)
grouping := fields.Get("group")
if grouping == "" {
grouping = "hour"
}
startField := utils.ToInt(fields.Get("start"))
endField := utils.ToInt(fields.Get("end"))
limit := utils.ToInt(fields.Get("limit"))
@ -198,10 +183,19 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
db := o.Db()
if grouping == "" {
grouping = "1h"
}
groupDur, err := time.ParseDuration(grouping)
if err != nil {
log.Errorln(err)
groupDur = 1 * time.Hour
}
query := &GroupQuery{
Start: time.Unix(startField, 0).UTC(),
End: time.Unix(endField, 0).UTC(),
Group: grouping,
Group: groupDur,
Order: orderBy,
Limit: int(limit),
Offset: int(offset),
@ -210,11 +204,14 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
}
if startField == 0 {
query.Start = time.Now().Add(-3 * types.Month).UTC()
query.Start = time.Now().Add(-7 * types.Day).UTC()
}
if endField == 0 {
query.End = time.Now().UTC()
}
if query.End.After(utils.Now()) {
query.End = utils.Now()
}
if query.Limit != 0 {
db = db.Limit(query.Limit)

View File

@ -30,14 +30,15 @@ func (it *Db) FormatTime(t time.Time) string {
}
}
func (it *Db) SelectByTime(increment string) string {
func (it *Db) SelectByTime(increment time.Duration) string {
seconds := int(increment.Seconds())
switch it.Type {
case "mysql":
return fmt.Sprintf("CONCAT(date_format(created_at, '%s')) AS timeframe", it.correctTimestamp(increment))
return fmt.Sprintf("FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(created_at) / %d) * %d) AS timeframe", seconds, seconds)
case "postgres":
return fmt.Sprintf("date_trunc('%s', created_at) AS timeframe", increment)
default:
return fmt.Sprintf("strftime('%s', created_at, 'utc') as timeframe", it.correctTimestamp(increment))
return fmt.Sprintf("datetime((strftime('%%s', created_at) / %d) * %d, 'unixepoch') as timeframe", seconds, seconds)
}
}

View File

@ -29,7 +29,7 @@ services:
MYSQL_ROOT_PASSWORD: password123
MYSQL_DATABASE: statping
MYSQL_USER: root
MYSQL_PASSWORD: password
MYSQL_PASSWORD: password123
ports:
- 3306:3306
healthcheck:

6
dev/prometheus.yml vendored
View File

@ -3,6 +3,12 @@ global:
evaluation_interval: 15s
scrape_configs:
- job_name: 'statping_local'
scrape_interval: 15s
bearer_token: 'samplesecret'
static_configs:
- targets: ['docker0:8585']
- job_name: 'statping'
scrape_interval: 15s
bearer_token: 'exampleapisecret'

View File

@ -36,12 +36,12 @@ class Api {
return axios.post('/api/services/' + data.id, data).then(response => (response.data))
}
async service_hits(id, start, end, group) {
return axios.get('/api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=true').then(response => (response.data))
async service_hits(id, start, end, group, fill=true) {
return axios.get('/api/services/' + id + '/hits_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_failures_data(id, start, end, group) {
return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=true').then(response => (response.data))
async service_failures_data(id, start, end, group, fill=true) {
return axios.get('/api/services/' + id + '/failure_data?start=' + start + '&end=' + end + '&group=' + group + '&fill=' + fill).then(response => (response.data))
}
async service_heatmap(id, start, end, group) {

View File

@ -539,4 +539,26 @@ input.inputTags-field:focus {
cursor: pointer;
}
.list-group-item {
min-height: 85pt;
}
.index_container {
min-height: 980pt;
}
/* Enter and leave animations can use different */
/* durations and timing functions. */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}
@import 'mobile';

View File

@ -17,7 +17,8 @@
.container {
padding: 0px !important;
padding-top: 15px !important;
padding-top: 4vh !important;
min-height: 960pt;
}
.group_header {
@ -29,6 +30,9 @@
margin-top: 0px;
width: 100%;
margin-bottom: 0;
box-shadow: 1px 10px 20px 0px;
height: 55pt;
color: rgba(0, 0, 0, 0.20);
}
.btn-sm {

View File

@ -1,7 +1,7 @@
<template>
<div>
<div class="col-12">
<h1 class="text-black-50">Messages</h1>
<h3 class="text-black-50">Messages</h3>
<table class="table table-striped">
<thead>
<tr>

View File

@ -1,11 +1,11 @@
<template>
<div>
<div class="col-12">
<h1 class="text-black-50">Services
<h3 class="text-black-50">Services
<router-link to="/dashboard/create_service" class="btn btn-outline-success mt-1 float-right">
<font-awesome-icon icon="plus"/> Create
</router-link>
</h1>
</h3>
<ServicesList/>

View File

@ -1,12 +1,12 @@
<template>
<div class="col-12">
<h1 class="text-black-50">Users</h1>
<h3 class="text-black-50">Users</h3>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">Type</th>
<th scope="col">Last Login</th>
<th scope="col" class="d-none d-md-table-cell">Last Login</th>
<th scope="col"></th>
</tr>
</thead>
@ -16,7 +16,7 @@
<td>{{user.username}}</td>
<td v-if="user.admin"><span class="badge badge-danger">ADMIN</span></td>
<td v-if="!user.admin"><span class="badge badge-primary">USER</span></td>
<td>{{toLocal(user.updated_at)}}</td>
<td class="d-none d-md-table-cell">{{toLocal(user.updated_at)}}</td>
<td class="text-right">
<div class="btn-group">
<a @click.prevent="editUser(user, edit)" href="" class="btn btn-outline-secondary"><font-awesome-icon icon="user" /> Edit</a>

View File

@ -35,7 +35,7 @@ export default {
methods: {
async lastDaysFailures() {
const start = this.nowSubtract(86400 * 30)
this.failureData = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(this.now()), "day")
this.failureData = await Api.service_failures_data(this.service.id, this.toUnix(start), this.toUnix(this.now()), "24h")
}
}
}

View File

@ -126,7 +126,7 @@
visible: function(newVal, oldVal) {
if (newVal && !this.showing) {
this.showing = true
this.chartHits("hour")
this.chartHits("1h")
}
}
},
@ -135,8 +135,8 @@
const start = this.nowSubtract((3600 * 24) * 30)
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), group)
if (this.data.length === 0 && group !== "hour") {
await this.chartHits("hour")
if (this.data.length === 0 && group !== "1h") {
await this.chartHits("30m")
}
this.series = [{
name: this.service.name,

View File

@ -64,7 +64,7 @@
methods: {
async chartHeatmap() {
const start = this.nowSubtract((3600 * 24) * 7)
const data = await Api.service_heatmap(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
const data = await Api.service_heatmap(this.service.id, this.toUnix(start), this.toUnix(new Date()), "1h")
let dataArr = []
data.forEach(function(d) {

View File

@ -49,9 +49,9 @@
}
},
async mounted() {
this.set1 = await this.getHits(24, "hour")
this.set1 = await this.getHits(24, "1h")
this.set1_name = this.calc(this.set1)
this.set2 = await this.getHits(24 * 7, "day")
this.set2 = await this.getHits(24 * 7, "24h")
this.set2_name = this.calc(this.set2)
this.loaded = true
},

View File

@ -1,9 +1,9 @@
<template>
<div class="col-12">
<h1 class="text-black-50 mt-5">
<h3 class="text-black-50 mt-5">
{{message.id ? `Update ${message.title}` : "Create Message"}}
<button @click="removeEdit" v-if="message.id" class="mt-3 btn float-right btn-danger btn-sm">Close</button>
</h1>
</h3>
<div class="card">
<div class="card-body">
<form @submit="saveMessage">

View File

@ -1,9 +1,9 @@
<template>
<div>
<h1 class="text-black-50 mt-5">
<h3 class="text-black-50 mt-5">
{{user.id ? `Update ${user.username}` : "Create User"}}
<button @click.prevent="removeEdit" v-if="user.id" class="mt-3 btn float-right btn-danger btn-sm">Close</button></h1>
<button @click.prevent="removeEdit" v-if="user.id" class="mt-3 btn float-right btn-danger btn-sm">Close</button></h3>
<div class="card">
<div class="card-body">

View File

@ -1,5 +1,5 @@
<template>
<div class="container col-md-7 col-sm-12 mt-4 sm-container">
<div class="container col-md-7 col-sm-12 mt-4 sm-container index_container">
<Header/>

View File

@ -250,7 +250,7 @@ export default {
const start = this.nowSubtract((3600 * 24) * 7)
this.start_time = start
this.end_time = new Date()
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), "hour")
this.data = await Api.service_hits(this.service.id, this.toUnix(start), this.toUnix(new Date()), "1h")
this.series = [{
name: this.service.name,
...this.data

View File

@ -187,7 +187,7 @@ func IsAdmin(r *http.Request) bool {
if !core.App.Setup {
return false
}
if os.Getenv("GO_ENV") == "test" {
if utils.Getenv("GO_ENV", false).(bool) {
return true
}
claim, err := getJwtToken(r)

View File

@ -128,7 +128,7 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
groupQuery := database.ParseQueries(r, service.AllHits())
objs, err := groupQuery.GraphData(database.ByAverage("latency"))
objs, err := groupQuery.GraphData(database.ByAverage("latency", 1000))
if err != nil {
sendErrorJson(err, w, r)
return
@ -164,7 +164,7 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
groupQuery := database.ParseQueries(r, service.AllHits())
objs, err := groupQuery.GraphData(database.ByAverage("ping_time"))
objs, err := groupQuery.GraphData(database.ByAverage("ping_time", 1000))
if err != nil {
sendErrorJson(err, w, r)
return

View File

@ -90,6 +90,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(err, w, r)
return
}
sendErrorJson(err, w, r)
return
}

View File

@ -104,7 +104,7 @@ func InitialSetup(configs *DbConfig) error {
admin := &users.User{
Username: username,
Password: utils.HashPassword(password),
Password: password,
Email: "info@admin.com",
Admin: null.NewNullBool(true),
}

View File

@ -14,6 +14,10 @@ import (
"github.com/hunterlong/statping/types/notifications"
"github.com/hunterlong/statping/types/services"
"github.com/hunterlong/statping/types/users"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
// InsertNotifierDB inject the Statping database instance to the Notifier package

View File

@ -6,6 +6,10 @@ import (
"github.com/prometheus/common/log"
"sync"
"time"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
func Samples() {

View File

@ -18,6 +18,18 @@ func (h Hitters) Db() database.Database {
return h.db
}
func (h Hitters) First() *Hit {
var hit Hit
h.db.Order("id ASC").Limit(1).Find(&hit)
return &hit
}
func (h Hitters) Last() *Hit {
var hit Hit
h.db.Order("id DESC").Limit(1).Find(&hit)
return &hit
}
func (h Hitters) Since(t time.Time) []*Hit {
var hits []*Hit
h.db.Since(t).Find(&hits)
@ -30,9 +42,9 @@ func (h Hitters) List() []*Hit {
return hits
}
func (h Hitters) Last(amount int) []*Hit {
func (h Hitters) LastCount(amounts int) []*Hit {
var hits []*Hit
h.db.Limit(amount).Find(&hits)
h.db.Order("id asc").Limit(amounts).Find(&hits)
return hits
}

View File

@ -1,10 +1,16 @@
package hits
import (
"fmt"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"sync"
"time"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/postgres"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
var SampleHits = 9900.
@ -13,29 +19,38 @@ func Samples() {
tx := DB().Begin()
sg := new(sync.WaitGroup)
createdAt := time.Now().Add(-1 * types.Month)
for i := int64(1); i <= 4; i++ {
sg.Add(1)
p := utils.NewPerlin(2, 2, 5, time.Now().UnixNano())
go func() {
defer sg.Done()
for hi := 0.; hi <= SampleHits; hi++ {
latency := p.Noise1D(hi / 500)
createdAt = createdAt.Add(1 * time.Minute)
hit := &Hit{
Service: i,
CreatedAt: createdAt.UTC(),
Latency: latency,
}
tx = tx.Create(&hit)
}
}()
err := createHitsAt(tx, i, sg)
if err != nil {
log.Error(err)
}
tx = DB().Begin()
}
sg.Wait()
if err := tx.Commit().Error(); err != nil {
log.Error(err)
}
}
func createHitsAt(db database.Database, serviceID int64, sg *sync.WaitGroup) error {
createdAt := utils.Now().Add(-30 * types.Day)
p := utils.NewPerlin(2, 2, 5, utils.Now().UnixNano())
i := 0
for hi := 0.; hi <= SampleHits; hi++ {
latency := p.Noise1D(hi / 500)
createdAt = createdAt.Add(10 * time.Minute)
hit := &Hit{
Service: serviceID,
Latency: latency,
PingTime: latency * 0.15,
CreatedAt: createdAt,
}
db = db.Create(&hit)
fmt.Printf("Creating hit %d hit %d: %.2f %v\n", serviceID, hit.Id, latency, createdAt.String())
i++
}
return db.Commit().Error()
}

View File

@ -10,16 +10,16 @@ func (i *Incident) Updates() []*IncidentUpdate {
}
func (i *IncidentUpdate) Create() error {
db := database.DB().Create(&i)
db := database.DB().Create(i)
return db.Error()
}
func (i *IncidentUpdate) Update() error {
db := database.DB().Update(&i)
db := database.DB().Update(i)
return db.Error()
}
func (i *IncidentUpdate) Delete() error {
db := database.DB().Delete(&i)
db := database.DB().Delete(i)
return db.Error()
}

View File

@ -1 +0,0 @@
package types

View File

@ -19,16 +19,16 @@ func All() []*Message {
}
func (m *Message) Create() error {
db := DB().Create(&m)
db := DB().Create(m)
return db.Error()
}
func (m *Message) Update() error {
db := DB().Update(&m)
db := DB().Update(m)
return db.Error()
}
func (m *Message) Delete() error {
db := DB().Delete(&m)
db := DB().Delete(m)
return db.Error()
}

View File

@ -2,7 +2,6 @@ package services
import (
"fmt"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types/failures"
"strings"
"time"
@ -16,10 +15,10 @@ func (s *Service) AllFailures() failures.Failurer {
return failures.AllFailures(s)
}
func (s *Service) LastFailures(amount int) []*failures.Failure {
var fail []*failures.Failure
database.DB().Limit(amount).Find(&fail)
return fail
func (s *Service) LastFailure() *failures.Failure {
var fail failures.Failure
failures.DB().Where("service = ?", s.Id).Order("id desc").Limit(1).Find(&fail)
return &fail
}
func (s *Service) FailuresCount() int {
@ -35,11 +34,11 @@ func (s *Service) FailuresSince(t time.Time) []*failures.Failure {
}
func (s *Service) DowntimeText() string {
last := s.LastFailures(1)
if len(last) == 0 {
last := s.LastFailure()
if last == nil {
return ""
}
return parseError(last[0])
return parseError(last)
}
// ParseError returns a human readable error for a Failure

View File

@ -9,6 +9,14 @@ func (s *Service) HitsColumnID() (string, int64) {
return "service", s.Id
}
func (s *Service) FirstHit() *hits.Hit {
return hits.AllHits(s).First()
}
func (s *Service) LastHit() *hits.Hit {
return hits.AllHits(s).Last()
}
func (s *Service) AllHits() hits.Hitters {
return hits.AllHits(s)
}

View File

@ -186,14 +186,14 @@ func (s *Service) OnlineSince(ago time.Time) float32 {
// Downtime returns the amount of time of a offline service
func (s *Service) Downtime() time.Duration {
hits := s.AllHits().Last(1)
fail := s.AllFailures().Last(1)
if len(fail) == 0 {
hit := s.LastHit()
fail := s.LastFailure()
if hit == nil {
return time.Duration(0)
}
if len(fail) == 0 {
return time.Now().UTC().Sub(fail[0].CreatedAt.UTC())
if fail == nil {
return utils.Now().Sub(fail.CreatedAt)
}
since := fail[0].CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
return since
return fail.CreatedAt.Sub(hit.CreatedAt)
}

View File

@ -15,11 +15,6 @@ func AuthUser(username, password string) (*User, bool) {
log.Warnln(fmt.Errorf("user %v not found", username))
return nil, false
}
fmt.Println(username, password)
fmt.Println(username, user.Password)
if CheckHash(password, user.Password) {
user.UpdatedAt = time.Now().UTC()
user.Update()

View File

@ -4,6 +4,7 @@ import (
"errors"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/utils"
"github.com/prometheus/common/log"
"time"
)
@ -13,14 +14,14 @@ func DB() database.Database {
func Find(id int64) (*User, error) {
var user User
db := DB().Where("id = ?", id).Find(user)
db := DB().Where("id = ?", id).Find(&user)
return &user, db.Error()
}
func FindByUsername(username string) (*User, error) {
var user *User
db := DB().Where("username = ?", username).Find(user)
return user, db.Error()
var user User
db := DB().Where("username = ?", username).Find(&user)
return &user, db.Error()
}
func All() []*User {
@ -35,21 +36,27 @@ func (u *User) Create() error {
return errors.New("did not supply user password")
}
u.Password = utils.HashPassword(u.Password)
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
u.ApiKey = utils.NewSHA1Hash(16)
u.ApiSecret = utils.NewSHA1Hash(16)
db := DB().Create(u)
if db.Error() == nil {
log.Warnf("User #%d (%s) has been created", u.Id, u.Username)
}
return db.Error()
}
func (u *User) Update() error {
u.ApiKey = utils.NewSHA1Hash(5)
u.ApiSecret = utils.NewSHA1Hash(10)
db := DB().Update(&u)
//u.ApiKey = utils.NewSHA1Hash(5)
//u.ApiSecret = utils.NewSHA1Hash(10)
db := DB().Update(u)
return db.Error()
}
func (u *User) Delete() error {
db := DB().Delete(u)
if db.Error() == nil {
log.Warnf("User #%d (%s) has been deleted")
}
return db.Error()
}