mirror of https://github.com/jlelse/GoBlog
Save notifications to DB, Notificationsadmin
This commit is contained in:
parent
a114d4d8d9
commit
be929058cf
|
@ -130,6 +130,15 @@ func migrateDb() error {
|
|||
return err
|
||||
},
|
||||
},
|
||||
&migrator.Migration{
|
||||
Name: "00011",
|
||||
Func: func(tx *sql.Tx) error {
|
||||
_, err := tx.Exec(`
|
||||
create table notifications (id integer primary key autoincrement, time integer not null, text text not null);
|
||||
`)
|
||||
return err
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
9
go.mod
9
go.mod
|
@ -43,18 +43,19 @@ require (
|
|||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.7.1
|
||||
github.com/tdewolff/minify/v2 v2.9.13
|
||||
github.com/tdewolff/parse/v2 v2.5.11 // indirect
|
||||
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
|
||||
github.com/vcraescu/go-paginator v1.0.1-0.20201114172518-2cfc59fe05c2
|
||||
github.com/yuin/goldmark v1.3.2
|
||||
github.com/yuin/goldmark-emoji v1.0.1
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.16.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 // indirect
|
||||
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect
|
||||
golang.org/x/mod v0.4.1 // indirect
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 // indirect
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 // indirect
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 // indirect
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
|
|
19
go.sum
19
go.sum
|
@ -284,8 +284,9 @@ github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s
|
|||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tdewolff/minify/v2 v2.9.13 h1:RrwQhgGoYBhKN/ezStGB+crU64wPK1ZE5Jmkl63lif0=
|
||||
github.com/tdewolff/minify/v2 v2.9.13/go.mod h1:faNOp+awAoo+fhFHD+NAkBOaXBAvJI2X2SDERGKnARo=
|
||||
github.com/tdewolff/parse/v2 v2.5.10 h1:vj35n+ljq8LuYUx436s4qB18wuwP7thrLv+t1syE39M=
|
||||
github.com/tdewolff/parse/v2 v2.5.10/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/parse/v2 v2.5.11 h1:Wq0x026IKZh9GPUB5Fp+v5bki/SNmpIkdltcnm6HrO0=
|
||||
github.com/tdewolff/parse/v2 v2.5.11/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
|
||||
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=
|
||||
github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
|
@ -324,8 +325,8 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
@ -370,8 +371,8 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
|
@ -381,8 +382,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
|||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
|
@ -404,8 +405,8 @@ golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341 h1:2/QtM1mL37YmcsT8HaDNHDgTqqFVw+zr8UzMiBVLzYU=
|
||||
golang.org/x/sys v0.0.0-20210217105451-b926d437f341/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf h1:MZ2shdL+ZM/XzY3ZGOnh4Nlpnxz5GSOhOmtHo3iPU6M=
|
||||
|
|
9
http.go
9
http.go
|
@ -144,6 +144,15 @@ func buildHandler() (http.Handler, error) {
|
|||
})
|
||||
})
|
||||
|
||||
// Notifications
|
||||
notificationsPath := "/notifications"
|
||||
r.Route(notificationsPath, func(r chi.Router) {
|
||||
r.Use(authMiddleware)
|
||||
handler := notificationsAdmin(notificationsPath)
|
||||
r.Get("/", handler)
|
||||
r.Get(paginationPath, handler)
|
||||
})
|
||||
|
||||
// Posts
|
||||
pp, err := allPostPaths(statusPublished)
|
||||
if err != nil {
|
||||
|
|
146
notifications.go
146
notifications.go
|
@ -1,15 +1,157 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/vcraescu/go-paginator"
|
||||
)
|
||||
|
||||
type notification struct {
|
||||
ID int
|
||||
Time int64
|
||||
Text string
|
||||
}
|
||||
|
||||
func sendNotification(text string) {
|
||||
log.Println("Notification:", text)
|
||||
n := ¬ification{
|
||||
Time: time.Now().Unix(),
|
||||
Text: text,
|
||||
}
|
||||
if err := saveNotification(n); err != nil {
|
||||
log.Println("Failed to save notification:", err.Error())
|
||||
}
|
||||
if appConfig.Notifications.Telegram.Enabled {
|
||||
err := sendTelegramMessage(text, "", appConfig.Notifications.Telegram.BotToken, appConfig.Notifications.Telegram.ChatID)
|
||||
err := sendTelegramMessage(n.Text, "", appConfig.Notifications.Telegram.BotToken, appConfig.Notifications.Telegram.ChatID)
|
||||
if err != nil {
|
||||
log.Println("Failed to send Telegram notification:", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func saveNotification(n *notification) error {
|
||||
if _, err := appDbExec("insert into notifications (time, text) values (@time, @text)", sql.Named("time", n.Time), sql.Named("text", n.Text)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type notificationsRequestConfig struct {
|
||||
offset, limit int
|
||||
}
|
||||
|
||||
func buildNotificationsQuery(config *notificationsRequestConfig) (query string, args []interface{}) {
|
||||
args = []interface{}{}
|
||||
query = "select id, time, text from notifications order by id desc"
|
||||
if config.limit != 0 || config.offset != 0 {
|
||||
query += " limit @limit offset @offset"
|
||||
args = append(args, sql.Named("limit", config.limit), sql.Named("offset", config.offset))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func getNotifications(config *notificationsRequestConfig) ([]*notification, error) {
|
||||
notifications := []*notification{}
|
||||
query, args := buildNotificationsQuery(config)
|
||||
rows, err := appDbQuery(query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
n := ¬ification{}
|
||||
err = rows.Scan(&n.ID, &n.Time, &n.Text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
notifications = append(notifications, n)
|
||||
}
|
||||
return notifications, nil
|
||||
}
|
||||
|
||||
func countNotifications(config *notificationsRequestConfig) (count int, err error) {
|
||||
query, params := buildNotificationsQuery(config)
|
||||
query = "select count(*) from (" + query + ")"
|
||||
row, err := appDbQueryRow(query, params...)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = row.Scan(&count)
|
||||
return
|
||||
}
|
||||
|
||||
type notificationsPaginationAdapter struct {
|
||||
config *notificationsRequestConfig
|
||||
nums int64
|
||||
}
|
||||
|
||||
func (p *notificationsPaginationAdapter) Nums() (int64, error) {
|
||||
if p.nums == 0 {
|
||||
nums, _ := countNotifications(p.config)
|
||||
p.nums = int64(nums)
|
||||
}
|
||||
return p.nums, nil
|
||||
}
|
||||
|
||||
func (p *notificationsPaginationAdapter) Slice(offset, length int, data interface{}) error {
|
||||
modifiedConfig := *p.config
|
||||
modifiedConfig.offset = offset
|
||||
modifiedConfig.limit = length
|
||||
|
||||
notifications, err := getNotifications(&modifiedConfig)
|
||||
reflect.ValueOf(data).Elem().Set(reflect.ValueOf(¬ifications).Elem())
|
||||
return err
|
||||
}
|
||||
|
||||
func notificationsAdmin(notificationPath string) func(http.ResponseWriter, *http.Request) {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Adapter
|
||||
pageNoString := chi.URLParam(r, "page")
|
||||
pageNo, _ := strconv.Atoi(pageNoString)
|
||||
p := paginator.New(¬ificationsPaginationAdapter{config: ¬ificationsRequestConfig{}}, 10)
|
||||
p.SetPage(pageNo)
|
||||
var notifications []*notification
|
||||
err := p.Results(¬ifications)
|
||||
if err != nil {
|
||||
serveError(w, r, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Navigation
|
||||
var hasPrev, hasNext bool
|
||||
var prevPage, nextPage int
|
||||
var prevPath, nextPath string
|
||||
hasPrev, _ = p.HasPrev()
|
||||
if hasPrev {
|
||||
prevPage, _ = p.PrevPage()
|
||||
} else {
|
||||
prevPage, _ = p.Page()
|
||||
}
|
||||
if prevPage < 2 {
|
||||
prevPath = notificationPath
|
||||
} else {
|
||||
prevPath = fmt.Sprintf("%s/page/%d", notificationPath, prevPage)
|
||||
}
|
||||
hasNext, _ = p.HasNext()
|
||||
if hasNext {
|
||||
nextPage, _ = p.NextPage()
|
||||
} else {
|
||||
nextPage, _ = p.Page()
|
||||
}
|
||||
nextPath = fmt.Sprintf("%s/page/%d", notificationPath, nextPage)
|
||||
// Render
|
||||
render(w, templateNotificationsAdmin, &renderData{
|
||||
Data: map[string]interface{}{
|
||||
"Notifications": notifications,
|
||||
"HasPrev": hasPrev,
|
||||
"HasNext": hasNext,
|
||||
"Prev": slashIfEmpty(prevPath),
|
||||
"Next": slashIfEmpty(nextPath),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
37
render.go
37
render.go
|
@ -19,24 +19,27 @@ import (
|
|||
"github.com/goodsign/monday"
|
||||
)
|
||||
|
||||
const templatesDir = "templates"
|
||||
const templatesExt = ".gohtml"
|
||||
const (
|
||||
templatesDir = "templates"
|
||||
templatesExt = ".gohtml"
|
||||
|
||||
const templateBase = "base"
|
||||
const templatePost = "post"
|
||||
const templateError = "error"
|
||||
const templateIndex = "index"
|
||||
const templateTaxonomy = "taxonomy"
|
||||
const templateSearch = "search"
|
||||
const templateSummary = "summary"
|
||||
const templatePhotosSummary = "photosummary"
|
||||
const templateEditor = "editor"
|
||||
const templateLogin = "login"
|
||||
const templateStaticHome = "statichome"
|
||||
const templateBlogStats = "blogstats"
|
||||
const templateComment = "comment"
|
||||
const templateCaptcha = "captcha"
|
||||
const templateCommentsAdmin = "commentsadmin"
|
||||
templateBase = "base"
|
||||
templatePost = "post"
|
||||
templateError = "error"
|
||||
templateIndex = "index"
|
||||
templateTaxonomy = "taxonomy"
|
||||
templateSearch = "search"
|
||||
templateSummary = "summary"
|
||||
templatePhotosSummary = "photosummary"
|
||||
templateEditor = "editor"
|
||||
templateLogin = "login"
|
||||
templateStaticHome = "statichome"
|
||||
templateBlogStats = "blogstats"
|
||||
templateComment = "comment"
|
||||
templateCaptcha = "captcha"
|
||||
templateCommentsAdmin = "commentsadmin"
|
||||
templateNotificationsAdmin = "notificationsadmin"
|
||||
)
|
||||
|
||||
var templates map[string]*template.Template
|
||||
var templateFunctions template.FuncMap
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
Target: <a href="{{ $comment.Target }}" target="_blank">{{ $comment.Target }}</a><br/>
|
||||
Name: {{ if $comment.Website }}<a href="{{ $comment.Website }}" target="_blank" rel="nofollow noopener noreferrer ugc">{{ $comment.Name }}</a>{{ else }}{{ $comment.Name }}{{ end }}
|
||||
</p>
|
||||
<p class="e-content">
|
||||
<p>
|
||||
{{ html $comment.Comment }}
|
||||
</p>
|
||||
<form method="post">
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ string .Blog.Lang "notifications" }} - {{ .Blog.Title }}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main>
|
||||
<h1>{{ string .Blog.Lang "notifications" }}</h1>
|
||||
{{ range $i, $notification := .Data.Notifications }}
|
||||
<p>
|
||||
ID: {{ $notification.ID }}<br/>
|
||||
Time: {{ unixtodate $notification.Time }}<br/>
|
||||
Text: {{ $notification.Text }}
|
||||
</p>
|
||||
{{ end }}
|
||||
{{ if .Data.HasPrev }}
|
||||
<p><a href="{{ .Data.Prev }}">{{ string .Blog.Lang "prev" }}</a></p>
|
||||
{{ end }}
|
||||
{{ if .Data.HasNext }}
|
||||
<p><a href="{{ .Data.Next }}">{{ string .Blog.Lang "next" }}</a></p>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "notificationsadmin" }}
|
||||
{{ template "base" . }}
|
||||
{{ end }}
|
|
@ -19,6 +19,7 @@ likeof: "Like of"
|
|||
login: "Login"
|
||||
nameopt: "Name (optional)"
|
||||
next: "Next"
|
||||
notifications: "Notifications"
|
||||
oldcontent: "⚠️ This entry is already over one year old. It may no longer be up to date. Opinions may have changed."
|
||||
password: "Password"
|
||||
prev: "Previous"
|
||||
|
|
Loading…
Reference in New Issue