diff --git a/internal/api/rest.go b/internal/api/rest.go
index b255694..3525fba 100644
--- a/internal/api/rest.go
+++ b/internal/api/rest.go
@@ -718,14 +718,26 @@ func (api *RestApi) updateUser(rw http.ResponseWriter, r *http.Request) {
return
}
- // TODO: Handle anything but roles...
+ // Get Values
newrole := r.FormValue("add-role")
- if err := api.Authentication.AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil {
- http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
- return
- }
+ delrole := r.FormValue("remove-role")
- rw.Write([]byte("success"))
+ // TODO: Handle anything but roles...
+ if (newrole != "") {
+ if err := api.Authentication.AddRole(r.Context(), mux.Vars(r)["id"], newrole); err != nil {
+ http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
+ return
+ }
+ rw.Write([]byte("Add Role Success"))
+ } else if (delrole != "") {
+ if err := api.Authentication.RemoveRole(r.Context(), mux.Vars(r)["id"], delrole); err != nil {
+ http.Error(rw, err.Error(), http.StatusUnprocessableEntity)
+ return
+ }
+ rw.Write([]byte("Remove Role Success"))
+ } else {
+ http.Error(rw, "Not Add or Del?", http.StatusInternalServerError)
+ }
}
func (api *RestApi) updateConfiguration(rw http.ResponseWriter, r *http.Request) {
diff --git a/internal/auth/users.go b/internal/auth/users.go
index f0468d8..fbdc08d 100644
--- a/internal/auth/users.go
+++ b/internal/auth/users.go
@@ -120,7 +120,7 @@ func (auth *Authentication) AddRole(
return err
}
- if role != RoleAdmin && role != RoleApi && role != RoleUser {
+ if role != RoleAdmin && role != RoleApi && role != RoleUser && role != RoleSupport {
return fmt.Errorf("invalid user role: %#v", role)
}
@@ -137,13 +137,40 @@ func (auth *Authentication) AddRole(
return nil
}
-func FetchUser(
- ctx context.Context,
- db *sqlx.DB,
- username string) (*model.User, error) {
+func (auth *Authentication) RemoveRole(ctx context.Context, username string, role string) error {
+ user, err := auth.GetUser(username)
+ if err != nil {
+ return err
+ }
+ if role != RoleAdmin && role != RoleApi && role != RoleUser {
+ return fmt.Errorf("invalid user role: %#v", role)
+ }
+
+ var exists bool
+ var newroles []string
+ for _, r := range user.Roles {
+ if r != role {
+ newroles = append(newroles, r) // Append all roles not matching requested delete role
+ } else {
+ exists = true
+ }
+ }
+
+ if (exists == true) {
+ var mroles, _ = json.Marshal(newroles)
+ if _, err := sq.Update("user").Set("roles", mroles).Where("user.username = ?", username).RunWith(auth.db).Exec(); err != nil {
+ return err
+ }
+ return nil
+ } else {
+ return fmt.Errorf("user %#v already does not have role %#v", username, role)
+ }
+}
+
+func FetchUser(ctx context.Context, db *sqlx.DB, username string) (*model.User, error) {
me := GetUser(ctx)
- if me != nil && !me.HasRole(RoleAdmin) && me.Username != username {
+ if me != nil && !me.HasRole(RoleAdmin) && !me.HasRole(RoleSupport) && me.Username != username {
return nil, errors.New("forbidden")
}
diff --git a/internal/graph/schema.resolvers.go b/internal/graph/schema.resolvers.go
index da682e9..1aa8a04 100644
--- a/internal/graph/schema.resolvers.go
+++ b/internal/graph/schema.resolvers.go
@@ -152,9 +152,7 @@ func (r *queryResolver) Job(ctx context.Context, id string) (*schema.Job, error)
return nil, err
}
- if user := auth.GetUser(ctx); user != nil &&
- !user.HasRole(auth.RoleAdmin) &&
- job.User != user.Username {
+ if user := auth.GetUser(ctx); user != nil && !user.HasRole(auth.RoleAdmin) && !user.HasRole(auth.RoleSupport) && job.User != user.Username {
return nil, errors.New("you are not allowed to see this job")
}
diff --git a/internal/repository/job.go b/internal/repository/job.go
index d28fdfd..0496698 100644
--- a/internal/repository/job.go
+++ b/internal/repository/job.go
@@ -308,7 +308,7 @@ func (r *JobRepository) FindJobOrUser(ctx context.Context, searchterm string) (j
user := auth.GetUser(ctx)
if id, err := strconv.Atoi(searchterm); err == nil {
qb := sq.Select("job.id").From("job").Where("job.job_id = ?", id)
- if user != nil && !user.HasRole(auth.RoleAdmin) {
+ if user != nil && !user.HasRole(auth.RoleAdmin) && !user.HasRole(auth.RoleSupport) {
qb = qb.Where("job.user = ?", user.Username)
}
@@ -320,7 +320,7 @@ func (r *JobRepository) FindJobOrUser(ctx context.Context, searchterm string) (j
}
}
- if user == nil || user.HasRole(auth.RoleAdmin) {
+ if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleSupport) {
err := sq.Select("job.user").Distinct().From("job").
Where("job.user = ?", searchterm).
RunWith(r.stmtCache).QueryRow().Scan(&username)
diff --git a/internal/repository/query.go b/internal/repository/query.go
index 917bbaf..fad6091 100644
--- a/internal/repository/query.go
+++ b/internal/repository/query.go
@@ -94,7 +94,7 @@ func (r *JobRepository) CountJobs(
func SecurityCheck(ctx context.Context, query sq.SelectBuilder) sq.SelectBuilder {
user := auth.GetUser(ctx)
- if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleApi) {
+ if user == nil || user.HasRole(auth.RoleAdmin) || user.HasRole(auth.RoleApi) || user.HasRole(auth.RoleSupport) {
return query
}
diff --git a/internal/routerConfig/routes.go b/internal/routerConfig/routes.go
index 9a4557a..a5ea524 100644
--- a/internal/routerConfig/routes.go
+++ b/internal/routerConfig/routes.go
@@ -270,15 +270,17 @@ func SetupRoutes(router *mux.Router) {
title = strings.Replace(route.Title, "", id.(string), 1)
}
- username, isAdmin := "", true
+ username, isAdmin, isSupporter := "", true, true
+
if user := auth.GetUser(r.Context()); user != nil {
username = user.Username
isAdmin = user.HasRole(auth.RoleAdmin)
+ isSupporter = user.HasRole(auth.RoleSupport)
}
page := web.Page{
Title: title,
- User: web.User{Username: username, IsAdmin: isAdmin},
+ User: web.User{Username: username, IsAdmin: isAdmin, IsSupporter: isSupporter},
Config: conf,
Infos: infos,
}
diff --git a/pkg/lrucache/README.md b/pkg/lrucache/README.md
index ad58bc9..855a185 100644
--- a/pkg/lrucache/README.md
+++ b/pkg/lrucache/README.md
@@ -1,7 +1,5 @@
# In-Memory LRU Cache for Golang Applications
-[![](https://pkg.go.dev/badge/github.com/iamlouk/lrucache?utm_source=godoc)](https://pkg.go.dev/github.com/iamlouk/lrucache)
-
This library can be embedded into your existing go applications
and play the role *Memcached* or *Redis* might play for others.
It is inspired by [PHP Symfony's Cache Components](https://symfony.com/doc/current/components/cache/adapters/array_cache_adapter.html),
diff --git a/pkg/lrucache/cache.go b/pkg/lrucache/cache.go
index aedfd5c..679bd2e 100644
--- a/pkg/lrucache/cache.go
+++ b/pkg/lrucache/cache.go
@@ -1,3 +1,7 @@
+// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
+// All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
package lrucache
import (
diff --git a/pkg/lrucache/cache_test.go b/pkg/lrucache/cache_test.go
index bfab653..7ba5504 100644
--- a/pkg/lrucache/cache_test.go
+++ b/pkg/lrucache/cache_test.go
@@ -1,3 +1,7 @@
+// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
+// All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
package lrucache
import (
diff --git a/pkg/lrucache/handler.go b/pkg/lrucache/handler.go
index e83ba10..db6687f 100644
--- a/pkg/lrucache/handler.go
+++ b/pkg/lrucache/handler.go
@@ -1,3 +1,7 @@
+// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
+// All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
package lrucache
import (
diff --git a/pkg/lrucache/handler_test.go b/pkg/lrucache/handler_test.go
index cb05f31..4013c63 100644
--- a/pkg/lrucache/handler_test.go
+++ b/pkg/lrucache/handler_test.go
@@ -1,3 +1,7 @@
+// Copyright (C) 2022 NHR@FAU, University Erlangen-Nuremberg.
+// All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
package lrucache
import (
diff --git a/web/frontend/package.json b/web/frontend/package.json
index 2f2ab55..174fa5f 100644
--- a/web/frontend/package.json
+++ b/web/frontend/package.json
@@ -1,6 +1,7 @@
{
- "name": "svelte-app",
+ "name": "cc-frontend",
"version": "1.0.0",
+ "license": "MIT",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w"
@@ -12,7 +13,7 @@
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
- "svelte": "^3.42.6"
+ "svelte": "^3.49.0"
},
"dependencies": {
"@rollup/plugin-replace": "^2.4.1",
diff --git a/web/frontend/rollup.config.js b/web/frontend/rollup.config.js
index 8144e9c..2737c8a 100644
--- a/web/frontend/rollup.config.js
+++ b/web/frontend/rollup.config.js
@@ -66,6 +66,6 @@ export default [
entrypoint('systems', 'src/systems.entrypoint.js'),
entrypoint('node', 'src/node.entrypoint.js'),
entrypoint('analysis', 'src/analysis.entrypoint.js'),
- entrypoint('status', 'src/status.entrypoint.js')
+ entrypoint('status', 'src/status.entrypoint.js'),
+ entrypoint('config', 'src/config.entrypoint.js')
];
-
diff --git a/web/frontend/src/Config.root.svelte b/web/frontend/src/Config.root.svelte
new file mode 100644
index 0000000..6b1eb40
--- /dev/null
+++ b/web/frontend/src/Config.root.svelte
@@ -0,0 +1,31 @@
+
+
+{#if user.IsAdmin}
+
+
+ Admin Options
+
+
+
+{/if}
+
+
+
+ Plotting Options
+
+
+
diff --git a/web/frontend/src/config.entrypoint.js b/web/frontend/src/config.entrypoint.js
new file mode 100644
index 0000000..5c9e525
--- /dev/null
+++ b/web/frontend/src/config.entrypoint.js
@@ -0,0 +1,12 @@
+import {} from './header.entrypoint.js'
+import Config from './Config.root.svelte'
+
+new Config({
+ target: document.getElementById('svelte-app'),
+ props: {
+ user: user
+ },
+ context: new Map([
+ ['cc-config', clusterCockpitConfig]
+ ])
+})
diff --git a/web/frontend/src/config/AdminSettings.svelte b/web/frontend/src/config/AdminSettings.svelte
new file mode 100644
index 0000000..d1ce542
--- /dev/null
+++ b/web/frontend/src/config/AdminSettings.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/config/PlotSettings.svelte b/web/frontend/src/config/PlotSettings.svelte
new file mode 100644
index 0000000..36326bd
--- /dev/null
+++ b/web/frontend/src/config/PlotSettings.svelte
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/frontend/src/config/admin/AddUser.svelte b/web/frontend/src/config/admin/AddUser.svelte
new file mode 100644
index 0000000..bcbd240
--- /dev/null
+++ b/web/frontend/src/config/admin/AddUser.svelte
@@ -0,0 +1,91 @@
+
+
+
+
+
+
diff --git a/web/frontend/src/config/admin/EditRole.svelte b/web/frontend/src/config/admin/EditRole.svelte
new file mode 100644
index 0000000..4615c0a
--- /dev/null
+++ b/web/frontend/src/config/admin/EditRole.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+ Edit User Roles
+
+
+
+ Role...
+ User
+ Support
+ Admin
+ API
+
+
+
+ Add
+ Remove
+
+
+ {#if displayMessage}Update: {message.msg}
{/if}
+
+
+
diff --git a/web/frontend/src/config/admin/Options.svelte b/web/frontend/src/config/admin/Options.svelte
new file mode 100644
index 0000000..44f9650
--- /dev/null
+++ b/web/frontend/src/config/admin/Options.svelte
@@ -0,0 +1,29 @@
+
+
+
+
+ Scramble Names / Presentation Mode
+
+ Active?
+
+
diff --git a/web/frontend/src/config/admin/ShowUsers.svelte b/web/frontend/src/config/admin/ShowUsers.svelte
new file mode 100644
index 0000000..5726fc4
--- /dev/null
+++ b/web/frontend/src/config/admin/ShowUsers.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+ Special Users
+
+ Not created by an LDAP sync and/or having a role other than user
+ Reload
+
+
+
+
+
+ Username
+ Name
+ Email
+ Roles
+ JWT
+ Delete
+
+
+
+ {#each userList as user}
+
+
+ Delete
+
+ {:else}
+
+
+ Loading...
+
+
+ {/each}
+
+
+
+
+
diff --git a/web/frontend/src/config/admin/ShowUsersRow.svelte b/web/frontend/src/config/admin/ShowUsersRow.svelte
new file mode 100644
index 0000000..64b5dd4
--- /dev/null
+++ b/web/frontend/src/config/admin/ShowUsersRow.svelte
@@ -0,0 +1,27 @@
+
+
+{user.username}
+{user.name}
+{user.email}
+{user.roles.join(', ')}
+
+ {#if ! jwt}
+ Gen. JWT
+ {:else}
+
+ {/if}
+
diff --git a/web/frontend/yarn.lock b/web/frontend/yarn.lock
index f80e078..7113711 100644
--- a/web/frontend/yarn.lock
+++ b/web/frontend/yarn.lock
@@ -28,6 +28,46 @@
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052"
integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==
+"@jridgewell/gen-mapping@^0.3.0":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
+ integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
+ dependencies:
+ "@jridgewell/set-array" "^1.0.1"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/resolve-uri@^3.0.3":
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
+ integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
+
+"@jridgewell/set-array@^1.0.1":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
+ integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
+
+"@jridgewell/source-map@^0.3.2":
+ version "0.3.2"
+ resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
+ integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.0"
+ "@jridgewell/trace-mapping" "^0.3.9"
+
+"@jridgewell/sourcemap-codec@^1.4.10":
+ version "1.4.14"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
+ integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
+
+"@jridgewell/trace-mapping@^0.3.9":
+ version "0.3.14"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
+ integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.0.3"
+ "@jridgewell/sourcemap-codec" "^1.4.10"
+
"@popperjs/core@^2.9.2":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.0.tgz#6734f8ebc106a0860dff7f92bf90df193f0935d7"
@@ -121,6 +161,11 @@
"@urql/core" "^2.3.4"
wonka "^4.0.14"
+acorn@^8.5.0:
+ version "8.8.0"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
+ integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
+
ansi-styles@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d"
@@ -432,11 +477,6 @@ source-map@^0.6.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
-source-map@~0.7.2:
- version "0.7.3"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
- integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
-
sourcemap-codec@^1.4.4:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
@@ -456,10 +496,10 @@ supports-color@^7.0.0:
dependencies:
has-flag "^4.0.0"
-svelte@^3.42.6:
- version "3.44.2"
- resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.44.2.tgz#3e69be2598308dfc8354ba584cec54e648a50f7f"
- integrity sha512-jrZhZtmH3ZMweXg1Q15onb8QlWD+a5T5Oca4C1jYvSURp2oD35h4A5TV6t6MEa93K4LlX6BkafZPdQoFjw/ylA==
+svelte@^3.49.0:
+ version "3.49.0"
+ resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.49.0.tgz#5baee3c672306de1070c3b7888fc2204e36a4029"
+ integrity sha512-+lmjic1pApJWDfPCpUUTc1m8azDqYCG1JN9YEngrx/hUyIcFJo6VZhj0A1Ai0wqoHcEIuQy+e9tk+4uDgdtsFA==
sveltestrap@^5.6.1:
version "5.6.3"
@@ -469,12 +509,13 @@ sveltestrap@^5.6.1:
"@popperjs/core" "^2.9.2"
terser@^5.0.0:
- version "5.10.0"
- resolved "https://registry.yarnpkg.com/terser/-/terser-5.10.0.tgz#b86390809c0389105eb0a0b62397563096ddafcc"
- integrity sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==
+ version "5.14.2"
+ resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10"
+ integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==
dependencies:
+ "@jridgewell/source-map" "^0.3.2"
+ acorn "^8.5.0"
commander "^2.20.0"
- source-map "~0.7.2"
source-map-support "~0.5.20"
uplot@^1.6.7:
diff --git a/web/templates/config.tmpl b/web/templates/config.tmpl
index deccf92..2f3eaf1 100644
--- a/web/templates/config.tmpl
+++ b/web/templates/config.tmpl
@@ -1,295 +1,15 @@
{{define "content"}}
-{{if .User.IsAdmin}}
-
-
-
-
-
Special Users
-
Not created by an LDAP sync and/or having a role other than user
-
-
-
-
-
- Username
- Name
- Email
- Roles
- JWT
- Delete
-
-
-
-
-
- Loading...
-
-
-
-
-
-
-
-
-
-
-
-
-
Add Role to User
-
-
-
- Role...
- User
- Admin
- API
-
- Button
-
-
-
-
-
-
-
Scramble Names / Presentation Mode
-
-
-
-
-
+
{{end}}
-
-
-
+{{define "stylesheets"}}
+
+{{end}}
+{{define "javascript"}}
+
+
{{end}}
\ No newline at end of file
diff --git a/web/web.go b/web/web.go
index 0afa549..25bbc68 100644
--- a/web/web.go
+++ b/web/web.go
@@ -56,6 +56,7 @@ func init() {
type User struct {
Username string // Username of the currently logged in user
IsAdmin bool
+ IsSupporter bool
}
type Page struct {