2
cache/cache.go
vendored
2
cache/cache.go
vendored
@ -15,6 +15,7 @@ type UserWithExpired struct {
|
||||
Username string
|
||||
Email string
|
||||
Password string
|
||||
Totp string
|
||||
AccessAt time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
@ -103,6 +104,7 @@ func GetUser(email string) (*UserWithExpired, error) {
|
||||
Username: userData.Username,
|
||||
Email: userData.Email,
|
||||
Password: userData.Password,
|
||||
Totp: userData.Totp,
|
||||
AccessAt: time.Now(),
|
||||
}
|
||||
|
||||
|
@ -45,6 +45,8 @@ type Database interface {
|
||||
UpdateUploadedByte(index int64, fileID string)
|
||||
UpdateUploadedChunk(index int64, fileID string)
|
||||
FinalizeFileUpload(fileID string)
|
||||
|
||||
InitializeTotp(email string, secret string) error
|
||||
}
|
||||
|
||||
func NewMYSQLdb(username, password, host, port, dbName string) Database {
|
||||
@ -262,6 +264,20 @@ func (db *mySQLdb) FinalizeFileUpload(fileID string) {
|
||||
db.Save(&file)
|
||||
}
|
||||
|
||||
func (db *mySQLdb) InitializeTotp(email string, secret string) error {
|
||||
var user models.User
|
||||
err := db.DB.Table("users").Where("email = ?", email).First(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Totp = secret
|
||||
err = db.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// POSTGRES FUNCTION
|
||||
func (db *postgresDB) IsUserRegistered(email string, username string) bool {
|
||||
var data models.User
|
||||
@ -369,3 +385,17 @@ func (db *postgresDB) FinalizeFileUpload(fileID string) {
|
||||
file.Done = true
|
||||
db.Save(&file)
|
||||
}
|
||||
|
||||
func (db *postgresDB) InitializeTotp(email string, secret string) error {
|
||||
var user models.User
|
||||
err := db.DB.Table("users").Where("email = $1", email).First(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Totp = secret
|
||||
err = db.Save(&user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
6
go.mod
6
go.mod
@ -6,6 +6,9 @@ require (
|
||||
github.com/a-h/templ v0.2.648
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mdp/qrterminal/v3 v3.2.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
golang.org/x/crypto v0.21.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gorm.io/driver/mysql v1.5.6
|
||||
@ -20,6 +23,9 @@ require (
|
||||
github.com/jackc/pgx/v5 v5.4.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/term v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
rsc.io/qr v0.2.0 // indirect
|
||||
)
|
||||
|
12
go.sum
12
go.sum
@ -21,15 +21,25 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk=
|
||||
github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
|
||||
@ -47,3 +57,5 @@ gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4c
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.8 h1:WAGEZ/aEcznN4D03laj8DKnehe1e9gYQAjW8xyPRdeo=
|
||||
gorm.io/gorm v1.25.8/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
|
||||
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
|
||||
|
139
handler/auth/totp/totp.go
Normal file
139
handler/auth/totp/totp.go
Normal file
@ -0,0 +1,139 @@
|
||||
package totpHandler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/fossyy/filekeeper/session"
|
||||
"github.com/fossyy/filekeeper/types"
|
||||
"github.com/fossyy/filekeeper/utils"
|
||||
totpView "github.com/fossyy/filekeeper/view/totp"
|
||||
"github.com/xlzd/gotp"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func GET(w http.ResponseWriter, r *http.Request) {
|
||||
_, user, _ := session.GetSession(r)
|
||||
if user.Authenticated || user.Totp == "" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
component := totpView.Main("Filekeeper - 2FA Page")
|
||||
err := component.Render(r.Context(), w)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func POST(w http.ResponseWriter, r *http.Request) {
|
||||
r.ParseForm()
|
||||
code := r.Form.Get("code")
|
||||
_, user, key := session.GetSession(r)
|
||||
totp := gotp.NewDefaultTOTP(user.Totp)
|
||||
|
||||
if totp.Verify(code, time.Now().Unix()) {
|
||||
storeSession, err := session.Get(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
storeSession.Values["user"] = types.User{
|
||||
UserID: user.UserID,
|
||||
Email: user.Email,
|
||||
Username: user.Username,
|
||||
Authenticated: true,
|
||||
}
|
||||
userAgent := r.Header.Get("User-Agent")
|
||||
browserInfo, osInfo := ParseUserAgent(userAgent)
|
||||
|
||||
sessionInfo := session.SessionInfo{
|
||||
SessionID: storeSession.ID,
|
||||
Browser: browserInfo["browser"],
|
||||
Version: browserInfo["version"],
|
||||
OS: osInfo["os"],
|
||||
OSVersion: osInfo["version"],
|
||||
IP: utils.ClientIP(r),
|
||||
Location: "Indonesia",
|
||||
}
|
||||
|
||||
storeSession.Save(w)
|
||||
session.AddSessionInfo(user.Email, &sessionInfo)
|
||||
|
||||
cookie, err := r.Cookie("redirect")
|
||||
if errors.Is(err, http.ErrNoCookie) {
|
||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: "redirect",
|
||||
MaxAge: -1,
|
||||
})
|
||||
http.Redirect(w, r, cookie.Value, http.StatusSeeOther)
|
||||
return
|
||||
} else {
|
||||
fmt.Fprint(w, "wrong")
|
||||
}
|
||||
}
|
||||
|
||||
func ParseUserAgent(userAgent string) (map[string]string, map[string]string) {
|
||||
browserInfo := make(map[string]string)
|
||||
osInfo := make(map[string]string)
|
||||
if strings.Contains(userAgent, "Firefox") {
|
||||
browserInfo["browser"] = "Firefox"
|
||||
parts := strings.Split(userAgent, "Firefox/")
|
||||
if len(parts) > 1 {
|
||||
version := strings.Split(parts[1], " ")[0]
|
||||
browserInfo["version"] = version
|
||||
}
|
||||
} else if strings.Contains(userAgent, "Chrome") {
|
||||
browserInfo["browser"] = "Chrome"
|
||||
parts := strings.Split(userAgent, "Chrome/")
|
||||
if len(parts) > 1 {
|
||||
version := strings.Split(parts[1], " ")[0]
|
||||
browserInfo["version"] = version
|
||||
}
|
||||
} else {
|
||||
browserInfo["browser"] = "Unknown"
|
||||
browserInfo["version"] = "Unknown"
|
||||
}
|
||||
|
||||
if strings.Contains(userAgent, "Windows") {
|
||||
osInfo["os"] = "Windows"
|
||||
parts := strings.Split(userAgent, "Windows ")
|
||||
if len(parts) > 1 {
|
||||
version := strings.Split(parts[1], ";")[0]
|
||||
osInfo["version"] = version
|
||||
}
|
||||
} else if strings.Contains(userAgent, "Macintosh") {
|
||||
osInfo["os"] = "Mac OS"
|
||||
parts := strings.Split(userAgent, "Mac OS X ")
|
||||
if len(parts) > 1 {
|
||||
version := strings.Split(parts[1], ";")[0]
|
||||
osInfo["version"] = version
|
||||
}
|
||||
} else if strings.Contains(userAgent, "Linux") {
|
||||
osInfo["os"] = "Linux"
|
||||
osInfo["version"] = "Unknown"
|
||||
} else if strings.Contains(userAgent, "Android") {
|
||||
osInfo["os"] = "Android"
|
||||
parts := strings.Split(userAgent, "Android ")
|
||||
if len(parts) > 1 {
|
||||
version := strings.Split(parts[1], ";")[0]
|
||||
osInfo["version"] = version
|
||||
}
|
||||
} else if strings.Contains(userAgent, "iPhone") || strings.Contains(userAgent, "iPad") || strings.Contains(userAgent, "iPod") {
|
||||
osInfo["os"] = "iOS"
|
||||
parts := strings.Split(userAgent, "OS ")
|
||||
if len(parts) > 1 {
|
||||
version := strings.Split(parts[1], " ")[0]
|
||||
osInfo["version"] = version
|
||||
}
|
||||
} else {
|
||||
osInfo["os"] = "Unknown"
|
||||
osInfo["version"] = "Unknown"
|
||||
}
|
||||
|
||||
return browserInfo, osInfo
|
||||
}
|
@ -4,19 +4,22 @@ import (
|
||||
"errors"
|
||||
"github.com/a-h/templ"
|
||||
"github.com/fossyy/filekeeper/cache"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/fossyy/filekeeper/logger"
|
||||
"github.com/fossyy/filekeeper/session"
|
||||
"github.com/fossyy/filekeeper/types"
|
||||
"github.com/fossyy/filekeeper/utils"
|
||||
signinView "github.com/fossyy/filekeeper/view/signin"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var log *logger.AggregatedLogger
|
||||
var errorMessages = make(map[string]string)
|
||||
|
||||
func init() {
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
errorMessages = map[string]string{
|
||||
"redirect_uri_mismatch": "The redirect URI provided does not match the one registered with our service. Please contact the administrator for assistance.",
|
||||
@ -92,6 +95,20 @@ func POST(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
if email == userData.Email && utils.CheckPasswordHash(password, userData.Password) {
|
||||
if userData.Totp != "" {
|
||||
storeSession := session.Create()
|
||||
storeSession.Values["user"] = types.User{
|
||||
UserID: userData.UserID,
|
||||
Email: email,
|
||||
Username: userData.Username,
|
||||
Totp: userData.Totp,
|
||||
Authenticated: false,
|
||||
}
|
||||
storeSession.Save(w)
|
||||
http.Redirect(w, r, "/auth/totp", http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
storeSession := session.Create()
|
||||
storeSession.Values["user"] = types.User{
|
||||
UserID: userData.UserID,
|
||||
|
86
handler/user/totp/setup.go
Normal file
86
handler/user/totp/setup.go
Normal file
@ -0,0 +1,86 @@
|
||||
package userHandlerTotpSetup
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/fossyy/filekeeper/cache"
|
||||
userTotpSetupView "github.com/fossyy/filekeeper/view/user/totp"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/fossyy/filekeeper/db"
|
||||
"github.com/fossyy/filekeeper/types"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"github.com/xlzd/gotp"
|
||||
)
|
||||
|
||||
func generateQRCode(uri string) (string, error) {
|
||||
qr, err := qrcode.New(uri, qrcode.Medium)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate QR code: %w", err)
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
if err := png.Encode(&buffer, qr.Image(256)); err != nil {
|
||||
return "", fmt.Errorf("failed to encode QR code to PNG: %w", err)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(buffer.Bytes()), nil
|
||||
}
|
||||
|
||||
func GET(w http.ResponseWriter, r *http.Request) {
|
||||
secret := gotp.RandomSecret(16)
|
||||
userSession := r.Context().Value("user").(types.User)
|
||||
totp := gotp.NewDefaultTOTP(secret)
|
||||
uri := totp.ProvisioningUri(userSession.Email, "filekeeper")
|
||||
base64Str, err := generateQRCode(uri)
|
||||
if err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
component := userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession)
|
||||
if err := component.Render(r.Context(), w); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func POST(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
code := r.Form.Get("totp")
|
||||
secret := r.Form.Get("secret")
|
||||
totp := gotp.NewDefaultTOTP(secret)
|
||||
userSession := r.Context().Value("user").(types.User)
|
||||
if totp.Verify(code, time.Now().Unix()) {
|
||||
if err := db.DB.InitializeTotp(userSession.Email, secret); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
cache.DeleteUser(userSession.Email)
|
||||
fmt.Fprint(w, "Authentication successful! Access granted.")
|
||||
return
|
||||
} else {
|
||||
uri := totp.ProvisioningUri(userSession.Email, "filekeeper")
|
||||
|
||||
base64Str, err := generateQRCode(uri)
|
||||
if err != nil {
|
||||
fmt.Printf("%v\n", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
component := userTotpSetupView.Main("Filekeeper - 2FA Setup Page", base64Str, secret, userSession)
|
||||
if err := component.Render(r.Context(), w); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
googleOauthHandler "github.com/fossyy/filekeeper/handler/auth/google"
|
||||
googleOauthCallbackHandler "github.com/fossyy/filekeeper/handler/auth/google/callback"
|
||||
googleOauthSetupHandler "github.com/fossyy/filekeeper/handler/auth/google/setup"
|
||||
totpHandler "github.com/fossyy/filekeeper/handler/auth/totp"
|
||||
downloadHandler "github.com/fossyy/filekeeper/handler/download"
|
||||
downloadFileHandler "github.com/fossyy/filekeeper/handler/download/file"
|
||||
forgotPasswordHandler "github.com/fossyy/filekeeper/handler/forgotPassword"
|
||||
@ -16,6 +17,7 @@ import (
|
||||
uploadHandler "github.com/fossyy/filekeeper/handler/upload"
|
||||
"github.com/fossyy/filekeeper/handler/upload/initialisation"
|
||||
userHandler "github.com/fossyy/filekeeper/handler/user"
|
||||
userHandlerTotpSetup "github.com/fossyy/filekeeper/handler/user/totp"
|
||||
"github.com/fossyy/filekeeper/middleware"
|
||||
"net/http"
|
||||
)
|
||||
@ -46,7 +48,17 @@ func SetupRoutes() *http.ServeMux {
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.HandleFunc("/totp", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
middleware.Guest(totpHandler.GET, w, r)
|
||||
case http.MethodPost:
|
||||
middleware.Guest(totpHandler.POST, w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
authRouter.HandleFunc("/google/callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
@ -129,6 +141,17 @@ func SetupRoutes() *http.ServeMux {
|
||||
}
|
||||
})
|
||||
|
||||
handler.HandleFunc("/user/totp/setup", func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
middleware.Auth(userHandlerTotpSetup.GET, w, r)
|
||||
case http.MethodPost:
|
||||
middleware.Auth(userHandlerTotpSetup.POST, w, r)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
})
|
||||
|
||||
// Upload router
|
||||
uploadRouter := http.NewServeMux()
|
||||
handler.Handle("/upload/", http.StripPrefix("/upload", uploadRouter))
|
||||
|
@ -1,2 +1,2 @@
|
||||
CREATE TABLE IF NOT EXISTS users (user_id VARCHAR(255) PRIMARY KEY NOT NULL,username VARCHAR(255) UNIQUE NOT NULL,email VARCHAR(255) UNIQUE NOT NULL,password TEXT NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS users (user_id VARCHAR(255) PRIMARY KEY NOT NULL,username VARCHAR(255) UNIQUE NOT NULL,email VARCHAR(255) UNIQUE NOT NULL,password TEXT NOT NULL,totp VARCHAR(255) DEFAULT NULL);
|
||||
CREATE TABLE IF NOT EXISTS files (id VARCHAR(255) PRIMARY KEY NOT NULL,owner_id VARCHAR(255) NOT NULL,name TEXT NOT NULL,size BIGINT NOT NULL,downloaded BIGINT NOT NULL,uploaded_byte BIGINT NOT NULL DEFAULT 0, uploaded_chunk BIGINT NOT NULL DEFAULT -1,done BOOLEAN NOT NULL DEFAULT FALSE,FOREIGN KEY (owner_id) REFERENCES users(user_id));
|
@ -176,6 +176,14 @@ func GetSession(r *http.Request) (UserStatus, types.User, string) {
|
||||
return Unauthorized, types.User{}, ""
|
||||
}
|
||||
|
||||
if !userSession.Authenticated && userSession.Totp != "" {
|
||||
return Unauthorized, userSession, cookie.Value
|
||||
}
|
||||
|
||||
if !userSession.Authenticated {
|
||||
return Unauthorized, types.User{}, ""
|
||||
}
|
||||
|
||||
return Authorized, userSession, cookie.Value
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ type User struct {
|
||||
Username string `gorm:"unique;not null"`
|
||||
Email string `gorm:"unique;not null"`
|
||||
Password string `gorm:"not null"`
|
||||
Totp string `gorm:"not null"`
|
||||
}
|
||||
|
||||
type File struct {
|
||||
|
@ -13,6 +13,7 @@ type User struct {
|
||||
UserID uuid.UUID
|
||||
Email string
|
||||
Username string
|
||||
Totp string
|
||||
Authenticated bool
|
||||
}
|
||||
|
||||
|
54
view/totp/totp.templ
Normal file
54
view/totp/totp.templ
Normal file
@ -0,0 +1,54 @@
|
||||
package totpView
|
||||
|
||||
import (
|
||||
"github.com/fossyy/filekeeper/view/layout"
|
||||
)
|
||||
|
||||
templ content(title string) {
|
||||
@layout.Base(title){
|
||||
<main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10">
|
||||
<div class="flex min-h-screen items-center justify-center bg-background px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-foreground">Verify Your Identity</h2>
|
||||
<p class="mt-2 text-center text-sm text-muted-foreground">
|
||||
Enter the 6-digit code sent to your registered device to complete the login process.
|
||||
</p>
|
||||
</div>
|
||||
<form class="space-y-6" method="POST">
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-muted-foreground">
|
||||
Verification Code
|
||||
</label>
|
||||
<div class="mt-1">
|
||||
<input
|
||||
class="h-10 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 block w-full appearance-none rounded-md border border-input bg-background px-3 py-2 placeholder-muted-foreground shadow-sm focus:border-primary focus:outline-none focus:ring-primary sm:text-sm"
|
||||
id="code"
|
||||
autocomplete="one-time-code"
|
||||
required=""
|
||||
placeholder="123456"
|
||||
pattern="[0-9]{6}"
|
||||
maxlength="6"
|
||||
type="text"
|
||||
name="code"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="items-center whitespace-nowrap ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-10 flex w-full justify-center rounded-md bg-primary py-2 px-4 text-sm font-medium text-primary-foreground shadow-sm hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2"
|
||||
type="submit"
|
||||
>
|
||||
Verify Code
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
||||
templ Main(title string) {
|
||||
@content(title)
|
||||
}
|
97
view/user/totp/setup.templ
Normal file
97
view/user/totp/setup.templ
Normal file
@ -0,0 +1,97 @@
|
||||
package userTotpSetupView
|
||||
|
||||
import (
|
||||
"github.com/fossyy/filekeeper/view/layout"
|
||||
"github.com/fossyy/filekeeper/types"
|
||||
)
|
||||
|
||||
templ content(title string, qrcode string, code string, user types.User) {
|
||||
@layout.Base(title){
|
||||
@layout.Navbar(user)
|
||||
<main class="container mx-auto px-4 py-12 md:px-6 md:py-16 lg:py-10">
|
||||
<div class="mx-auto max-w-md px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div class="space-y-6 text-center">
|
||||
<div class="flex items-center">
|
||||
<a
|
||||
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-slate-200 hover:text-accent-foreground h-10 w-10 mr-4"
|
||||
href="/user" hx-get="/user" hx-swap="outerHTML" hx-push-url="true" hx-target="#content"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-5 w-5"
|
||||
>
|
||||
<path d="m12 19-7-7 7-7"></path>
|
||||
<path d="M19 12H5"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<h1 class="text-3xl font-bold">Set up Two-Factor Authentication</h1>
|
||||
</div>
|
||||
<p class="text-muted-foreground">Secure your account with time-based one-time passwords (TOTP).</p>
|
||||
<div class="mt-4 text-left text-muted-foreground">
|
||||
<p>Here's how to set up the Google Authenticator app:</p>
|
||||
<ol class="list-decimal pl-6">
|
||||
<li>Download the Google Authenticator app on your mobile device.</li>
|
||||
<li>Open the app and tap "Begin Setup".</li>
|
||||
<li>Select "Scan a barcode" and point your camera at the QR code below.</li>
|
||||
<li>The app will automatically add your account and display a 6-digit code.</li>
|
||||
<li>Enter this code on the website to complete the setup.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded-lg border rounded-lg bg-muted p-6bg-card text-card-foreground shadow-sm mt-5" data-v0-t="card">
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex items-center justify-center">
|
||||
<img
|
||||
src={"data:image/png;base64," + qrcode}
|
||||
width="200"
|
||||
height="200"
|
||||
alt="QR Code"
|
||||
class="rounded-lg"
|
||||
style="aspect-ratio: 200 / 200; object-fit: cover;"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-6 space-y-2">
|
||||
<p class="font-medium">Backup Code:</p>
|
||||
<div class="rounded-md bg-background px-4 py-2 text-sm font-mono text-muted-foreground">12345-67890</div>
|
||||
<p class="font-medium">TOTP Secret:</p>
|
||||
<div class="rounded-md bg-background px-4 py-2 text-sm font-mono text-muted-foreground">
|
||||
{code}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="/user/totp/setup">
|
||||
<div class="grid gap-2">
|
||||
<label
|
||||
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
for="totp">
|
||||
Totp Code
|
||||
</label>
|
||||
<input id="secret" name="secret" value={code} type='hidden' />
|
||||
<input
|
||||
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
id="totp" name="totp" placeholder="Code from authenticator app" />
|
||||
<div class="flex items-center p-6">
|
||||
<button class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2 w-full" type="submit">
|
||||
Enable TOTP
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@layout.Footer()
|
||||
}
|
||||
}
|
||||
|
||||
templ Main(title string, qrcode string, code string, user types.User) {
|
||||
@content(title, qrcode, code, user)
|
||||
}
|
@ -71,15 +71,13 @@ templ content(title string, user types.User, ListSession []*session.SessionInfo)
|
||||
for="two-factor">
|
||||
Two-Factor Authentication
|
||||
</label>
|
||||
<label class="inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" value="" class="sr-only peer" checked />
|
||||
<div
|
||||
class="relative w-11 h-6 bg-gray-200 rounded-full peer peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600">
|
||||
</div>
|
||||
</label>
|
||||
<input type="checkbox" aria-hidden="true"
|
||||
style="transform:translateX(-100%);position:absolute;pointer-events:none;opacity:0;margin:0"
|
||||
tabindex="-1" value="on" />
|
||||
<a
|
||||
class="hover:bg-gray-200 inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
|
||||
type="button" id="radix-:rq:" aria-haspopup="menu"
|
||||
aria-expanded="false" data-state="closed"
|
||||
href="/user/totp/setup" hx-get="/user/totp/setup" hx-swap="outerHTML" hx-push-url="true" hx-target="#content">
|
||||
Setup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
|
Reference in New Issue
Block a user