feat: add admin publishing workflow and yar theme
Add Go/Postgres admin APIs, Angular admin UI, manual build flow, asset uploads, markdown import/export, configurable slug generation, and the Yar reading theme. Exclude local docs and generated development artifacts from version control.
This commit is contained in:
parent
b78f4b39c9
commit
f0b50d13ea
121 changed files with 27139 additions and 550 deletions
75
backend/internal/admin/auth.go
Normal file
75
backend/internal/admin/auth.go
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const SessionCookieName = "osaet_admin_session"
|
||||
|
||||
var ErrInvalidCredentials = errors.New("invalid username or password")
|
||||
|
||||
type contextKey string
|
||||
|
||||
const userContextKey contextKey = "adminUser"
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
if password == "" {
|
||||
return "", errors.New("password is required")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("hash password: %w", err)
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
func CheckPassword(hash string, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
||||
}
|
||||
|
||||
func NewSessionToken() (string, error) {
|
||||
token := make([]byte, 32)
|
||||
if _, err := rand.Read(token); err != nil {
|
||||
return "", fmt.Errorf("generate session token: %w", err)
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(token), nil
|
||||
}
|
||||
|
||||
func SessionTokenHash(token string) string {
|
||||
sum := sha256.Sum256([]byte(token))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func SetSessionCookie(c *gin.Context, token string, expiresAt time.Time) {
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
Expires: expiresAt,
|
||||
MaxAge: int(time.Until(expiresAt).Seconds()),
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
func ClearSessionCookie(c *gin.Context) {
|
||||
http.SetCookie(c.Writer, &http.Cookie{
|
||||
Name: SessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue