mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2024-12-12 05:20:26 +00:00
Make account system concurrent-safe and refactor it a little
This commit is contained in:
parent
184aa1ae32
commit
b1f33c872c
2
flag.go
2
flag.go
@ -51,7 +51,7 @@ func parseCliArgs() {
|
||||
case "none":
|
||||
case "fixed":
|
||||
user.AuthUsed = true
|
||||
user.PopulateFixedUserStorage()
|
||||
user.ReadUsersFromFilesystem()
|
||||
default:
|
||||
log.Fatal("Error: unknown auth method:", util.AuthMethod)
|
||||
}
|
||||
|
1
go.mod
1
go.mod
@ -6,5 +6,6 @@ require (
|
||||
github.com/adrg/xdg v0.2.2
|
||||
github.com/gorilla/feeds v1.1.1
|
||||
github.com/hashicorp/go-memdb v1.3.0
|
||||
github.com/kr/pretty v0.2.1 // indirect
|
||||
github.com/valyala/quicktemplate v1.6.3
|
||||
)
|
||||
|
11
go.sum
11
go.sum
@ -1,6 +1,7 @@
|
||||
github.com/adrg/xdg v0.2.2 h1:A7ZHKRz5KGOLJX/bg7IPzStryhvCzAE1wX+KWawPiAo=
|
||||
github.com/adrg/xdg v0.2.2/go.mod h1:7I2hH/IT30IsupOpKZ5ue7/qNi3CoKzD6tL3HwpaRMQ=
|
||||
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
|
||||
github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
|
||||
@ -8,14 +9,22 @@ github.com/hashicorp/go-immutable-radix v1.3.0 h1:8exGP7ego3OmkfksihtSouGMZ+hQrh
|
||||
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-memdb v1.3.0 h1:xdXq34gBOMEloa9rlGStLxmfX/dyIK8htOv36dQUwHU=
|
||||
github.com/hashicorp/go-memdb v1.3.0/go.mod h1:Mluclgwib3R93Hk5fxEfiRhB+6Dar64wWh71LpNSe3g=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
@ -29,5 +38,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
@ -121,8 +121,7 @@ func (hop *HistoryOp) WithMsg(userMsg string) *HistoryOp {
|
||||
|
||||
// WithUser sets a user for the commit.
|
||||
func (hop *HistoryOp) WithUser(u *user.User) *HistoryOp {
|
||||
u = u.OrAnon()
|
||||
if u.Group != user.UserAnon {
|
||||
if u.Group != "anon" {
|
||||
hop.name = u.Name
|
||||
hop.email = u.Name + "@mycorrhiza"
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) {
|
||||
newName = CanonicalName(rq.PostFormValue("new-name"))
|
||||
_, newNameIsUsed = HyphaStorage[newName]
|
||||
recursive = rq.PostFormValue("recursive") == "true"
|
||||
u = user.FromRequest(rq).OrAnon()
|
||||
u = user.FromRequest(rq)
|
||||
)
|
||||
switch {
|
||||
case !u.CanProceed("rename-confirm"):
|
||||
@ -151,7 +151,7 @@ func handlerUploadText(w http.ResponseWriter, rq *http.Request) {
|
||||
var (
|
||||
hyphaName = HyphaNameFromRq(rq, "upload-text")
|
||||
textData = rq.PostFormValue("text")
|
||||
u = user.FromRequest(rq).OrAnon()
|
||||
u = user.FromRequest(rq)
|
||||
)
|
||||
if ok := user.CanProceed(rq, "upload-text"); !ok {
|
||||
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to edit pages.")
|
||||
@ -174,7 +174,7 @@ func handlerUploadBinary(w http.ResponseWriter, rq *http.Request) {
|
||||
log.Println(rq.URL)
|
||||
var (
|
||||
hyphaName = HyphaNameFromRq(rq, "upload-binary")
|
||||
u = user.FromRequest(rq).OrAnon()
|
||||
u = user.FromRequest(rq)
|
||||
)
|
||||
if !u.CanProceed("upload-binary") {
|
||||
HttpErr(w, http.StatusForbidden, hyphaName, "Not enough rights", "You must be an editor to upload attachments.")
|
||||
|
@ -1,7 +1,7 @@
|
||||
package hyphae
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/bouncepaw/mycorrhiza/storage"
|
||||
)
|
||||
|
||||
type Hypha struct {
|
||||
@ -15,7 +15,7 @@ type Hypha struct {
|
||||
|
||||
// AddHypha adds a hypha named `name` with such `textPath` and `binaryPath`. Both paths can be empty. Does //not// check for hypha's existence beforehand. Count is handled.
|
||||
func AddHypha(name, textPath, binaryPath string) {
|
||||
txn := db.Txn(true)
|
||||
txn := storage.DB.Txn(true)
|
||||
txn.Insert("hyphae",
|
||||
&Hypha{
|
||||
Name: name,
|
||||
@ -31,54 +31,3 @@ func AddHypha(name, textPath, binaryPath string) {
|
||||
// DeleteHypha clears both paths and all out-links from the named hypha and marks it as non-existent. It does not actually delete it from the memdb. Count is handled.
|
||||
func DeleteHypha(name string) {
|
||||
}
|
||||
|
||||
// Create the DB schema
|
||||
var schema = &memdb.DBSchema{
|
||||
Tables: map[string]*memdb.TableSchema{
|
||||
"hyphae": &memdb.TableSchema{
|
||||
Name: "hyphae",
|
||||
Indexes: map[string]*memdb.IndexSchema{
|
||||
"id": &memdb.IndexSchema{
|
||||
Name: "id",
|
||||
Unique: true,
|
||||
Indexer: &memdb.StringFieldIndex{Field: "Name"},
|
||||
},
|
||||
"exists": &memdb.IndexSchema{
|
||||
Name: "exists",
|
||||
Unique: false,
|
||||
Indexer: &memdb.BoolFieldIndex{Field: "Exists"},
|
||||
},
|
||||
"text-path": &memdb.IndexSchema{
|
||||
Name: "text-path",
|
||||
Unique: true,
|
||||
Indexer: &memdb.StringFieldIndex{Field: "TextPath"},
|
||||
},
|
||||
"binary-path": &memdb.IndexSchema{
|
||||
Name: "binary-path",
|
||||
Unique: true,
|
||||
Indexer: &memdb.StringFieldIndex{Field: "BinaryPath"},
|
||||
},
|
||||
"out-links": &memdb.IndexSchema{
|
||||
Name: "out-links",
|
||||
Unique: false,
|
||||
Indexer: &memdb.StringSliceFieldIndex{Field: "OutLinks"},
|
||||
},
|
||||
"back-links": &memdb.IndexSchema{
|
||||
Name: "back-links",
|
||||
Unique: false,
|
||||
Indexer: &memdb.StringSliceFieldIndex{Field: "BackLinks"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var db *memdb.MemDB
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
db, err = memdb.NewMemDB(schema)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@ -120,6 +120,9 @@ func ParagraphToHtml(hyphaName, input string) string {
|
||||
startsWith = func(t string) bool {
|
||||
return bytes.HasPrefix(p.Bytes(), []byte(t))
|
||||
}
|
||||
noTagsActive = func() bool {
|
||||
return !(tagState[spanItalic] || tagState[spanBold] || tagState[spanMono] || tagState[spanSuper] || tagState[spanSub] || tagState[spanMark] || tagState[spanLink])
|
||||
}
|
||||
)
|
||||
|
||||
for p.Len() != 0 {
|
||||
@ -147,7 +150,7 @@ func ParagraphToHtml(hyphaName, input string) string {
|
||||
p.Next(2)
|
||||
case startsWith("[["):
|
||||
ret.WriteString(getLinkNode(p, hyphaName, true))
|
||||
case startsWith("https://"), startsWith("http://"), startsWith("gemini://"), startsWith("gopher://"), startsWith("ftp://"):
|
||||
case (startsWith("https://") || startsWith("http://") || startsWith("gemini://") || startsWith("gopher://") || startsWith("ftp://")) && noTagsActive():
|
||||
ret.WriteString(getLinkNode(p, hyphaName, false))
|
||||
default:
|
||||
ret.WriteString(html.EscapeString(getTextNode(p)))
|
||||
|
65
storage/storage.go
Normal file
65
storage/storage.go
Normal file
@ -0,0 +1,65 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-memdb"
|
||||
)
|
||||
|
||||
// Create the DB schema
|
||||
var schema = &memdb.DBSchema{
|
||||
Tables: map[string]*memdb.TableSchema{
|
||||
"hyphae": &memdb.TableSchema{
|
||||
Name: "hyphae",
|
||||
Indexes: map[string]*memdb.IndexSchema{
|
||||
"id": &memdb.IndexSchema{
|
||||
Name: "id",
|
||||
Unique: true,
|
||||
Indexer: &memdb.StringFieldIndex{Field: "Name"},
|
||||
},
|
||||
"exists": &memdb.IndexSchema{
|
||||
Name: "exists",
|
||||
Unique: false,
|
||||
Indexer: &memdb.BoolFieldIndex{Field: "Exists"},
|
||||
},
|
||||
"out-links": &memdb.IndexSchema{
|
||||
Name: "out-links",
|
||||
Unique: false,
|
||||
Indexer: &memdb.StringSliceFieldIndex{Field: "OutLinks"},
|
||||
},
|
||||
"back-links": &memdb.IndexSchema{
|
||||
Name: "back-links",
|
||||
Unique: false,
|
||||
Indexer: &memdb.StringSliceFieldIndex{Field: "BackLinks"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var DB *memdb.MemDB
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
DB, err = memdb.NewMemDB(schema)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func ForEveryRecord(table string, λ func(obj interface{})) error {
|
||||
txn := DB.Txn(false)
|
||||
defer txn.Abort()
|
||||
|
||||
it, err := txn.Get(table, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for obj := it.Next(); obj != nil; obj = it.Next() {
|
||||
λ(obj)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TxnW() *memdb.Txn { return DB.Txn(true) }
|
||||
func TxnR() *memdb.Txn { return DB.Txn(false) }
|
@ -6,7 +6,7 @@
|
||||
{% if user.AuthUsed %}
|
||||
<h1>Login</h1>
|
||||
<form method="post" action="/login-data" id="login-form" enctype="multipart/form-data">
|
||||
<p>Use the data you were given by the administrator.</p>
|
||||
<p>Use the data you were given by an administrator.</p>
|
||||
<fieldset>
|
||||
<legend>Username</legend>
|
||||
<input type="text" required autofocus name="username" autocomplete="on">
|
||||
@ -15,7 +15,7 @@
|
||||
<legend>Password</legend>
|
||||
<input type="password" required name="password" autocomplete="on">
|
||||
</fieldset>
|
||||
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.</p>
|
||||
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.</p>
|
||||
<input type="submit">
|
||||
<a href="/">Cancel</a>
|
||||
</form>
|
||||
|
@ -33,7 +33,7 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
|
||||
qw422016.N().S(`
|
||||
<h1>Login</h1>
|
||||
<form method="post" action="/login-data" id="login-form" enctype="multipart/form-data">
|
||||
<p>Use the data you were given by the administrator.</p>
|
||||
<p>Use the data you were given by an administrator.</p>
|
||||
<fieldset>
|
||||
<legend>Username</legend>
|
||||
<input type="text" required autofocus name="username" autocomplete="on">
|
||||
@ -42,7 +42,7 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
|
||||
<legend>Password</legend>
|
||||
<input type="password" required name="password" autocomplete="on">
|
||||
</fieldset>
|
||||
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you.</p>
|
||||
<p>By submitting this form you give this wiki a permission to store cookies in your browser. It lets the engine associate your edits with you. You will stay logged in until you log out.</p>
|
||||
<input type="submit">
|
||||
<a href="/">Cancel</a>
|
||||
</form>
|
||||
|
@ -21,8 +21,9 @@ var navEntries = []navEntry{
|
||||
|
||||
{% func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) %}
|
||||
{% code
|
||||
u := user.FromRequest(rq).OrAnon()
|
||||
u := user.FromRequest(rq)
|
||||
%}
|
||||
|
||||
<nav class="navlinks">
|
||||
<ul>
|
||||
{%- for _, entry := range navEntries -%}
|
||||
@ -30,7 +31,7 @@ var navEntries = []navEntry{
|
||||
<li><b>{%s revisionHash[0] %}</b></li>
|
||||
{%- elseif navType == entry.path -%}
|
||||
<li><b>{%s entry.title %}</b></li>
|
||||
{%- elseif entry.path != "revision" && u.Group.CanAccessRoute(entry.path) -%}
|
||||
{%- elseif entry.path != "revision" && u.CanProceed(entry.path) -%}
|
||||
<li><a href="/{%s entry.path %}/{%s hyphaName %}">{%s entry.title %}</a></li>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
@ -42,7 +43,7 @@ var navEntries = []navEntry{
|
||||
{% func userMenuHTML(u *user.User) %}
|
||||
{% if user.AuthUsed %}
|
||||
<li class="navlinks__user">
|
||||
{% if u.Group == user.UserAnon %}
|
||||
{% if u.Group == "anon" %}
|
||||
<a href="/login">Login</a>
|
||||
{% else %}
|
||||
<a href="/page/{%s util.UserTree %}/{%s u.Name %}">{%s u.Name %}</a>
|
||||
|
@ -50,163 +50,164 @@ func streamnavHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navTy
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line templates/common.qtpl:24
|
||||
u := user.FromRequest(rq).OrAnon()
|
||||
u := user.FromRequest(rq)
|
||||
|
||||
//line templates/common.qtpl:25
|
||||
qw422016.N().S(`
|
||||
|
||||
<nav class="navlinks">
|
||||
<ul>
|
||||
`)
|
||||
//line templates/common.qtpl:28
|
||||
//line templates/common.qtpl:29
|
||||
for _, entry := range navEntries {
|
||||
//line templates/common.qtpl:29
|
||||
//line templates/common.qtpl:30
|
||||
if navType == "revision" && entry.path == "revision" {
|
||||
//line templates/common.qtpl:29
|
||||
qw422016.N().S(` <li><b>`)
|
||||
//line templates/common.qtpl:30
|
||||
qw422016.N().S(` <li><b>`)
|
||||
//line templates/common.qtpl:31
|
||||
qw422016.E().S(revisionHash[0])
|
||||
//line templates/common.qtpl:30
|
||||
//line templates/common.qtpl:31
|
||||
qw422016.N().S(`</b></li>
|
||||
`)
|
||||
//line templates/common.qtpl:31
|
||||
//line templates/common.qtpl:32
|
||||
} else if navType == entry.path {
|
||||
//line templates/common.qtpl:31
|
||||
//line templates/common.qtpl:32
|
||||
qw422016.N().S(` <li><b>`)
|
||||
//line templates/common.qtpl:32
|
||||
//line templates/common.qtpl:33
|
||||
qw422016.E().S(entry.title)
|
||||
//line templates/common.qtpl:32
|
||||
//line templates/common.qtpl:33
|
||||
qw422016.N().S(`</b></li>
|
||||
`)
|
||||
//line templates/common.qtpl:33
|
||||
} else if entry.path != "revision" && u.Group.CanAccessRoute(entry.path) {
|
||||
//line templates/common.qtpl:33
|
||||
//line templates/common.qtpl:34
|
||||
} else if entry.path != "revision" && u.CanProceed(entry.path) {
|
||||
//line templates/common.qtpl:34
|
||||
qw422016.N().S(` <li><a href="/`)
|
||||
//line templates/common.qtpl:34
|
||||
//line templates/common.qtpl:35
|
||||
qw422016.E().S(entry.path)
|
||||
//line templates/common.qtpl:34
|
||||
//line templates/common.qtpl:35
|
||||
qw422016.N().S(`/`)
|
||||
//line templates/common.qtpl:34
|
||||
//line templates/common.qtpl:35
|
||||
qw422016.E().S(hyphaName)
|
||||
//line templates/common.qtpl:34
|
||||
//line templates/common.qtpl:35
|
||||
qw422016.N().S(`">`)
|
||||
//line templates/common.qtpl:34
|
||||
//line templates/common.qtpl:35
|
||||
qw422016.E().S(entry.title)
|
||||
//line templates/common.qtpl:34
|
||||
//line templates/common.qtpl:35
|
||||
qw422016.N().S(`</a></li>
|
||||
`)
|
||||
//line templates/common.qtpl:35
|
||||
}
|
||||
//line templates/common.qtpl:36
|
||||
}
|
||||
//line templates/common.qtpl:36
|
||||
//line templates/common.qtpl:37
|
||||
}
|
||||
//line templates/common.qtpl:37
|
||||
qw422016.N().S(` `)
|
||||
//line templates/common.qtpl:37
|
||||
//line templates/common.qtpl:38
|
||||
qw422016.N().S(userMenuHTML(u))
|
||||
//line templates/common.qtpl:37
|
||||
//line templates/common.qtpl:38
|
||||
qw422016.N().S(`
|
||||
</ul>
|
||||
</nav>
|
||||
`)
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
}
|
||||
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
func writenavHTML(qq422016 qtio422016.Writer, rq *http.Request, hyphaName, navType string, revisionHash ...string) {
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
streamnavHTML(qw422016, rq, hyphaName, navType, revisionHash...)
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
}
|
||||
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
func navHTML(rq *http.Request, hyphaName, navType string, revisionHash ...string) string {
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
writenavHTML(qb422016, rq, hyphaName, navType, revisionHash...)
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
qs422016 := string(qb422016.B)
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
return qs422016
|
||||
//line templates/common.qtpl:40
|
||||
//line templates/common.qtpl:41
|
||||
}
|
||||
|
||||
//line templates/common.qtpl:42
|
||||
//line templates/common.qtpl:43
|
||||
func streamuserMenuHTML(qw422016 *qt422016.Writer, u *user.User) {
|
||||
//line templates/common.qtpl:42
|
||||
//line templates/common.qtpl:43
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line templates/common.qtpl:43
|
||||
//line templates/common.qtpl:44
|
||||
if user.AuthUsed {
|
||||
//line templates/common.qtpl:43
|
||||
//line templates/common.qtpl:44
|
||||
qw422016.N().S(`
|
||||
<li class="navlinks__user">
|
||||
`)
|
||||
//line templates/common.qtpl:45
|
||||
if u.Group == user.UserAnon {
|
||||
//line templates/common.qtpl:45
|
||||
//line templates/common.qtpl:46
|
||||
if u.Group == "anon" {
|
||||
//line templates/common.qtpl:46
|
||||
qw422016.N().S(`
|
||||
<a href="/login">Login</a>
|
||||
`)
|
||||
//line templates/common.qtpl:47
|
||||
//line templates/common.qtpl:48
|
||||
} else {
|
||||
//line templates/common.qtpl:47
|
||||
//line templates/common.qtpl:48
|
||||
qw422016.N().S(`
|
||||
<a href="/page/`)
|
||||
//line templates/common.qtpl:48
|
||||
//line templates/common.qtpl:49
|
||||
qw422016.E().S(util.UserTree)
|
||||
//line templates/common.qtpl:48
|
||||
//line templates/common.qtpl:49
|
||||
qw422016.N().S(`/`)
|
||||
//line templates/common.qtpl:48
|
||||
//line templates/common.qtpl:49
|
||||
qw422016.E().S(u.Name)
|
||||
//line templates/common.qtpl:48
|
||||
//line templates/common.qtpl:49
|
||||
qw422016.N().S(`">`)
|
||||
//line templates/common.qtpl:48
|
||||
//line templates/common.qtpl:49
|
||||
qw422016.E().S(u.Name)
|
||||
//line templates/common.qtpl:48
|
||||
//line templates/common.qtpl:49
|
||||
qw422016.N().S(`</a>
|
||||
`)
|
||||
//line templates/common.qtpl:49
|
||||
//line templates/common.qtpl:50
|
||||
}
|
||||
//line templates/common.qtpl:49
|
||||
//line templates/common.qtpl:50
|
||||
qw422016.N().S(`
|
||||
</li>
|
||||
`)
|
||||
//line templates/common.qtpl:51
|
||||
//line templates/common.qtpl:52
|
||||
}
|
||||
//line templates/common.qtpl:51
|
||||
//line templates/common.qtpl:52
|
||||
qw422016.N().S(`
|
||||
`)
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
}
|
||||
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
func writeuserMenuHTML(qq422016 qtio422016.Writer, u *user.User) {
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
qw422016 := qt422016.AcquireWriter(qq422016)
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
streamuserMenuHTML(qw422016, u)
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
qt422016.ReleaseWriter(qw422016)
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
}
|
||||
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
func userMenuHTML(u *user.User) string {
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
qb422016 := qt422016.AcquireByteBuffer()
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
writeuserMenuHTML(qb422016, u)
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
qs422016 := string(qb422016.B)
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
qt422016.ReleaseByteBuffer(qb422016)
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
return qs422016
|
||||
//line templates/common.qtpl:52
|
||||
//line templates/common.qtpl:53
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ If `contents` == "", a helpful message is shown instead.
|
||||
{% endif %}
|
||||
</section>
|
||||
<hr class="page-separator"/>
|
||||
{% if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon %}
|
||||
{% if u := user.FromRequest(rq); !user.AuthUsed || u.Group != "anon" %}
|
||||
<form action="/upload-binary/{%s hyphaName %}"
|
||||
method="post" enctype="multipart/form-data">
|
||||
<label for="upload-binary__input">Upload a new attachment</label>
|
||||
|
@ -228,7 +228,7 @@ func StreamPageHTML(qw422016 *qt422016.Writer, rq *http.Request, hyphaName, navi
|
||||
<hr class="page-separator"/>
|
||||
`)
|
||||
//line templates/http_readers.qtpl:51
|
||||
if u := user.FromRequest(rq).OrAnon(); !user.AuthUsed || u.Group > user.UserAnon {
|
||||
if u := user.FromRequest(rq); !user.AuthUsed || u.Group != "anon" {
|
||||
//line templates/http_readers.qtpl:51
|
||||
qw422016.N().S(`
|
||||
<form action="/upload-binary/`)
|
||||
|
@ -10,53 +10,55 @@ import (
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
)
|
||||
|
||||
func PopulateFixedUserStorage() {
|
||||
// ReadUsersFromFilesystem reads all user information from filesystem and stores it internally. Call it during initialization.
|
||||
func ReadUsersFromFilesystem() {
|
||||
rememberUsers(usersFromFixedCredentials())
|
||||
readTokensToUsers()
|
||||
}
|
||||
|
||||
func usersFromFixedCredentials() []*User {
|
||||
var users []*User
|
||||
contents, err := ioutil.ReadFile(util.FixedCredentialsPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = json.Unmarshal(contents, &UserStorage.Users)
|
||||
err = json.Unmarshal(contents, &users)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, user := range UserStorage.Users {
|
||||
user.Group = groupFromString(user.GroupString)
|
||||
}
|
||||
log.Println("Found", len(UserStorage.Users), "fixed users")
|
||||
log.Println("Found", len(users), "fixed users")
|
||||
return users
|
||||
}
|
||||
|
||||
contents, err = ioutil.ReadFile(tokenStoragePath())
|
||||
func rememberUsers(uu []*User) {
|
||||
// uu is used to not shadow the `users` in `users.go`.
|
||||
for _, user := range uu {
|
||||
users.Store(user.Name, user)
|
||||
}
|
||||
}
|
||||
|
||||
func readTokensToUsers() {
|
||||
contents, err := ioutil.ReadFile(tokenStoragePath())
|
||||
if os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tmp map[string]string
|
||||
err = json.Unmarshal(contents, &tmp)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for token, username := range tmp {
|
||||
user := UserStorage.userByName(username)
|
||||
UserStorage.Tokens[token] = user
|
||||
commenceSession(username, token)
|
||||
}
|
||||
log.Println("Found", len(tmp), "active sessions")
|
||||
}
|
||||
|
||||
func dumpTokens() {
|
||||
tmp := make(map[string]string)
|
||||
for token, user := range UserStorage.Tokens {
|
||||
tmp[token] = user.Name
|
||||
}
|
||||
blob, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
ioutil.WriteFile(tokenStoragePath(), blob, 0644)
|
||||
}
|
||||
}
|
||||
|
||||
// Return path to tokens.json.
|
||||
// Return path to tokens.json. Creates folders if needed.
|
||||
func tokenStoragePath() string {
|
||||
dir, err := xdg.DataFile("mycorrhiza/tokens.json")
|
||||
if err != nil {
|
||||
@ -67,3 +69,21 @@ func tokenStoragePath() string {
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func dumpTokens() {
|
||||
tmp := make(map[string]string)
|
||||
|
||||
tokens.Range(func(k, v interface{}) bool {
|
||||
token := k.(string)
|
||||
username := v.(string)
|
||||
tmp[token] = username
|
||||
return true
|
||||
})
|
||||
|
||||
blob, err := json.Marshal(tmp)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
} else {
|
||||
ioutil.WriteFile(tokenStoragePath(), blob, 0644)
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func groupFromString(s string) UserGroup {
|
||||
switch s {
|
||||
case "admin":
|
||||
return UserAdmin
|
||||
case "moderator":
|
||||
return UserModerator
|
||||
case "trusted":
|
||||
return UserTrusted
|
||||
case "editor":
|
||||
return UserEditor
|
||||
default:
|
||||
log.Fatal("Unknown user group", s)
|
||||
return UserAnon
|
||||
}
|
||||
}
|
||||
|
||||
// UserGroup represents a group that a user is part of.
|
||||
type UserGroup int
|
||||
|
||||
const (
|
||||
// UserAnon is the default user group which all unauthorized visitors have.
|
||||
UserAnon UserGroup = iota
|
||||
// UserEditor is a user who can edit and upload stuff.
|
||||
UserEditor
|
||||
// UserTrusted is a trusted editor who can also rename stuff.
|
||||
UserTrusted
|
||||
// UserModerator is a moderator who can also delete stuff.
|
||||
UserModerator
|
||||
// UserAdmin can do everything.
|
||||
UserAdmin
|
||||
)
|
||||
|
||||
var minimalRights = map[string]UserGroup{
|
||||
"edit": UserEditor,
|
||||
"upload-binary": UserEditor,
|
||||
"upload-text": UserEditor,
|
||||
"rename-ask": UserTrusted,
|
||||
"rename-confirm": UserTrusted,
|
||||
"delete-ask": UserModerator,
|
||||
"delete-confirm": UserModerator,
|
||||
"reindex": UserAdmin,
|
||||
}
|
||||
|
||||
func (ug UserGroup) CanAccessRoute(route string) bool {
|
||||
if !AuthUsed {
|
||||
return true
|
||||
}
|
||||
if minimalRight, ok := minimalRights[route]; ok {
|
||||
if ug >= minimalRight {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func CanProceed(rq *http.Request, route string) bool {
|
||||
return FromRequest(rq).OrAnon().CanProceed(route)
|
||||
}
|
||||
|
||||
func (u *User) CanProceed(route string) bool {
|
||||
return u.Group.CanAccessRoute(route)
|
||||
}
|
75
user/net.go
Normal file
75
user/net.go
Normal file
@ -0,0 +1,75 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
)
|
||||
|
||||
// CanProceed returns `true` if the user in `rq` has enough rights to access `route`.
|
||||
func CanProceed(rq *http.Request, route string) bool {
|
||||
return FromRequest(rq).CanProceed(route)
|
||||
}
|
||||
|
||||
// FromRequest returns user from `rq`. If there is no user, an anon user is returned instead.
|
||||
func FromRequest(rq *http.Request) *User {
|
||||
cookie, err := rq.Cookie("mycorrhiza_token")
|
||||
if err != nil {
|
||||
return emptyUser()
|
||||
}
|
||||
return userByToken(cookie.Value)
|
||||
}
|
||||
|
||||
// LogoutFromRequest logs the user in `rq` out and rewrites the cookie in `w`.
|
||||
func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) {
|
||||
cookieFromUser, err := rq.Cookie("mycorrhiza_token")
|
||||
if err == nil {
|
||||
http.SetCookie(w, cookie("token", "", time.Unix(0, 0)))
|
||||
terminateSession(cookieFromUser.Value)
|
||||
}
|
||||
}
|
||||
|
||||
// LoginDataHTTP logs such user in and returns string representation of an error if there is any.
|
||||
func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string {
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
if !HasUsername(username) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Println("Unknown username", username, "was entered")
|
||||
return "unknown username"
|
||||
}
|
||||
if !CredentialsOK(username, password) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Println("A wrong password was entered for username", username)
|
||||
return "wrong password"
|
||||
}
|
||||
token, err := AddSession(username)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return err.Error()
|
||||
}
|
||||
http.SetCookie(w, cookie("token", token, time.Now().Add(365*24*time.Hour)))
|
||||
return ""
|
||||
}
|
||||
|
||||
// AddSession saves a session for `username` and returns a token to use.
|
||||
func AddSession(username string) (string, error) {
|
||||
token, err := util.RandomString(16)
|
||||
if err == nil {
|
||||
commenceSession(username, token)
|
||||
log.Println("New token for", username, "is", token)
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
// A handy cookie constructor
|
||||
func cookie(name_suffix, val string, t time.Time) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: "mycorrhiza_" + name_suffix,
|
||||
Value: val,
|
||||
Expires: t,
|
||||
Path: "/",
|
||||
}
|
||||
}
|
176
user/user.go
176
user/user.go
@ -1,138 +1,66 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/bouncepaw/mycorrhiza/util"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func (u *User) OrAnon() *User {
|
||||
if u == nil {
|
||||
return &User{}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func LogoutFromRequest(w http.ResponseWriter, rq *http.Request) {
|
||||
cookieFromUser, err := rq.Cookie("mycorrhiza_token")
|
||||
if err == nil {
|
||||
http.SetCookie(w, cookie("token", "", time.Unix(0, 0)))
|
||||
terminateSession(cookieFromUser.Value)
|
||||
}
|
||||
}
|
||||
|
||||
func (us *FixedUserStorage) userByToken(token string) *User {
|
||||
if user, ok := us.Tokens[token]; ok {
|
||||
return user
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *FixedUserStorage) userByName(username string) *User {
|
||||
for _, user := range us.Users {
|
||||
if user.Name == username {
|
||||
return user
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func FromRequest(rq *http.Request) *User {
|
||||
cookie, err := rq.Cookie("mycorrhiza_token")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return UserStorage.userByToken(cookie.Value).OrAnon()
|
||||
}
|
||||
|
||||
func LoginDataHTTP(w http.ResponseWriter, rq *http.Request, username, password string) string {
|
||||
w.Header().Set("Content-Type", "text/html;charset=utf-8")
|
||||
if !HasUsername(username) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Println("Unknown username", username, "was entered")
|
||||
return "unknown username"
|
||||
}
|
||||
if !CredentialsOK(username, password) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
log.Println("A wrong password was entered for username", username)
|
||||
return "wrong password"
|
||||
}
|
||||
token, err := AddSession(username)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return err.Error()
|
||||
}
|
||||
http.SetCookie(w, cookie("token", token, time.Now().Add(14*24*time.Hour)))
|
||||
return ""
|
||||
}
|
||||
|
||||
// AddSession saves a session for `username` and returns a token to use.
|
||||
func AddSession(username string) (string, error) {
|
||||
token, err := util.RandomString(16)
|
||||
if err == nil {
|
||||
for _, user := range UserStorage.Users {
|
||||
if user.Name == username {
|
||||
UserStorage.Tokens[token] = user
|
||||
go dumpTokens()
|
||||
}
|
||||
}
|
||||
log.Println("New token for", username, "is", token)
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
func terminateSession(token string) {
|
||||
delete(UserStorage.Tokens, token)
|
||||
go dumpTokens()
|
||||
}
|
||||
|
||||
func HasUsername(username string) bool {
|
||||
for _, user := range UserStorage.Users {
|
||||
if user.Name == username {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CredentialsOK(username, password string) bool {
|
||||
for _, user := range UserStorage.Users {
|
||||
if user.Name == username && user.Password == password {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type FixedUserStorage struct {
|
||||
Users []*User
|
||||
Tokens map[string]*User
|
||||
}
|
||||
|
||||
var UserStorage = FixedUserStorage{Tokens: make(map[string]*User)}
|
||||
|
||||
// AuthUsed shows if a method of authentication is used. You should set it by yourself.
|
||||
var AuthUsed bool
|
||||
|
||||
// User is a user.
|
||||
type User struct {
|
||||
// Name is a username. It must follow hypha naming rules.
|
||||
Name string `json:"name"`
|
||||
// Group the user is part of.
|
||||
Group UserGroup `json:"-"`
|
||||
GroupString string `json:"group"`
|
||||
Group string `json:"group"`
|
||||
Password string `json:"password"`
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// A handy cookie constructor
|
||||
func cookie(name_suffix, val string, t time.Time) *http.Cookie {
|
||||
return &http.Cookie{
|
||||
Name: "mycorrhiza_" + name_suffix,
|
||||
Value: val,
|
||||
Expires: t,
|
||||
Path: "/",
|
||||
// Route — Right (more is more right)
|
||||
var minimalRights = map[string]int{
|
||||
"edit": 1,
|
||||
"upload-binary": 1,
|
||||
"upload-text": 1,
|
||||
"rename-ask": 2,
|
||||
"rename-confirm": 2,
|
||||
"delete-ask": 3,
|
||||
"delete-confirm": 3,
|
||||
"reindex": 4,
|
||||
}
|
||||
|
||||
// Group — Right
|
||||
var groupRight = map[string]int{
|
||||
"anon": 0,
|
||||
"editor": 1,
|
||||
"trusted": 2,
|
||||
"moderator": 3,
|
||||
"admin": 4,
|
||||
}
|
||||
|
||||
func emptyUser() *User {
|
||||
return &User{
|
||||
Name: "anon",
|
||||
Group: "anon",
|
||||
Password: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (user *User) CanProceed(route string) bool {
|
||||
if !AuthUsed {
|
||||
return true
|
||||
}
|
||||
|
||||
user.RLock()
|
||||
defer user.RUnlock()
|
||||
|
||||
right, _ := groupRight[user.Group]
|
||||
minimalRight, _ := minimalRights[route]
|
||||
if right >= minimalRight {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (user *User) isCorrectPassword(password string) bool {
|
||||
user.RLock()
|
||||
defer user.RUnlock()
|
||||
|
||||
return password == user.Password
|
||||
}
|
||||
|
44
user/users.go
Normal file
44
user/users.go
Normal file
@ -0,0 +1,44 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var AuthUsed bool
|
||||
var users sync.Map
|
||||
var tokens sync.Map
|
||||
|
||||
func HasUsername(username string) bool {
|
||||
_, has := users.Load(username)
|
||||
return has
|
||||
}
|
||||
|
||||
func CredentialsOK(username, password string) bool {
|
||||
return userByName(username).isCorrectPassword(password)
|
||||
}
|
||||
|
||||
func userByToken(token string) *User {
|
||||
if usernameUntyped, ok := tokens.Load(token); ok {
|
||||
username := usernameUntyped.(string)
|
||||
return userByName(username)
|
||||
}
|
||||
return emptyUser()
|
||||
}
|
||||
|
||||
func userByName(username string) *User {
|
||||
if userUntyped, ok := users.Load(username); ok {
|
||||
user := userUntyped.(*User)
|
||||
return user
|
||||
}
|
||||
return emptyUser()
|
||||
}
|
||||
|
||||
func commenceSession(username, token string) {
|
||||
tokens.Store(token, username)
|
||||
go dumpTokens()
|
||||
}
|
||||
|
||||
func terminateSession(token string) {
|
||||
tokens.Delete(token)
|
||||
go dumpTokens()
|
||||
}
|
Loading…
Reference in New Issue
Block a user