added login page
This commit is contained in:
parent
c0d546b167
commit
b108615af0
49
comics.go
49
comics.go
|
@ -47,6 +47,9 @@ type Options struct {
|
|||
UploadSize int64
|
||||
SiteURL string
|
||||
SecretPath string
|
||||
Username string
|
||||
Password string
|
||||
CreateUser bool
|
||||
}
|
||||
|
||||
type Comic struct {
|
||||
|
@ -66,16 +69,17 @@ type Context struct {
|
|||
Image string
|
||||
}
|
||||
|
||||
Comic *Comic
|
||||
Current int
|
||||
Previous int
|
||||
Combo int
|
||||
Next int
|
||||
First int
|
||||
Last int
|
||||
Title string
|
||||
UserName string
|
||||
SiteURL string
|
||||
Comic *Comic
|
||||
Current int
|
||||
Previous int
|
||||
Combo int
|
||||
Next int
|
||||
First int
|
||||
Last int
|
||||
Title string
|
||||
UserName string
|
||||
SiteURL string
|
||||
Message string
|
||||
}
|
||||
|
||||
type Err struct {
|
||||
|
@ -95,9 +99,16 @@ func (o * Options) Parse() error {
|
|||
flag.Int64Var(&o.UploadSize, "z", 10, "Max upload size for posts")
|
||||
flag.StringVar(&o.SiteURL, "0", "https://comics.blackram.works", "URL of the site once deployed")
|
||||
flag.StringVar(&o.SecretPath, "s", "./secret.key", "Sets path to secret key file")
|
||||
flag.StringVar(&o.Username, "w", "", "Username for the new user")
|
||||
flag.StringVar(&o.Password, "q", "", "Password for the new user")
|
||||
flag.BoolVar(&o.CreateUser, "c", false, "Creates a new user. Needs -w and -q")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if o.CreateUser {
|
||||
o.RunManager = true
|
||||
}
|
||||
|
||||
if _, err := os.Stat(o.TemplatesPath); errors.Is(err, os.ErrNotExist) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -443,9 +454,19 @@ func main() {
|
|||
fs = http.FileServer(http.Dir(options.MediaPath))
|
||||
http.Handle("/media/", http.StripPrefix("/media/", fs))
|
||||
|
||||
|
||||
// errors
|
||||
http.HandleFunc("/401", return401)
|
||||
http.HandleFunc("/404", return404)
|
||||
http.HandleFunc("/500", return500)
|
||||
|
||||
|
||||
// manager
|
||||
if options.RunManager {
|
||||
log.Fatal(startManager(options.Address, options.Port, options.DBPath,
|
||||
options.MediaPath))
|
||||
err = startManager(options.Address, options.Port, options.DBPath, options.MediaPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -458,10 +479,6 @@ func main() {
|
|||
http.HandleFunc("/all", allView)
|
||||
http.HandleFunc("/blog", firstView)
|
||||
|
||||
// errors
|
||||
http.HandleFunc("/404", return404)
|
||||
http.HandleFunc("/500", return500)
|
||||
|
||||
uri := fmt.Sprintf("%s:%d", options.Address, options.Port)
|
||||
log.Println("listening to http://" + uri)
|
||||
log.Fatal(http.ListenAndServe(uri, logRequest(http.DefaultServeMux)))
|
||||
|
|
360
manager.go
360
manager.go
|
@ -7,11 +7,13 @@ import (
|
|||
"fmt"
|
||||
"path"
|
||||
"time"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"net/http"
|
||||
"math/rand"
|
||||
"database/sql"
|
||||
"crypto/sha256"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/golang-jwt/jwt"
|
||||
|
@ -20,20 +22,33 @@ import (
|
|||
|
||||
var seededRand *rand.Rand = nil
|
||||
var secretKey string = ""
|
||||
var secretKeyHash string = ""
|
||||
|
||||
const authSquema string = `
|
||||
const userSquema string = `
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
id CHAR(20) NOT NULL PRIMARY KEY,
|
||||
name CHAR(20),
|
||||
password CHAR(100)
|
||||
password CHAR(50)
|
||||
);`
|
||||
|
||||
const tokenSquema string = `
|
||||
CREATE TABLE IF NOT EXISTS token (
|
||||
id CHAR(20) NOT NULL PRIMARY KEY,
|
||||
expiry INTEGER NOT NULL,
|
||||
username CHAR(20) NOT NULL
|
||||
);`
|
||||
|
||||
type User struct {
|
||||
ID int
|
||||
ID string
|
||||
Name string
|
||||
Password string
|
||||
}
|
||||
|
||||
type ManagerContext struct {
|
||||
Context
|
||||
User User
|
||||
}
|
||||
|
||||
type ConfirmContext struct {
|
||||
Action string
|
||||
Message string
|
||||
|
@ -41,25 +56,149 @@ type ConfirmContext struct {
|
|||
OnNo string
|
||||
}
|
||||
|
||||
type TokenClaims struct {
|
||||
jwt.StandardClaims
|
||||
ID string
|
||||
Expiry time.Time
|
||||
Username string
|
||||
}
|
||||
|
||||
type ViewHandler func(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
func (u *User) readRow(db * sql.Rows) error {
|
||||
return db.Scan(&u.ID, &u.Name, &u.Password)
|
||||
}
|
||||
|
||||
func generateToken(username string) (string, error) {
|
||||
token := jwt.New(jwt.SigningMethodEdDSA)
|
||||
claims := token.Claims.(jwt.MapClaims)
|
||||
|
||||
claims["exp"] = time.Now().Add(10 * time.Minute)
|
||||
claims["authorized"] = true
|
||||
claims["user"] = username
|
||||
|
||||
tokenString, err := token.SignedString(secretKey)
|
||||
if err != nil {
|
||||
return "Signing Error", err
|
||||
func (t *TokenClaims) readRow(db * sql.Rows) error {
|
||||
var e int64
|
||||
err := db.Scan(&t.ID, &e, &t.Username)
|
||||
t.Expiry = time.Unix(e, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
func generateToken(username string, seconds time.Duration) (string, *TokenClaims, error) {
|
||||
claims := TokenClaims{
|
||||
ID: randomString(20),
|
||||
Expiry: time.Now().Add(seconds * time.Second).Truncate((time.Second)),
|
||||
Username: username,
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
tokenString, err := token.SignedString([]byte(secretKey))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
err = cleannupTokens()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
_, err = db.Exec("INSERT INTO token(id, expiry, username) VALUES(?, ?, ?);",
|
||||
claims.ID, claims.Expiry.Unix(), claims.Username)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return tokenString, &claims, nil
|
||||
}
|
||||
|
||||
func cleannupTokens() error {
|
||||
_, err := db.Exec("DELETE FROM token WHERE expiry < ?;", time.Now().Unix())
|
||||
return err
|
||||
}
|
||||
|
||||
func verifyToken(token string) (*TokenClaims, error) {
|
||||
claims := &TokenClaims{}
|
||||
|
||||
t, err := jwt.ParseWithClaims(token, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return []byte(secretKey), nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !t.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
err = cleannupTokens()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rows, err := db.Query("SELECT * FROM token WHERE id = ?;", claims.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
if !rows.Next() {
|
||||
return nil, errors.New(fmt.Sprintf("no such claim with id \"%s\" was found", claims.ID))
|
||||
}
|
||||
|
||||
claimsOnDB := &TokenClaims{}
|
||||
err = claimsOnDB.readRow(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !(claimsOnDB.ID == claims.ID && claims.Expiry == claimsOnDB.Expiry &&
|
||||
claims.Username == claimsOnDB.Username) {
|
||||
log.Println(*claims)
|
||||
log.Println(*claimsOnDB)
|
||||
return nil, errors.New("claims do not match database records")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func revokeToken(id string) error {
|
||||
_, err := db.Exec("DELETE FROM token WHERE id = ?;", id)
|
||||
return err
|
||||
}
|
||||
|
||||
func saltPassword(password string, id string) string {
|
||||
s := password + id + secretKeyHash
|
||||
if len(s) > 50 {
|
||||
s = s[:50]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func addUser(name string, password string) (*User, error) {
|
||||
id := randomString(20)
|
||||
|
||||
var user *User = nil
|
||||
users, err := getAllUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for user == nil {
|
||||
user = &User{}
|
||||
for _, u := range(users) {
|
||||
if name == u.Name {
|
||||
return nil, errors.New("user with the same name already exists")
|
||||
}
|
||||
|
||||
if id == u.ID {
|
||||
id = randomString(20)
|
||||
user = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p, err := bcrypt.GenerateFromPassword([]byte(saltPassword(password, id)), 8)
|
||||
_, err = db.Exec("INSERT INTO user(id, name, password) VALUES(?, ?, ?);",
|
||||
id, name, string(p))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func getAllUsers() ([]User, error) {
|
||||
|
@ -74,7 +213,7 @@ func getAllUsers() ([]User, error) {
|
|||
u := User{}
|
||||
err := u.readRow(rows)
|
||||
if err != nil {
|
||||
return users, err
|
||||
return nil, err
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
|
@ -91,6 +230,81 @@ func randomString(length int) string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
func getSessionToken(r *http.Request) (string, error) {
|
||||
c, err := r.Cookie("token")
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return c.Value, err
|
||||
}
|
||||
|
||||
func getSessionUser(r * http.Request) (*User, *TokenClaims, error) {
|
||||
c, err := getSessionToken(r)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return authenticateUserByToken(c)
|
||||
}
|
||||
|
||||
func authenticateUserByToken(token string) (*User, *TokenClaims, error) {
|
||||
claims, err := verifyToken(token)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
users, _ := getAllUsers()
|
||||
var user *User = nil
|
||||
for i, u := range(users) {
|
||||
if u.Name == claims.Username {
|
||||
user = &users[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
err = errors.New("no such user \"" + claims.Username + "\"")
|
||||
}
|
||||
|
||||
return user, claims, nil
|
||||
}
|
||||
|
||||
func athenticateUserByPassword(username string, password string) (*User, error) {
|
||||
if len(username) >= 20 {
|
||||
username = username[:20]
|
||||
}
|
||||
if len(password) >= 50 {
|
||||
password = password[:50]
|
||||
}
|
||||
|
||||
users, err := getAllUsers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var user *User = nil
|
||||
for i, u := range(users) {
|
||||
if u.Name == username {
|
||||
user = &users[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, errors.New("No such user \"" + username + "\" found")
|
||||
}
|
||||
|
||||
log.Println(user.ID)
|
||||
password = saltPassword(password, user.ID)
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, err
|
||||
}
|
||||
|
||||
func managerIndexView(w http.ResponseWriter, r * http.Request) {
|
||||
var err error
|
||||
|
||||
|
@ -103,14 +317,22 @@ func managerIndexView(w http.ResponseWriter, r * http.Request) {
|
|||
}
|
||||
} ()
|
||||
|
||||
user, _, err := getSessionUser(r)
|
||||
if err != nil {
|
||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
if len(strings.TrimPrefix(r.URL.Path, "/")) > 0 {
|
||||
return404(w,r)
|
||||
return
|
||||
}
|
||||
|
||||
context := Context{
|
||||
Comics: nil,
|
||||
Title: "Black Ram Comics Manager",
|
||||
context := ManagerContext{
|
||||
Context: Context{
|
||||
Title: "Black Ram Comics Manager",
|
||||
},
|
||||
User: *user,
|
||||
}
|
||||
|
||||
comics, err := allComics()
|
||||
|
@ -226,7 +448,16 @@ func managerPublishView(w http.ResponseWriter, r * http.Request) {
|
|||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func loginView(w http.ResponseWriter, r * http.Request) {
|
||||
func managerLoginView(w http.ResponseWriter, r * http.Request) {
|
||||
context := Context{
|
||||
Title: "Blackram Manager",
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
executeTemplate(w, "login", context)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "POST" {
|
||||
return404(w, r)
|
||||
return
|
||||
|
@ -235,36 +466,58 @@ func loginView(w http.ResponseWriter, r * http.Request) {
|
|||
username := r.FormValue("username")
|
||||
password := r.FormValue("password")
|
||||
if username == "" || password == "" {
|
||||
context.Message = "missing name/password"
|
||||
executeTemplate(w, "login", context)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := athenticateUserByPassword(username, password)
|
||||
if err != nil {
|
||||
context.Message = "wrong username/password"
|
||||
executeTemplate(w, "login", context)
|
||||
return
|
||||
}
|
||||
|
||||
token, claims, err := generateToken(user.Name, 60)
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("failed to generate token for \"%s\": %s", user.Name, err.Error()))
|
||||
return500(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "token",
|
||||
Value: token,
|
||||
Expires: claims.Expiry,
|
||||
})
|
||||
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func managerLogoutView(w http.ResponseWriter, r * http.Request) {
|
||||
if r.Method != "GET" {
|
||||
return404(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if len(username) >= 20 {
|
||||
username = username[:20]
|
||||
}
|
||||
if len(password) >= 100 {
|
||||
password = password[:100]
|
||||
}
|
||||
|
||||
users, _ := getAllUsers()
|
||||
var user *User = nil
|
||||
for i, u := range(users) {
|
||||
if u.Name == username {
|
||||
user = &users[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
_, claims, err := getSessionUser(r)
|
||||
if err != nil {
|
||||
log.Println("failed to get session user: " + err.Error())
|
||||
return401(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return401(w, r)
|
||||
err = revokeToken(claims.ID)
|
||||
if err != nil {
|
||||
log.Println("failed to revoke session token: " + err.Error())
|
||||
return500(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "token",
|
||||
Value: "",
|
||||
})
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
@ -272,19 +525,24 @@ func managerEditView(w http.ResponseWriter, r * http.Request) {
|
|||
}
|
||||
|
||||
func startManager(address string, port int, dbPath string, mediaPath string) error {
|
||||
_, err := db.Exec(authSquema)
|
||||
seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
_, err := db.Exec(userSquema)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
_, err = db.Exec(tokenSquema)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
seededRand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
_, err = os.Stat(options.SecretPath)
|
||||
if os.IsNotExist(err) {
|
||||
secretKey = randomString(100)
|
||||
err = os.WriteFile(options.SecretPath, []byte(secretKey), 0600)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("failed to create random key: %s", err))
|
||||
log.Fatal(fmt.Sprintf("failed to create random key on path \"%s\": %s",
|
||||
options.SecretPath, err))
|
||||
}
|
||||
} else {
|
||||
s, err := os.ReadFile(options.SecretPath)
|
||||
|
@ -293,11 +551,25 @@ func startManager(address string, port int, dbPath string, mediaPath string) err
|
|||
log.Fatal(fmt.Sprintf("failed to read random key: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
sh := sha256.Sum256([]byte(secretKey))
|
||||
secretKeyHash = fmt.Sprintf("%x", sh)
|
||||
|
||||
if options.CreateUser {
|
||||
_, err := addUser(options.Username, options.Password)
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("failed to create new user: %s", err.Error()))
|
||||
}
|
||||
log.Println(fmt.Sprintf("successfuly created user: \"%s\"", options.Username))
|
||||
return nil
|
||||
}
|
||||
|
||||
http.HandleFunc("/", managerIndexView)
|
||||
http.HandleFunc("/edit/", managerEditView)
|
||||
http.HandleFunc("/remove/", managerRemoveView)
|
||||
http.HandleFunc("/publish", managerPublishView)
|
||||
http.HandleFunc("/login", managerLoginView)
|
||||
http.HandleFunc("/logout", managerLogoutView)
|
||||
|
||||
uri := fmt.Sprintf("%s:%d", address, port)
|
||||
log.Println("listening to http://" + uri)
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" type="/image/png" href="/static/img/favicon.png"/>
|
||||
<link rel="stylesheet" href="/static/css/form.css">
|
||||
<link rel="stylesheet" href="/static/css/base.css">
|
||||
<link rel="stylesheet" href="/static/css/login.css">
|
||||
<link rel="stylesheet" href="/static/css/top.css">
|
||||
<title>{{ .Title }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="top">
|
||||
<div class="title">
|
||||
<h1>{{ .Title }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="login">
|
||||
<div class="manager-form">
|
||||
<form action="/login" method="post">
|
||||
<label for="username"><Username></label>
|
||||
<input type="text" class="input" name="username">
|
||||
<label for="password"><Password></label>
|
||||
<input type="text" class="input" name="password">
|
||||
<input type="submit" style="display:none;" id="login">
|
||||
<label type="submit" class="login fake-button" for="login"><Login></label>
|
||||
</form>
|
||||
</div>
|
||||
{{ if .Message }}
|
||||
<p class="message">{{ .Message }}</p>
|
||||
{{ end }}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -2,7 +2,7 @@
|
|||
<div class="manager-top">
|
||||
<div class="logging-buttom">
|
||||
<a href="/logout">
|
||||
<h1>{{ .UserName }} <Logout></h1>
|
||||
<h1>{{ .User.Name }} <Logout></h1>
|
||||
</a>
|
||||
</div>
|
||||
<div class="site-button">
|
||||
|
@ -18,7 +18,7 @@
|
|||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="icon" type="/image/png" href="/static/img/favicon.png"/>
|
||||
<link rel="stylesheet" href="/static/css/publish.css">
|
||||
<link rel="stylesheet" href="/static/css/form.css">
|
||||
<link rel="stylesheet" href="/static/css/manager.css">
|
||||
<link rel="stylesheet" href="/static/css/base.css">
|
||||
<link rel="stylesheet" href="/static/css/top.css">
|
||||
|
@ -27,10 +27,10 @@
|
|||
<body>
|
||||
{{ template "top" . }}
|
||||
|
||||
<div class="manager-publish">
|
||||
<div class="manager-form">
|
||||
<form enctype="multipart/form-data" action="/publish" method="post">
|
||||
<label for="title"><Title></label>
|
||||
<input type="text" class="title" name="title" onkeypress="return event.keyCode != 13;">
|
||||
<input type="text" class="input" name="title" onkeypress="return event.keyCode != 13;">
|
||||
<input type="file" name="image" style="display:none;" id="browse" accept=".jpg, .jpeg, .png">
|
||||
<label type="submit" class="browse fake-button" for="browse" class="fake-button"><Browse></label>
|
||||
<input type="submit" style="display:none;" id="publish">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
.manager-publish .title {
|
||||
.manager-form .input {
|
||||
outline: none;
|
||||
border: none;
|
||||
background: none;
|
||||
|
@ -9,18 +9,22 @@
|
|||
border-bottom: 0.125em solid #ebdbb2;
|
||||
}
|
||||
|
||||
.manager-publish label {
|
||||
.manager-form .login-space {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.manager-form label {
|
||||
margin-top: 0.25em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.manager-publish {
|
||||
.manager-form {
|
||||
display: flex;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.manager-publish label {
|
||||
.manager-form label {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
|
@ -31,25 +35,25 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.manager-publish label:hover {
|
||||
.manager-form label:hover {
|
||||
color: #ebdbb2;
|
||||
background-color: #282828;
|
||||
}
|
||||
|
||||
.manager-publish input {
|
||||
.manager-form input {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.manager-publish .description {
|
||||
.manager-form .description {
|
||||
height: 5em;
|
||||
}
|
||||
|
||||
.manager-publish * {
|
||||
.manager-form * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.manager-publish .description {
|
||||
.manager-form .description {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
body {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.login {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login .message {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
7oAhklgrWmAyaqdfD0ny3Z0uM3jAWSOilTM06iDomjboM8KXznDck7wj5UGqwWHeeWVwmnOzdKDLkaLLFeobyG1FO5upVYceVtSf
|
Loading…
Reference in New Issue