parent
a853f227e1
commit
cc5bd6f022
|
@ -26,7 +26,7 @@ Configuration
|
|||
|
||||
Create your first administrator account with create_user:
|
||||
~~~
|
||||
./createuser -admin -config {{BLOG_CONFIG}} -username test -email test@example.com -displayname "Hello World" -password secret1234
|
||||
./createuser -admin -sqlite path_to_sqlite -username test -email test@example.com -displayname "Hello World" -password secret1234
|
||||
~~~
|
||||
|
||||
Make sure -admin is set.
|
||||
|
|
|
@ -40,6 +40,7 @@ func main() {
|
|||
password := flag.String("password", "", "Password for the admin user ")
|
||||
email := flag.String("email", "", "Email for the created user ")
|
||||
displayName := flag.String("displayname", "", "Display name for the admin user ")
|
||||
isAdmin := flag.Bool("admin", false, "If set a new administrator will be created; otherwise a non-admin is created")
|
||||
file := flag.String("sqlite", "", "Location to the sqlite3 database file")
|
||||
|
||||
flag.Parse()
|
||||
|
@ -50,6 +51,7 @@ func main() {
|
|||
password: *password,
|
||||
email: *email,
|
||||
displayName: *displayName,
|
||||
admin: *isAdmin,
|
||||
sqlite: *file,
|
||||
}
|
||||
|
||||
|
@ -102,6 +104,7 @@ func (userFlags createUserFlag) CreateUser() error {
|
|||
DisplayName: userFlags.displayName,
|
||||
Email: userFlags.email,
|
||||
PlainPassword: []byte(userFlags.password),
|
||||
IsAdmin: userFlags.admin,
|
||||
Active: true,
|
||||
}
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ func (dbFlags initDatabaseFlags) initSQLite() error {
|
|||
"display_name VARCHAR(191) NOT NULL, " +
|
||||
"password CHAR(60) NOT NULL, " +
|
||||
"salt CHAR(32) NOT NULL, " +
|
||||
"is_admin boolean NOT NULL DEFAULT false, " +
|
||||
"active boolean NOT NULL DEFAULT true, " +
|
||||
"last_modified datetime NOT NULL," +
|
||||
"CONSTRAINT user_email_key UNIQUE (username), " +
|
||||
|
|
|
@ -29,19 +29,22 @@ func AdminProfileHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *h
|
|||
//AdminProfilePostHandler handles the updating of the user profile
|
||||
func AdminProfilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
|
||||
ctxUser, _ := middleware.User(r)
|
||||
ctxUser.PlainPassword = []byte(r.FormValue("current_password"))
|
||||
|
||||
u := &models.User{
|
||||
ID: ctxUser.ID,
|
||||
Username: r.FormValue("username"),
|
||||
Email: r.FormValue("email"),
|
||||
DisplayName: r.FormValue("displayname"),
|
||||
Active: true,
|
||||
ID: ctxUser.ID,
|
||||
Username: r.FormValue("username"),
|
||||
Email: r.FormValue("email"),
|
||||
DisplayName: r.FormValue("displayname"),
|
||||
Active: true,
|
||||
IsAdmin: ctxUser.IsAdmin,
|
||||
PlainPassword: []byte(r.FormValue("password")),
|
||||
}
|
||||
|
||||
if _, err := ctx.UserService.Authenticate(ctxUser, ctx.ConfigService.LoginMethod, []byte(r.PostFormValue("current_password"))); err != nil {
|
||||
if _, err := ctx.UserService.Authenticate(ctxUser, ctx.ConfigService.LoginMethod); err != nil {
|
||||
return &middleware.Template{
|
||||
Name: tplAdminProfile,
|
||||
Err: httperror.New(http.StatusUnauthorized, "Your password is invalid.", err),
|
||||
Err: httperror.New(http.StatusUnauthorized, "Your current password is invalid.", err),
|
||||
Active: "profile",
|
||||
Data: map[string]interface{}{
|
||||
"user": u,
|
||||
|
@ -51,12 +54,12 @@ func AdminProfilePostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
|
|||
|
||||
changePassword := false
|
||||
|
||||
if len(r.FormValue("password")) > 0 {
|
||||
if len(u.PlainPassword) > 0 {
|
||||
changePassword = true
|
||||
// Password change
|
||||
u.Password = []byte(r.FormValue("password"))
|
||||
u.PlainPassword = []byte(r.FormValue("password"))
|
||||
|
||||
if !bytes.Equal(u.Password, []byte(r.FormValue("retyped_password"))) {
|
||||
if !bytes.Equal(u.PlainPassword, []byte(r.FormValue("retyped_password"))) {
|
||||
return &middleware.Template{
|
||||
Name: tplAdminProfile,
|
||||
Active: "profile",
|
||||
|
|
|
@ -47,11 +47,12 @@ func LoginPostHandler(ctx *middleware.AppContext, rw http.ResponseWriter, r *htt
|
|||
}
|
||||
|
||||
u := &models.User{
|
||||
Username: username,
|
||||
Email: username,
|
||||
Username: username,
|
||||
Email: username,
|
||||
PlainPassword: password,
|
||||
}
|
||||
|
||||
user, err := ctx.UserService.Authenticate(u, ctx.ConfigService.LoginMethod, password)
|
||||
user, err := ctx.UserService.Authenticate(u, ctx.ConfigService.LoginMethod)
|
||||
|
||||
if err != nil {
|
||||
return &middleware.Template{
|
||||
|
|
|
@ -17,8 +17,7 @@ import (
|
|||
func AdminUsersHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *http.Request) *middleware.Template {
|
||||
page := getPageParam(r)
|
||||
|
||||
total, err := ctx.UserService.Count()
|
||||
|
||||
total, err := ctx.UserService.Count(models.All)
|
||||
if err != nil {
|
||||
return &middleware.Template{
|
||||
Name: tplAdminUsers,
|
||||
|
@ -44,17 +43,21 @@ func AdminUsersHandler(ctx *middleware.AppContext, w http.ResponseWriter, r *htt
|
|||
}
|
||||
}
|
||||
|
||||
userInvites, err := ctx.UserInviteService.List()
|
||||
var userInvites []models.UserInvite
|
||||
|
||||
if err != nil {
|
||||
return &middleware.Template{
|
||||
Name: tplAdminUsers,
|
||||
Err: err,
|
||||
Active: "users",
|
||||
Data: map[string]interface{}{
|
||||
"users": users,
|
||||
"pagination": p,
|
||||
},
|
||||
if cu, _ := middleware.User(r); cu.IsAdmin {
|
||||
userInvites, err = ctx.UserInviteService.List()
|
||||
|
||||
if err != nil {
|
||||
return &middleware.Template{
|
||||
Name: tplAdminUsers,
|
||||
Err: err,
|
||||
Active: "users",
|
||||
Data: map[string]interface{}{
|
||||
"users": users,
|
||||
"pagination": p,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,6 +88,7 @@ func AdminUserNewPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
|
|||
Email: r.FormValue("email"),
|
||||
PlainPassword: []byte(r.FormValue("password")),
|
||||
Active: convertCheckbox(r, "active"),
|
||||
IsAdmin: convertCheckbox(r, "admin"),
|
||||
}
|
||||
|
||||
userID, err := ctx.UserService.Create(u)
|
||||
|
@ -157,6 +161,7 @@ func AdminUserEditPostHandler(ctx *middleware.AppContext, w http.ResponseWriter,
|
|||
Username: r.FormValue("username"),
|
||||
PlainPassword: []byte(r.FormValue("password")),
|
||||
Active: convertCheckbox(r, "active"),
|
||||
IsAdmin: convertCheckbox(r, "admin"),
|
||||
}
|
||||
|
||||
changePassword := false
|
||||
|
@ -197,23 +202,15 @@ func AdminUserDeleteHandler(ctx *middleware.AppContext, w http.ResponseWriter, r
|
|||
}
|
||||
}
|
||||
|
||||
oneUser, err := ctx.UserService.OneUser()
|
||||
oneAdmin, err := ctx.UserService.OneAdmin()
|
||||
|
||||
if err != nil {
|
||||
return &middleware.Template{
|
||||
RedirectPath: "admin/users",
|
||||
Active: "users",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
if oneUser {
|
||||
if oneAdmin && user.IsAdmin {
|
||||
return &middleware.Template{
|
||||
RedirectPath: "admin/users",
|
||||
Active: "users",
|
||||
Err: httperror.New(http.StatusUnprocessableEntity,
|
||||
"Could not remove user. No user would remain.",
|
||||
fmt.Errorf("could not remove user %s no user would remain", user.Username)),
|
||||
"Could not remove administrator. No Administrator would remain.",
|
||||
fmt.Errorf("could not remove administrator %s no administrator would remain", user.Username)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -163,7 +163,9 @@ func (ctx AppContext) AuthHandler(handler http.Handler) http.Handler {
|
|||
//RequireAdmin ensures that the user is an admin; if not next handler in chain is not called
|
||||
func (ctx AppContext) RequireAdmin(handler http.Handler) http.Handler {
|
||||
fn := func(rw http.ResponseWriter, r *http.Request) {
|
||||
if _, err := User(r); err != nil {
|
||||
u, err := User(r)
|
||||
|
||||
if err != nil {
|
||||
logger.Log.Error(err)
|
||||
ctx.Templates.ExecuteTemplate(rw, "admin/error", map[string]interface{}{
|
||||
"ErrorMsg": "An internal server error occured",
|
||||
|
@ -171,6 +173,14 @@ func (ctx AppContext) RequireAdmin(handler http.Handler) http.Handler {
|
|||
return
|
||||
}
|
||||
|
||||
if u.IsAdmin == false {
|
||||
ctx.Templates.ExecuteTemplate(rw, "admin/error", map[string]interface{}{
|
||||
"ErrorMsg": "You have not the permissions to execute this action",
|
||||
"currentUser": u,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
handler.ServeHTTP(rw, r)
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
|
|
|
@ -128,11 +128,17 @@ func (as ArticleService) Update(a *Article, u *User) error {
|
|||
return err
|
||||
}
|
||||
|
||||
_, err := as.Datasource.Get(a.ID, a.Author, All)
|
||||
art, err := as.Datasource.Get(a.ID, a.Author, All)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
if art.Author.ID != u.ID {
|
||||
return httperror.PermissionDenied("update", "article", fmt.Errorf("could not update article %d user %d has no permission", a.ID, u.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return as.Datasource.Update(a)
|
||||
}
|
||||
|
||||
|
@ -144,6 +150,12 @@ func (as ArticleService) Publish(id int, u *User) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
if a.Author.ID != u.ID {
|
||||
return httperror.PermissionDenied("publish", "article", fmt.Errorf("could not publish article %d user %d has no permission", a.ID, u.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return as.Datasource.Publish(a)
|
||||
}
|
||||
|
||||
|
@ -155,6 +167,12 @@ func (as ArticleService) Delete(id int, u *User) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
if a.Author.ID != u.ID {
|
||||
return httperror.PermissionDenied("delete", "article", fmt.Errorf("could not delete article %d user %d has no permission", a.ID, u.ID))
|
||||
}
|
||||
}
|
||||
|
||||
return as.Datasource.Delete(a.ID)
|
||||
}
|
||||
|
||||
|
@ -170,6 +188,14 @@ func (as ArticleService) GetBySlug(s string, u *User, pc PublishedCriteria) (*Ar
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
if a.Author.ID != u.ID {
|
||||
return nil, httperror.PermissionDenied("view", "article", fmt.Errorf("could not get article %s user %d has no permission", a.Slug, u.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
|
@ -185,6 +211,14 @@ func (as ArticleService) GetByID(id int, u *User, pc PublishedCriteria) (*Articl
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
if a.Author.ID != u.ID {
|
||||
return nil, httperror.PermissionDenied("get", "article", fmt.Errorf("could not get article %d user %d has no permission", a.ID, u.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@ func (rdb SQLiteArticleDatasource) List(u *User, c *Category, p *Pagination, pc
|
|||
var ru User
|
||||
|
||||
if err := rows.Scan(&a.ID, &a.Headline, &a.Teaser, &a.Content, &a.Published, &a.PublishedOn, &a.Slug, &a.LastModified, &ru.ID, &ru.DisplayName,
|
||||
&ru.Email, &ru.Username, &a.CID, &a.CName); err != nil {
|
||||
&ru.Email, &ru.Username, &ru.IsAdmin, &a.CID, &a.CName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -95,6 +95,13 @@ func (rdb SQLiteArticleDatasource) Count(u *User, c *Category, pc PublishedCrite
|
|||
args = append(args, c.Name)
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
stmt.WriteString("a.user_id=? AND ")
|
||||
args = append(args, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if pc == NotPublished {
|
||||
stmt.WriteString("a.published = '0' ")
|
||||
} else if pc == All {
|
||||
|
@ -117,7 +124,7 @@ func (rdb SQLiteArticleDatasource) Get(articleID int, u *User, pc PublishedCrite
|
|||
var ru User
|
||||
|
||||
if err := selectArticleStmt(rdb.SQLConn, articleID, "", u, pc).Scan(&a.ID, &a.Headline, &a.PublishedOn, &a.Published, &a.Slug, &a.Teaser, &a.Content,
|
||||
&a.LastModified, &ru.ID, &ru.DisplayName, &ru.Email, &ru.Username); err != nil {
|
||||
&a.LastModified, &ru.ID, &ru.DisplayName, &ru.Email, &ru.Username, &ru.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -133,7 +140,7 @@ func (rdb SQLiteArticleDatasource) GetBySlug(slug string, u *User, pc PublishedC
|
|||
var ru User
|
||||
|
||||
if err := selectArticleStmt(rdb.SQLConn, -1, slug, u, pc).Scan(&a.ID, &a.Headline, &a.PublishedOn, &a.Published, &a.Slug, &a.Teaser, &a.Content,
|
||||
&a.LastModified, &ru.ID, &ru.DisplayName, &ru.Email, &ru.Username); err != nil {
|
||||
&a.LastModified, &ru.ID, &ru.DisplayName, &ru.Email, &ru.Username, &ru.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -182,7 +189,7 @@ func selectArticleStmt(db *sql.DB, articleID int, slug string, u *User, pc Publi
|
|||
var args []interface{}
|
||||
|
||||
stmt.WriteString("SELECT a.id, a.headline, a.published_on, a.published, a.slug, a.teaser, a.content, a.last_modified, ")
|
||||
stmt.WriteString("u.id, u.display_name, u.email, u.username ")
|
||||
stmt.WriteString("u.id, u.display_name, u.email, u.username, u.is_admin ")
|
||||
stmt.WriteString("FROM article a ")
|
||||
stmt.WriteString("INNER JOIN user u ON (a.user_id = u.id) ")
|
||||
stmt.WriteString("WHERE ")
|
||||
|
@ -202,7 +209,12 @@ func selectArticleStmt(db *sql.DB, articleID int, slug string, u *User, pc Publi
|
|||
stmt.WriteString("AND a.id=? ")
|
||||
args = append(args, articleID)
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
stmt.WriteString("AND a.user_id=? ")
|
||||
args = append(args, u.ID)
|
||||
}
|
||||
}
|
||||
stmt.WriteString("LIMIT 1")
|
||||
return db.QueryRow(stmt.String(), args...)
|
||||
}
|
||||
|
@ -213,7 +225,7 @@ func selectArticlesStmt(db *sql.DB, u *User, c *Category, p *Pagination, pc Publ
|
|||
var args []interface{}
|
||||
|
||||
stmt.WriteString("SELECT a.id, a.headline, a.teaser, a.content, a.published, a.published_on, a.slug, a.last_modified, ")
|
||||
stmt.WriteString("u.id, u.display_name, u.email, u.username, ")
|
||||
stmt.WriteString("u.id, u.display_name, u.email, u.username, u.is_admin, ")
|
||||
stmt.WriteString("c.id, c.name ")
|
||||
stmt.WriteString("FROM article a ")
|
||||
stmt.WriteString("INNER JOIN user u ON (a.user_id = u.id) ")
|
||||
|
@ -231,6 +243,13 @@ func selectArticlesStmt(db *sql.DB, u *User, c *Category, p *Pagination, pc Publ
|
|||
args = append(args, c.Name)
|
||||
}
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
stmt.WriteString("a.user_id=? AND ")
|
||||
args = append(args, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
if pc == NotPublished {
|
||||
stmt.WriteString("a.published='0' ")
|
||||
} else if pc == All {
|
||||
|
|
|
@ -38,7 +38,7 @@ func (rdb SQLiteCategoryDatasource) List(fc FilterCriteria) ([]Category, error)
|
|||
var args []interface{}
|
||||
|
||||
stmt.WriteString("SELECT DISTINCT c.id, c.name, c.slug, c.last_modified, ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email, u.is_admin ")
|
||||
stmt.WriteString("FROM category as c ")
|
||||
stmt.WriteString("INNER JOIN user as u ")
|
||||
stmt.WriteString("ON c.user_id = u.id ")
|
||||
|
@ -53,7 +53,7 @@ func (rdb SQLiteCategoryDatasource) List(fc FilterCriteria) ([]Category, error)
|
|||
stmt.WriteString("WHERE a.categorie_id IS NULL ")
|
||||
}
|
||||
|
||||
stmt.WriteString("ORDER BY c.name DESC ")
|
||||
stmt.WriteString("ORDER BY c.last_modified DESC ")
|
||||
|
||||
rows, err := rdb.SQLConn.Query(stmt.String(), args...)
|
||||
|
||||
|
@ -69,7 +69,7 @@ func (rdb SQLiteCategoryDatasource) List(fc FilterCriteria) ([]Category, error)
|
|||
var c Category
|
||||
var ru User
|
||||
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Slug, &c.LastModified, &ru.ID, &ru.DisplayName, &ru.Username, &ru.Email); err != nil {
|
||||
if err := rows.Scan(&c.ID, &c.Name, &c.Slug, &c.LastModified, &ru.ID, &ru.DisplayName, &ru.Username, &ru.Email, &ru.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -99,7 +99,7 @@ func (rdb SQLiteCategoryDatasource) Get(categoryID int) (*Category, error) {
|
|||
var stmt bytes.Buffer
|
||||
|
||||
stmt.WriteString("SELECT c.id, c.name, c.slug, c.last_modified, ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email, u.is_admin ")
|
||||
stmt.WriteString("FROM category as c ")
|
||||
stmt.WriteString("INNER JOIN user as u ")
|
||||
stmt.WriteString("ON u.id = c.user_id ")
|
||||
|
@ -109,7 +109,7 @@ func (rdb SQLiteCategoryDatasource) Get(categoryID int) (*Category, error) {
|
|||
var ru User
|
||||
|
||||
if err := rdb.SQLConn.QueryRow(stmt.String(), categoryID).Scan(&c.ID, &c.Name, &c.Slug, &c.LastModified, &ru.ID,
|
||||
&ru.DisplayName, &ru.Username, &ru.Email); err != nil {
|
||||
&ru.DisplayName, &ru.Username, &ru.Email, &ru.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ func (rdb SQLiteCategoryDatasource) GetBySlug(slug string) (*Category, error) {
|
|||
var stmt bytes.Buffer
|
||||
|
||||
stmt.WriteString("SELECT c.id, c.name, c.slug, c.last_modified, ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email, u.is_admin ")
|
||||
stmt.WriteString("FROM category as c ")
|
||||
stmt.WriteString("INNER JOIN user as u ")
|
||||
stmt.WriteString("ON u.id = c.user_id ")
|
||||
|
@ -132,7 +132,7 @@ func (rdb SQLiteCategoryDatasource) GetBySlug(slug string) (*Category, error) {
|
|||
var ru User
|
||||
|
||||
if err := rdb.SQLConn.QueryRow(stmt.String(), slug).Scan(&c.ID, &c.Name, &c.Slug, &c.LastModified, &ru.ID,
|
||||
&ru.DisplayName, &ru.Username, &ru.Email); err != nil {
|
||||
&ru.DisplayName, &ru.Username, &ru.Email, &ru.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -90,6 +91,12 @@ func (fs FileService) Delete(fileID int, location string, u *User) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
if file.Author.ID != u.ID {
|
||||
return httperror.PermissionDenied("delete", "file", fmt.Errorf("could not remove file %d user %d has no permission", fileID, u.ID))
|
||||
}
|
||||
}
|
||||
|
||||
err = fs.Datasource.Delete(fileID)
|
||||
|
||||
if err != nil {
|
||||
|
|
|
@ -19,7 +19,7 @@ func (rdb SQLiteFileDatasource) GetByFilename(filename string, u *User) (*File,
|
|||
var args []interface{}
|
||||
|
||||
stmt.WriteString("SELECT f.id, f.filename, f.content_type, f.size, f.last_modified, f.user_id, ")
|
||||
stmt.WriteString("u.display_name, u.username, u.email ")
|
||||
stmt.WriteString("u.display_name, u.username, u.email, u.is_admin ")
|
||||
stmt.WriteString("FROM file as f ")
|
||||
stmt.WriteString("INNER JOIN user as u ")
|
||||
stmt.WriteString("ON u.id = f.user_id ")
|
||||
|
@ -27,11 +27,18 @@ func (rdb SQLiteFileDatasource) GetByFilename(filename string, u *User) (*File,
|
|||
|
||||
args = append(args, filename)
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
stmt.WriteString("AND f.user_id=? ")
|
||||
args = append(args, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
var f File
|
||||
var ru User
|
||||
|
||||
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&f.ID, &f.FullFilename, &f.ContentType, &f.Size, &f.LastModified, &ru.ID,
|
||||
&ru.DisplayName, &ru.Username, &ru.Email); err != nil {
|
||||
&ru.DisplayName, &ru.Username, &ru.Email, &ru.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -48,7 +55,7 @@ func (rdb SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
|
|||
var args []interface{}
|
||||
|
||||
stmt.WriteString("SELECT f.id, f.filename, f.content_type, f.size, f.last_modified, f.user_id, ")
|
||||
stmt.WriteString("u.display_name, u.username, u.email ")
|
||||
stmt.WriteString("u.display_name, u.username, u.email, u.is_admin ")
|
||||
stmt.WriteString("FROM file as f ")
|
||||
stmt.WriteString("INNER JOIN user as u ")
|
||||
stmt.WriteString("ON u.id = f.user_id ")
|
||||
|
@ -56,11 +63,18 @@ func (rdb SQLiteFileDatasource) Get(fileID int, u *User) (*File, error) {
|
|||
|
||||
args = append(args, fileID)
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
stmt.WriteString("AND f.user_id=? ")
|
||||
args = append(args, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
var f File
|
||||
var ru User
|
||||
|
||||
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&f.ID, &f.FullFilename, &f.ContentType, &f.Size, &f.LastModified, &ru.ID,
|
||||
&ru.DisplayName, &ru.Username, &ru.Email); err != nil {
|
||||
&ru.DisplayName, &ru.Username, &ru.Email, &ru.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -95,10 +109,18 @@ func (rdb SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
|
|||
var args []interface{}
|
||||
|
||||
stmt.WriteString("SELECT f.id, f.filename, f.content_type, f.size, f.last_modified, ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email ")
|
||||
stmt.WriteString("u.id, u.display_name, u.username, u.email, u.is_admin ")
|
||||
stmt.WriteString("FROM file as f ")
|
||||
stmt.WriteString("INNER JOIN user as u ")
|
||||
stmt.WriteString("ON f.user_id = u.id ")
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
stmt.WriteString("WHERE f.user_id=? ")
|
||||
args = append(args, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
stmt.WriteString("ORDER BY f.last_modified DESC ")
|
||||
|
||||
if p != nil {
|
||||
|
@ -121,7 +143,7 @@ func (rdb SQLiteFileDatasource) List(u *User, p *Pagination) ([]File, error) {
|
|||
|
||||
for rows.Next() {
|
||||
if err = rows.Scan(&f.ID, &f.FullFilename, &f.ContentType, &f.Size, &f.LastModified, &us.ID, &us.DisplayName,
|
||||
&us.Username, &us.Email); err != nil {
|
||||
&us.Username, &us.Email, &u.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -147,6 +169,13 @@ func (rdb SQLiteFileDatasource) Count(u *User) (int, error) {
|
|||
|
||||
stmt.WriteString("SELECT count(id) FROM file ")
|
||||
|
||||
if u != nil {
|
||||
if !u.IsAdmin {
|
||||
stmt.WriteString("WHERE user_id = ?")
|
||||
args = append(args, u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
var total int
|
||||
|
||||
if err := rdb.SQLConn.QueryRow(stmt.String(), args...).Scan(&total); err != nil {
|
||||
|
|
|
@ -6,6 +6,7 @@ package models
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
@ -24,7 +25,7 @@ type UserDatasourceService interface {
|
|||
List(p *Pagination) ([]User, error)
|
||||
Get(userID int) (*User, error)
|
||||
Update(u *User, changePassword bool) error
|
||||
Count() (int, error)
|
||||
Count(ac AdminCriteria) (int, error)
|
||||
GetByMail(mail string) (*User, error)
|
||||
GetByUsername(username string) (*User, error)
|
||||
Remove(userID int) error
|
||||
|
@ -41,6 +42,7 @@ type User struct {
|
|||
Salt []byte
|
||||
LastModified time.Time
|
||||
Active bool
|
||||
IsAdmin bool
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -161,8 +163,8 @@ func (us UserService) duplicateUsername(username string) error {
|
|||
}
|
||||
|
||||
//Count returns the amount of users
|
||||
func (us UserService) Count() (int, error) {
|
||||
return us.Datasource.Count()
|
||||
func (us UserService) Count(a AdminCriteria) (int, error) {
|
||||
return us.Datasource.Count(a)
|
||||
}
|
||||
|
||||
//List returns a list of users. Limits the amount based on the defined pagination
|
||||
|
@ -261,7 +263,14 @@ func (us UserService) Update(u *User, changePassword bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !oldUser.IsAdmin {
|
||||
if oldUser.ID != u.ID {
|
||||
return httperror.PermissionDenied("update", "user", fmt.Errorf("permission denied user %d is not granted to update user %d", oldUser.ID, u.ID))
|
||||
}
|
||||
}
|
||||
|
||||
if us.UserInterceptor != nil {
|
||||
|
||||
if err := us.UserInterceptor.PreUpdate(oldUser, u); err != nil {
|
||||
return httperror.InternalServerError(fmt.Errorf("error while executing user interceptor 'PreUpdate' error %v", err))
|
||||
}
|
||||
|
@ -285,17 +294,18 @@ func (us UserService) Update(u *User, changePassword bool) error {
|
|||
return err
|
||||
}
|
||||
|
||||
oneAdmin, err := us.OneUser()
|
||||
oneAdmin, err := us.OneAdmin()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oneAdmin && !u.Active {
|
||||
return httperror.New(http.StatusUnprocessableEntity,
|
||||
"Could not update user, because no user would remain",
|
||||
fmt.Errorf("could not update user %s action, because no administrator would remain", oldUser.Username))
|
||||
|
||||
if oneAdmin {
|
||||
if (oldUser.IsAdmin && !u.IsAdmin) || (oldUser.IsAdmin && !u.Active) {
|
||||
return httperror.New(http.StatusUnprocessableEntity,
|
||||
"Could not update user, because no administrator would remain",
|
||||
fmt.Errorf("could not update user %s action, because no administrator would remain", oldUser.Username))
|
||||
}
|
||||
}
|
||||
|
||||
if changePassword {
|
||||
|
@ -327,25 +337,33 @@ func (us UserService) Update(u *User, changePassword bool) error {
|
|||
|
||||
// Authenticate authenticates the user by the given login method (email or username)
|
||||
// if the user was found but the password is wrong the found user and an error will be returned
|
||||
func (us UserService) Authenticate(u *User, loginMethod settings.LoginMethod, password []byte) (*User, error) {
|
||||
func (us UserService) Authenticate(u *User, loginMethod settings.LoginMethod) (*User, error) {
|
||||
var err error
|
||||
|
||||
if len(u.Username) == 0 || len(u.PlainPassword) == 0 {
|
||||
return nil, httperror.New(http.StatusUnauthorized, "Your username or password is invalid.", errors.New("no username or password were given"))
|
||||
}
|
||||
|
||||
var password = u.PlainPassword
|
||||
|
||||
if loginMethod == settings.EMail {
|
||||
u, err = us.Datasource.GetByMail(u.Email)
|
||||
} else {
|
||||
u, err = us.Datasource.GetByUsername(u.Username)
|
||||
}
|
||||
|
||||
u.PlainPassword = password
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
//Do some extra work
|
||||
bcrypt.CompareHashAndPassword([]byte("$2a$12$bQlRnXTNZMp6kCyoAlnf3uZW5vtmSj9CHP7pYplRUVK2n0C5xBHBa"), password)
|
||||
bcrypt.CompareHashAndPassword([]byte("$2a$12$bQlRnXTNZMp6kCyoAlnf3uZW5vtmSj9CHP7pYplRUVK2n0C5xBHBa"), u.PlainPassword)
|
||||
return nil, httperror.New(http.StatusUnauthorized, "Your username or password is invalid.", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := u.comparePassword(password); err != nil {
|
||||
if err := u.comparePassword(); err != nil {
|
||||
return u, httperror.New(http.StatusUnauthorized, "Your username or password is invalid.", err)
|
||||
}
|
||||
|
||||
|
@ -355,6 +373,10 @@ func (us UserService) Authenticate(u *User, loginMethod settings.LoginMethod, pa
|
|||
fmt.Errorf("the user with id %d tried to logged in but the account is deactivated", u.ID))
|
||||
}
|
||||
|
||||
u.PlainPassword = nil
|
||||
u.Password = nil
|
||||
u.Salt = nil
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
|
@ -366,16 +388,18 @@ func (us UserService) Remove(u *User) error {
|
|||
}
|
||||
}
|
||||
|
||||
oneAdmin, err := us.OneUser()
|
||||
oneAdmin, err := us.OneAdmin()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if oneAdmin {
|
||||
return httperror.New(http.StatusUnprocessableEntity,
|
||||
"Could not remove administrator. No Administrator would remain.",
|
||||
fmt.Errorf("could not remove administrator %s no administrator would remain", u.Username))
|
||||
if u.IsAdmin {
|
||||
return httperror.New(http.StatusUnprocessableEntity,
|
||||
"Could not remove administrator. No Administrator would remain.",
|
||||
fmt.Errorf("could not remove administrator %s no administrator would remain", u.Username))
|
||||
}
|
||||
}
|
||||
|
||||
err = us.Datasource.Remove(u.ID)
|
||||
|
@ -390,8 +414,8 @@ func (us UserService) Remove(u *User) error {
|
|||
}
|
||||
|
||||
//OneAdmin returns true if there is only one admin
|
||||
func (us UserService) OneUser() (bool, error) {
|
||||
c, err := us.Datasource.Count()
|
||||
func (us UserService) OneAdmin() (bool, error) {
|
||||
c, err := us.Datasource.Count(OnlyAdmins)
|
||||
|
||||
if err != nil {
|
||||
return true, err
|
||||
|
@ -404,6 +428,6 @@ func (us UserService) OneUser() (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
func (u User) comparePassword(password []byte) error {
|
||||
return bcrypt.CompareHashAndPassword(u.Password, utils.AppendBytes(password, u.Salt))
|
||||
func (u User) comparePassword() error {
|
||||
return bcrypt.CompareHashAndPassword(u.Password, utils.AppendBytes(u.PlainPassword, u.Salt))
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ func (ui UserInvite) Copy() *User {
|
|||
Username: ui.Username,
|
||||
Email: ui.Email,
|
||||
DisplayName: ui.DisplayName,
|
||||
IsAdmin: ui.IsAdmin,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ func (rdb SQLiteUserDatasource) List(p *Pagination) ([]User, error) {
|
|||
var stmt bytes.Buffer
|
||||
var args []interface{}
|
||||
|
||||
stmt.WriteString("SELECT id, username, email, display_name, last_modified, active FROM user ORDER BY username ASC ")
|
||||
stmt.WriteString("SELECT id, username, email, display_name, last_modified, active, is_admin FROM user ORDER BY username ASC ")
|
||||
|
||||
if p != nil {
|
||||
stmt.WriteString("LIMIT ? OFFSET ? ")
|
||||
|
@ -36,7 +36,7 @@ func (rdb SQLiteUserDatasource) List(p *Pagination) ([]User, error) {
|
|||
var u User
|
||||
|
||||
for rows.Next() {
|
||||
if err = rows.Scan(&u.ID, &u.Username, &u.Email, &u.DisplayName, &u.LastModified, &u.Active); err != nil {
|
||||
if err = rows.Scan(&u.ID, &u.Username, &u.Email, &u.DisplayName, &u.LastModified, &u.Active, &u.IsAdmin); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -54,10 +54,10 @@ func (rdb SQLiteUserDatasource) List(p *Pagination) ([]User, error) {
|
|||
func (rdb SQLiteUserDatasource) Get(userID int) (*User, error) {
|
||||
var u User
|
||||
|
||||
if err := rdb.SQLConn.QueryRow("SELECT u.id, u.username, u.email, u.display_name, u.last_modified, u.active, u.salt "+
|
||||
if err := rdb.SQLConn.QueryRow("SELECT u.id, u.username, u.email, u.display_name, u.last_modified, u.active, u.is_admin, u.salt "+
|
||||
"FROM user as u "+
|
||||
"WHERE u.id=? ", userID).
|
||||
Scan(&u.ID, &u.Username, &u.Email, &u.DisplayName, &u.LastModified, &u.Active, &u.Salt); err != nil {
|
||||
Scan(&u.ID, &u.Username, &u.Email, &u.DisplayName, &u.LastModified, &u.Active, &u.IsAdmin, &u.Salt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
@ -68,8 +68,8 @@ func (rdb SQLiteUserDatasource) Get(userID int) (*User, error) {
|
|||
func (rdb SQLiteUserDatasource) GetByMail(mail string) (*User, error) {
|
||||
var u User
|
||||
|
||||
if err := rdb.SQLConn.QueryRow("SELECT id, active, display_name, username, email, salt, password FROM user WHERE email=? ", mail).
|
||||
Scan(&u.ID, &u.Active, &u.DisplayName, &u.Username, &u.Email, &u.Salt, &u.Password); err != nil {
|
||||
if err := rdb.SQLConn.QueryRow("SELECT id, is_admin, active, display_name, username, email, salt, password FROM user WHERE email=? ", mail).
|
||||
Scan(&u.ID, &u.IsAdmin, &u.Active, &u.DisplayName, &u.Username, &u.Email, &u.Salt, &u.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
|
@ -79,8 +79,8 @@ func (rdb SQLiteUserDatasource) GetByMail(mail string) (*User, error) {
|
|||
func (rdb SQLiteUserDatasource) GetByUsername(username string) (*User, error) {
|
||||
var u User
|
||||
|
||||
if err := rdb.SQLConn.QueryRow("SELECT id, active, display_name, username, email, salt, password FROM user WHERE username=? ", username).
|
||||
Scan(&u.ID, &u.Active, &u.DisplayName, &u.Username, &u.Email, &u.Salt, &u.Password); err != nil {
|
||||
if err := rdb.SQLConn.QueryRow("SELECT id, is_admin, active, display_name, username, email, salt, password FROM user WHERE username=? ", username).
|
||||
Scan(&u.ID, &u.IsAdmin, &u.Active, &u.DisplayName, &u.Username, &u.Email, &u.Salt, &u.Password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
|
@ -88,8 +88,8 @@ func (rdb SQLiteUserDatasource) GetByUsername(username string) (*User, error) {
|
|||
|
||||
//Create creates an new user
|
||||
func (rdb SQLiteUserDatasource) Create(u *User) (int, error) {
|
||||
res, err := rdb.SQLConn.Exec("INSERT INTO user (salt, password, username, email, display_name, last_modified, active) VALUES(?, ?, ?, ?, ?, ?, ?);",
|
||||
u.Salt, u.Password, u.Username, u.Email, u.DisplayName, time.Now(), u.Active)
|
||||
res, err := rdb.SQLConn.Exec("INSERT INTO user (salt, password, username, email, display_name, last_modified, active, is_admin) VALUES(?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
u.Salt, u.Password, u.Username, u.Email, u.DisplayName, time.Now(), u.Active, u.IsAdmin)
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
|
@ -109,8 +109,8 @@ func (rdb SQLiteUserDatasource) Update(u *User, changePassword bool) error {
|
|||
|
||||
var args []interface{}
|
||||
|
||||
stmt.WriteString("UPDATE user SET display_name=?, username=?, email=?, last_modified=?, active=? ")
|
||||
args = append(args, u.DisplayName, u.Username, u.Email, time.Now(), u.Active)
|
||||
stmt.WriteString("UPDATE user SET display_name=?, username=?, email=?, last_modified=?, active=?, is_admin=? ")
|
||||
args = append(args, u.DisplayName, u.Username, u.Email, time.Now(), u.Active, u.IsAdmin)
|
||||
|
||||
if changePassword {
|
||||
stmt.WriteString(", salt=?, password=? ")
|
||||
|
@ -129,10 +129,20 @@ func (rdb SQLiteUserDatasource) Update(u *User, changePassword bool) error {
|
|||
}
|
||||
|
||||
//Count retuns the amount of users matches the AdminCriteria
|
||||
func (rdb SQLiteUserDatasource) Count() (int, error) {
|
||||
func (rdb SQLiteUserDatasource) Count(ac AdminCriteria) (int, error) {
|
||||
var stmt bytes.Buffer
|
||||
|
||||
stmt.WriteString("SELECT count(id) FROM user ")
|
||||
|
||||
if ac == OnlyAdmins {
|
||||
stmt.WriteString("WHERE is_admin = '1'")
|
||||
} else if ac == NoAdmins {
|
||||
stmt.WriteString("WHERE is_admin = '0'")
|
||||
}
|
||||
|
||||
var total int
|
||||
|
||||
if err := rdb.SQLConn.QueryRow("SELECT count(id) FROM user ").Scan(&total); err != nil {
|
||||
if err := rdb.SQLConn.QueryRow(stmt.String()).Scan(&total); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,15 @@
|
|||
|
||||
{{with .article}}
|
||||
<form action="/admin/article/edit/{{.ID}}" method="post">
|
||||
<label for="category">Category</label>
|
||||
|
||||
<select name="categoryID">
|
||||
<option></option>
|
||||
{{range .categories}}
|
||||
<option value="{{.ID}}">{{.Name}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
|
||||
<label for="headline">Headline</label>
|
||||
<input type="text" value="{{.Headline}}" id="headline" name="headline" placeholder="Headline..." required>
|
||||
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
<li>
|
||||
<a{{if .active}}{{if eq .active "articles"}} class="active" {{end}}{{end}} href="/admin/articles">Articles</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<a{{if .active}}{{if eq .active "categories"}} class="active" {{end}}{{end}} href="/admin/categories">Categories</a>
|
||||
</li>
|
||||
|
||||
{{if .currentUser.IsAdmin}}
|
||||
<li>
|
||||
<a{{if .active}}{{if eq .active "users"}} class="active" {{end}}{{end}} href="/admin/users">Users</a>
|
||||
</li>
|
||||
|
@ -16,6 +17,7 @@
|
|||
<li>
|
||||
<a{{if .active}}{{if eq .active "sites"}} class="active" {{end}}{{end}} href="/admin/sites">Sites</a>
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
<li>
|
||||
<a{{if .active}}{{if eq .active "files"}} class="active" {{end}}{{end}} href="/admin/files">Files</a>
|
||||
|
|
|
@ -20,6 +20,10 @@
|
|||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Password..." required>
|
||||
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="admin" name="admin" value="on"{{if .IsAdmin}} checked{{end}}>Admin?</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="active" name="active" value="on"{{if .Active}} checked{{end}}>Is activated?</label>
|
||||
</div>
|
||||
|
|
|
@ -11,15 +11,19 @@
|
|||
<form action="/admin/user/edit/{{.ID}}" method="post">
|
||||
<label for="username">Username</label>
|
||||
<input type="username" name="username" value="{{.Username}}" id="username" placeholder="Username..." required>
|
||||
|
||||
|
||||
<label for="email">Email</label>
|
||||
<input type="email" name="email" value="{{.Email}}" id="email" placeholder="Email..." required>
|
||||
|
||||
|
||||
<label for="displayname">Display name</label>
|
||||
<input type="text" value="{{.DisplayName}}" id="displayname" name="displayname" placeholder="Display name..." required>
|
||||
|
||||
|
||||
<label for="password">New password</label>
|
||||
<input type="password" id="password" name="password" placeholder="Password...">
|
||||
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="admin" name="admin" value="on"{{if .IsAdmin}} checked{{end}}>Admin?</label>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label><input type="checkbox" id="active" name="active" value="on"{{if .Active}} checked{{end}}>Is activated?</label>
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
{{template "admin/navigation" .}}
|
||||
<main>
|
||||
{{template "skel/flash" .}}
|
||||
|
||||
|
||||
<h2>User management</h2>
|
||||
|
||||
<p><a href="/admin/user/new">Add an user</a> | <a href="/admin/user-invite/new">Invite an user</a></p>
|
||||
|
||||
{{with .user_invites}}
|
||||
<h3>Open Invites</h3>
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -19,6 +19,7 @@
|
|||
<th>Username</th>
|
||||
<th>E-mail</th>
|
||||
<th>Display name</th>
|
||||
<th>Admin</th>
|
||||
<th>Invited by</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
|
@ -30,12 +31,13 @@
|
|||
<td>{{.Username}}</td>
|
||||
<td>{{.Email}}</td>
|
||||
<td>{{.DisplayName}}</td>
|
||||
<td>{{.IsAdmin | BoolToIcon}}</td>
|
||||
<td>{{.CreatedBy.DisplayName}}</td>
|
||||
<td class="action-data">
|
||||
<form method="post" action="/admin/user-invite/resend/{{.ID}}">
|
||||
<button type="submit" name="direction" value="resendinvite">
|
||||
Resend invite link
|
||||
</button>
|
||||
</button>
|
||||
{{$.csrfField}}
|
||||
</form>
|
||||
<a href="/admin/user-invite/delete/{{.ID}}" title="Remove">Remove</a>
|
||||
|
@ -46,9 +48,9 @@
|
|||
</table>
|
||||
<br>
|
||||
{{end}}
|
||||
|
||||
|
||||
<h3>Users</h3>
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -57,6 +59,7 @@
|
|||
<th>E-mail</th>
|
||||
<th>Display name</th>
|
||||
<th>Active</th>
|
||||
<th>Admin</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -68,6 +71,7 @@
|
|||
<td>{{.Email}}</td>
|
||||
<td>{{.DisplayName}}</td>
|
||||
<td>{{.Active | BoolToIcon}}</td>
|
||||
<td>{{.IsAdmin | BoolToIcon}}</td>
|
||||
<td class="action-data">
|
||||
<a href="/admin/user/edit/{{.ID}}" title="Edit">Edit</a>
|
||||
<a href="/admin/user/delete/{{.ID}}" title="Remove">Remove</a>
|
||||
|
|
Loading…
Reference in New Issue