Skip to content

Commit

Permalink
Add very basic admin page.
Browse files Browse the repository at this point in the history
  • Loading branch information
jholdstock committed Jun 8, 2020
1 parent 805f491 commit 5904cdb
Show file tree
Hide file tree
Showing 12 changed files with 309 additions and 76 deletions.
27 changes: 27 additions & 0 deletions database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var (
versionK = []byte("version")
// feeXPub is the extended public key used for collecting VSP fees.
feeXPubK = []byte("feeXPub")
// cookieSecret is the secret key for initializing the cookie store.
cookieSecretK = []byte("cookieSecret")
// privatekey is the private key.
privateKeyK = []byte("privatekey")
// lastaddressindex is the index of the last address used for fees.
Expand Down Expand Up @@ -103,6 +105,18 @@ func CreateNew(dbFile, feeXPub string) error {
return err
}

// Generate a secret key for initializing the cookie store.
log.Info("Generating cookie secret")
secret := make([]byte, 32)
_, err = rand.Read(secret)
if err != nil {
return err
}
err = vspBkt.Put(cookieSecretK, secret)
if err != nil {
return err
}

log.Info("Storing extended public key")
// Store fee xpub
err = vspBkt.Put(feeXPubK, []byte(feeXPub))
Expand Down Expand Up @@ -228,3 +242,16 @@ func (vdb *VspDatabase) GetFeeXPub() (string, error) {

return feeXPub, err
}

func (vdb *VspDatabase) GetCookieSecret() ([]byte, error) {
var cookieSecret []byte
err := vdb.db.View(func(tx *bolt.Tx) error {
vspBkt := tx.Bucket(vspBktK)

cookieSecret = vspBkt.Get(cookieSecretK)

return nil
})

return cookieSecret, err
}
2 changes: 1 addition & 1 deletion database/database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ func TestDatabase(t *testing.T) {
}

// TODO: Add tests for CountTickets, GetUnconfirmedTickets, GetPendingFees,
// GetUnconfirmedFees.
// GetUnconfirmedFees, GetAllTickets.
21 changes: 21 additions & 0 deletions database/ticket.go
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,24 @@ func (vdb *VspDatabase) GetUnconfirmedFees() ([]Ticket, error) {

return tickets, err
}

func (vdb *VspDatabase) GetAllTickets() ([]Ticket, error) {
var tickets []Ticket
err := vdb.db.View(func(tx *bolt.Tx) error {
ticketBkt := tx.Bucket(vspBktK).Bucket(ticketBktK)

return ticketBkt.ForEach(func(k, v []byte) error {
var ticket Ticket
err := json.Unmarshal(v, &ticket)
if err != nil {
return fmt.Errorf("could not unmarshal ticket: %v", err)
}

tickets = append(tickets, ticket)

return nil
})
})

return tickets, err
}
11 changes: 6 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ go 1.13

require (
decred.org/dcrwallet v1.2.3-0.20200519180100-f1aa4c354e05
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/dcrec v1.0.0
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984
github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693
github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57
github.com/decred/dcrd/wire v1.3.0
github.com/decred/slog v1.0.0
github.com/gin-gonic/gin v1.6.3
github.com/gorilla/sessions v1.2.0
github.com/jessevdk/go-flags v1.4.0
github.com/jrick/bitset v1.0.0
github.com/jrick/logrotate v1.0.0
Expand Down
24 changes: 14 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ github.com/decred/dcrd/addrmgr v1.1.0/go.mod h1:exghL+0+QeVvO4MXezWJ1C2tcpBn3ngf
github.com/decred/dcrd/blockchain/stake/v2 v2.0.2/go.mod h1:o2TT/l/YFdrt15waUdlZ3g90zfSwlA0WgQqHV9UGJF4=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:aDL94kcVJfaaJP+acWUJrlK7g7xEOqTSiFe6bSN3yRQ=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693 h1:2Wbj27h04yq4l8KSqCJkZQnmWiKfUI4kNYaUms5oSnY=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57 h1:4/glIgVrylnAAYlpfgBmxg8dX9DHWJu/tlBagcNiXz4=
github.com/decred/dcrd/blockchain/stake/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:4zE60yDWlfCDtmqnyP5o1k1K0oyhNn3Tvqo6F93/+RU=
github.com/decred/dcrd/blockchain/standalone v1.1.0 h1:yclvVGEY09Gf8A4GSAo+NCtL1dW2TYJ4OKp4+g0ICI0=
github.com/decred/dcrd/blockchain/standalone v1.1.0/go.mod h1:6K8ZgzlWM1Kz2TwXbrtiAvfvIwfAmlzrtpA7CVPCUPE=
github.com/decred/dcrd/blockchain/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:R9rIXU8kEJVC9Z4LAlh9bo9hiT3a+ihys3mCrz4PVao=
Expand All @@ -34,8 +34,8 @@ github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693 h1:zTb6LJaZpYAWItJbo/XY8howiwCceEfWzHZbI06PRNI=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57 h1:agpmzwuPv/iaePlk/2MxXPU6Zz14qy2LY1kWmRCBdAQ=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
Expand All @@ -61,23 +61,23 @@ github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215015031-3283587e6add/go.mod h1:C
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:48ZLpNNrRIYfqYxmvzMgOZrnTZUU3aTJveWtamCkOxo=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693 h1:6Sz3pijZe2cyY8y1hhTmeQDz6Bg+KheIA5Hkks1n8QY=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57 h1://D7EuW5XY8Me8uOHH2PH9APsdUQ0cm3KIyXXIcMYGc=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o=
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e h1:tBOk2P8F9JyRUSp0iRTs4nYEBro1FKBDIbg/UualLWw=
github.com/decred/dcrd/gcs/v2 v2.0.2-0.20200312171759-0a8cc56a776e/go.mod h1:JJGd1m0DrFgV4J2J8HKNB9YVkM06ewQHT6iINis39Z4=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693 h1:8MKtyq4+zxRC/SEtJuF4+qB+Svd5vvHZeG9kbHxg9wY=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57 h1:fGahtE/RfIBYlguw7tG11g1RBMBqqaZKVWFYlccYbX4=
github.com/decred/dcrd/hdkeychain/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:qKN0WzeSEEZ4fUBsTwKzOPkLP7GqSM6jBUm5Auq9mrM=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.0/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984 h1:xfdiilBsDinOLbglqzHH98fOO1iBtVrpSHMVpNK/2lg=
github.com/decred/dcrd/rpc/jsonrpc/types/v2 v2.0.1-0.20200527025017-6fc98347d984/go.mod h1:c5S+PtQWNIA2aUakgrLhrlopkMadcOv51dWhCEdo49c=
github.com/decred/dcrd/txscript/v2 v2.1.0/go.mod h1:XaJAVrZU4NWRx4UEzTiDAs86op1m8GRJLz24SDBKOi0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:ATMA8K0SOo+M9Wdbr6dMnAd8qICJi6pXjGLlKsJc99E=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693 h1:8QJvuZnGXKs1x2VftugOOApMyARYH4tnu4n4o+JASZo=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200528191943-a2771b8ce693/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57 h1:UaIGTrn1bpdZVgULdkwxZhULw5T5Jm3+OPJDw4+/p5c=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200607041702-62fa0661bd57/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE=
github.com/decred/dcrd/wire v1.3.0/go.mod h1:fnKGlUY2IBuqnpxx5dYRU5Oiq392OBqAuVjRVSkIoXM=
github.com/decred/go-socks v1.1.0/go.mod h1:sDhHqkZH0X4JjSa02oYOGhcGHYp12FsY1jQ/meV8md0=
Expand Down Expand Up @@ -105,6 +105,10 @@ github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
Expand Down
69 changes: 69 additions & 0 deletions webapi/admin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package webapi

import (
"net/http"

"github.com/gin-gonic/gin"
"github.com/gorilla/sessions"
)

// adminPage is the handler for "GET /admin". The admin template will be
// rendered if the current session is authenticated as an admin, otherwise the
// login template will be rendered.
func adminPage(c *gin.Context) {
session := c.MustGet("session").(*sessions.Session)
admin := session.Values["admin"]

if admin == nil {
c.HTML(http.StatusUnauthorized, "login.html", gin.H{})
return
}

tickets, err := db.GetAllTickets()
if err != nil {
log.Errorf("GetAllTickets error: %v", err)
c.String(http.StatusInternalServerError, "Error getting tickets from db")
return
}

c.HTML(http.StatusOK, "admin.html", gin.H{
"Tickets": tickets,
})
}

// adminLogin is the handler for "POST /admin". If a valid password is provided,
// the current session will be authenticated as an admin.
func adminLogin(c *gin.Context) {
password := c.PostForm("password")

if password != cfg.AdminPass {
log.Warnf("Failed login attempt from %s", c.ClientIP())
c.HTML(http.StatusUnauthorized, "login.html", gin.H{
"IncorrectPassword": true,
})
return
}

setAdminStatus(true, c)
}

// adminLogout is the handler for "POST /admin/logout". The current session will
// have its admin authentication removed.
func adminLogout(c *gin.Context) {
setAdminStatus(nil, c)
}

// setAdminStatus stores the authentication status of the current session.
func setAdminStatus(admin interface{}, c *gin.Context) {
session := c.MustGet("session").(*sessions.Session)
session.Values["admin"] = admin
err := session.Save(c.Request, c.Writer)
if err != nil {
log.Errorf("Error saving session: %v", err)
c.String(http.StatusInternalServerError, "Error saving session")
return
}

c.Redirect(http.StatusFound, "/admin")
c.Abort()
}
35 changes: 35 additions & 0 deletions webapi/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,51 @@ package webapi

import (
"net/http"
"strings"

"github.com/decred/vspd/rpc"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gorilla/sessions"
)

type ticketHashRequest struct {
TicketHash string `json:"tickethash" binding:"required"`
}

// withSession middleware adds a gorilla session to the request context for
// downstream handlers to make use of. Sessions are used by admin pages to
// maintain authentication status.
func withSession(store *sessions.CookieStore) gin.HandlerFunc {
return func(c *gin.Context) {
session, err := store.Get(c.Request, "vspd-session")
if err != nil {
// "value is not valid" occurs if the cookie secret changes. This is
// common during development (eg. when using the test harness) but
// it should not occur in production.
if strings.Contains(err.Error(), "securecookie: the value is not valid") {
log.Warn("Cookie secret has changed. Generating new session.")

// Persist the newly generated session.
err = store.Save(c.Request, c.Writer, session)
if err != nil {
log.Errorf("Error saving session: %v", err)
c.String(http.StatusInternalServerError, "Error saving session")
c.Abort()
return
}
} else {
log.Errorf("Session error: %v", err)
c.String(http.StatusInternalServerError, "Error getting session")
c.Abort()
return
}
}

c.Set("session", session)
}
}

// withDcrdClient middleware adds a dcrd client to the request
// context for downstream handlers to make use of.
func withDcrdClient() gin.HandlerFunc {
Expand Down
41 changes: 41 additions & 0 deletions webapi/templates/admin.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{{ template "header" . }}

<form action="/admin/logout" method="post">
<button type="submit">Logout</button>
</form>

<table>
<tr>
<td>Hash</td>
<td>CommitmentAddress</td>
<td>FeeAddressIndex</td>
<td>FeeAddress</td>
<td>FeeAmount</td>
<td>FeeExpiration</td>
<td>Confirmed</td>
<td>VoteChoices</td>
<td>VotingWIF</td>
<td>FeeTxHex</td>
<td>FeeTxHash</td>
<td>FeeConfirmed</td>
</tr>
{{ range .Tickets }}
<tr>
<td>{{ printf "%.10s" .Hash }}...</td>
<td>{{ printf "%.10s" .CommitmentAddress }}...</td>
<td>{{ printf "%d" .FeeAddressIndex }}</td>
<td>{{ printf "%.10s" .FeeAddress }}...</td>
<td>{{ printf "%f" .FeeAmount }}</td>
<td>{{ printf "%d" .FeeExpiration }}</td>
<td>{{ printf "%t" .Confirmed }}</td>
<td>{{ printf "%.10s" .VoteChoices }}...</td>
<td>{{ printf "%.10s" .VotingWIF }}...</td>
<td>{{ printf "%.10s" .FeeTxHex }}...</td>
<td>{{ printf "%.10s" .FeeTxHash }}...</td>
<td>{{ printf "%t" .FeeConfirmed }}</td>
</tr>
{{ end }}
</table>

</body>
</html>
45 changes: 45 additions & 0 deletions webapi/templates/header.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{{define "header"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">

<title>vspd</title>

<link rel="stylesheet" type="text/css" href="/public/css/fonts.css" />
<link rel="stylesheet" type="text/css" href="/public/css/vspd.css" />

<!-- Custom favicon -->
<!-- Apple PWA -->
<link rel="apple-touch-icon" sizes="57x57" href="/public/images/favicon/apple-touch-icon-57x57.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="60x60" href="/public/images/favicon/apple-touch-icon-60x60.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="72x72" href="/public/images/favicon/apple-touch-icon-72x72.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="76x76" href="/public/images/favicon/apple-touch-icon-76x76.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="114x114" href="/public/images/favicon/apple-touch-icon-114x114.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="120x120" href="/public/images/favicon/apple-touch-icon-120x120.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="144x144" href="/public/images/favicon/apple-touch-icon-144x144.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="152x152" href="/public/images/favicon/apple-touch-icon-152x152.png?v=gT6Mc">
<link rel="apple-touch-icon" sizes="180x180" href="/public/images/favicon/apple-touch-icon-180x180.png?v=gT6Mc">

<!-- Browser -->
<link rel="icon" href="/public/images/favicon/favicon.ico?v=gT6Mc">
<link rel="icon" href="/public/images/favicon/favicon-32x32.png?v=gT6Mc" type="image/png" sizes="32x32">
<link rel="icon" href="/public/images/favicon/favicon-16x16.png?v=gT6Mc" type="image/png" sizes="16x16">

<!-- Android PWA -->
<link rel="manifest" href="/public/images/favicon/manifest.json?v=gT6Mc">

<!-- Safari -->
<link rel="mask-icon" href="/public/images/favicon/safari-pinned-tab.svg?v=gT6Mc" color="#091440">

<!-- Windows PWA -->
<meta name="msapplication-TileColor" content="#091440">
<meta name="msapplication-TileImage" content="/public/images/favicon/mstile-144x144.png?v=gT6Mc">
<meta name="msapplication-config" content="/public/images/favicon/browserconfig.xml?v=gT6Mc">
<!-- End custom favicon -->
</head>

<body>
<img src="/public/images/decred-logo.svg" />
{{end}}
Loading

0 comments on commit 5904cdb

Please sign in to comment.