refactoring db; added email-verification

This commit is contained in:
$(pass /github/name)
2024-07-08 23:43:55 +02:00
parent 555d1be575
commit 87e9f71ceb
20 changed files with 890 additions and 255 deletions

View File

@@ -11,6 +11,6 @@
"AdminEmail": "admin@server.com"
},
"templates": {
"MailDir": "templates"
"MailDir": "templates/email"
}
}

View File

@@ -18,15 +18,17 @@ type UserController struct {
emailService services.EmailService
consentService services.ConsentService
bankAccountService services.BankAccountService
membershipService services.MembershipService
}
type RegistrationData struct {
User models.User `json:"user"`
BankAccount models.BankAccount `json:"bank_account"`
Membership models.Membership `json:"membership"`
}
func NewUserController(service services.UserService, emailService *services.EmailService, consentService services.ConsentService, bankAccountService services.BankAccountService) *UserController {
return &UserController{service, *emailService, consentService, bankAccountService}
func NewUserController(service services.UserService, emailService *services.EmailService, consentService services.ConsentService, bankAccountService services.BankAccountService, membershipService services.MembershipService) *UserController {
return &UserController{service, *emailService, consentService, bankAccountService, membershipService}
}
func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) {
@@ -42,7 +44,8 @@ func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) {
}
logger.Info.Printf("registering user: %v", regData.User)
id, err := uc.service.RegisterUser(&regData.User)
// Register User
id, token, err := uc.service.RegisterUser(&regData.User)
if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error.Printf("Couldn't register User: %v", err)
@@ -50,13 +53,16 @@ func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) {
return
}
regData.User.ID = id
// Register Bank Account
_, err = uc.bankAccountService.RegisterBankAccount(&regData.BankAccount)
if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error.Printf("Couldn't register bank account: %v", err)
rh.RespondWithError(http.StatusInternalServerError, "Couldn't register User")
rh.RespondWithError(http.StatusInternalServerError, "Couldn't register User-BankAccount")
return
}
// Register Consents
var consents = [2]models.Consent{
{
FirstName: regData.User.FirstName,
@@ -74,19 +80,25 @@ func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) {
for _, consent := range consents {
_, err = uc.consentService.RegisterConsent(&consent)
if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error.Printf("Couldn't register consent: %v", err)
rh.RespondWithError(http.StatusInternalServerError, "Couldn't register User")
rh.RespondWithError(http.StatusInternalServerError, "Couldn't register User-consent")
return
}
}
// Send welcome email to the user
if err := uc.emailService.SendWelcomeEmail(&regData.User); err != nil {
logger.Error.Printf("Failed to send welcome email to user: %v", err)
// rh.RespondWithError(http.StatusServiceUnavailable, "User creation succeeded, but failed to send welcome email to user")
// Register Membership
_, err = uc.membershipService.RegisterMembership(&regData.Membership)
if err != nil {
logger.Error.Printf("Couldn't register membership: %v", err)
rh.RespondWithError(http.StatusInternalServerError, "Couldn't register User-membership")
return
}
// Send notifications
if err := uc.emailService.SendVerificationEmail(&regData.User, &token); err != nil {
logger.Error.Printf("Failed to send email verification email to user: %v", err)
// rh.RespondWithError(http.StatusServiceUnavailable, "User creation succeeded, but failed to send welcome email to user")
}
// Notify admin of new user registration
if err := uc.emailService.NotifyAdminOfNewUser(&regData.User); err != nil {
logger.Error.Printf("Failed to notify admin of new user registration: %v", err)
@@ -98,6 +110,30 @@ func (uc *UserController) RegisterUser(w http.ResponseWriter, r *http.Request) {
})
}
func (uc *UserController) VerifyMailHandler(w http.ResponseWriter, r *http.Request) {
rh := utils.NewResponseHandler(w)
token := r.URL.Query().Get("token")
if token == "" {
logger.Error.Println("Missing token to verify mail")
rh.RespondWithError(http.StatusNoContent, "Missing token")
return
}
user, err := uc.service.VerifyUser(&token)
if err != nil {
logger.Error.Printf("Cannot verify user: %v", err)
rh.RespondWithError(http.StatusUnauthorized, "Cannot verify user")
}
membership, err := uc.membershipService.FindMembershipByUserID(user.ID)
if err != nil {
logger.Error.Printf("Cannot get membership of user %v: %v", user.ID, err)
rh.RespondWithError(http.StatusInternalServerError, "Cannot get Membership of user")
}
uc.emailService.SendWelcomeEmail(user, membership)
}
/* func (uc *UserController) LoginUser(w http.ResponseWriter, r *http.Request) {
var credentials struct {
Email string `json:"email"`

View File

@@ -1,5 +1,5 @@
CREATE TABLE users (
id INT PRIMARY KEY,
id INTEGER PRIMARY KEY,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
first_name VARCHAR(50) NOT NULL,
@@ -11,7 +11,6 @@ CREATE TABLE users (
drivers_id_checked BOOLEAN,
role_id INT,
payment_status VARCHAR(50),
membership_status VARCHAR(50),
date_of_birth DATE,
address VARCHAR(255),
profile_picture VARCHAR(255),
@@ -21,7 +20,7 @@ CREATE TABLE users (
);
CREATE TABLE banking (
id INT PRIMARY KEY,
id INTEGER PRIMARY KEY,
user_id INT,
iban VARCHAR(34) NOT NULL,
bic VARCHAR(11) NOT NULL,
@@ -32,25 +31,31 @@ CREATE TABLE banking (
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE membership (
id INT PRIMARY KEY,
user_id INT,
membership_fee DECIMAL(10, 2),
rental_fee DECIMAL(10, 2),
CREATE TABLE memberships (
id INTEGER PRIMARY KEY,
user_id INT NOT NULL,
parent_id INT,
model_id INT,
start_date DATE,
end_date DATE,
status VARCHAR(50),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE membership_plans (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
monthly_fee DECIMAL(10, 2),
hourly_rate DECIMAL(10, 2),
included_hours_per_year INT,
remaining_hours_per_year INT,
included_hours_per_month INT,
remaining_hours_per_month INT,
membership_model VARCHAR(50),
membership_id VARCHAR(50),
membership_start_date DATE,
membership_end_date DATE,
discount_rate DECIMAL(5, 2),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
details TEXT
);
CREATE TABLE rentals (
id INT PRIMARY KEY,
id INTEGER PRIMARY KEY,
car_id INT,
user_id INT,
start_datetime DATETIME NOT NULL,
@@ -64,12 +69,12 @@ CREATE TABLE rentals (
);
CREATE TABLE roles (
id INT PRIMARY KEY,
id INTEGER PRIMARY KEY,
role_name VARCHAR(50) NOT NULL
);
CREATE TABLE consents (
id INT PRIMARY KEY,
id INTEGER PRIMARY KEY,
user_id INT,
updated_at DATE,
created_at DATE,
@@ -81,7 +86,7 @@ CREATE TABLE consents (
);
CREATE TABLE cars (
id INT PRIMARY KEY,
id INTEGER PRIMARY KEY,
licence_plate VARCHAR(15) NOT NULL UNIQUE,
insurance VARCHAR(255) NOT NULL,
acquired_date DATE NOT NULL,
@@ -100,10 +105,10 @@ CREATE TABLE cars (
);
CREATE TABLE email_verifications (
id INT PRIMARY KEY,
id INTEGER PRIMARY KEY,
user_id INT,
verification_token VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL,
verified_at DATETIME,
verified_at DATETIME DEFAULT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@@ -0,0 +1,12 @@
package models
import "time"
type Membership struct {
StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"`
Status string `json:"status"`
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
ParentID int64 `json:"parent_id"`
}

View File

@@ -0,0 +1,10 @@
package models
type MembershipPlan struct {
Name string `json:"name"`
MonthlyFee float32 `json:"monthly_fee"`
HourlyRate float32 `json:"hourly_rate"`
IncludedPerYear int16 `json:"included_hours_per_year"`
IncludedPerMonth int16 `json:"included_hours_per_month"`
Details string `json:"details"`
}

View File

@@ -5,10 +5,19 @@ import "time"
type User struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Password string `json:"password"`
LastName string `json:"last_name"`
DateOfBirth time.Time `json:"date_of_birth"`
Email string `json:"email"`
ProfilePicture string `json:"profile_picture"`
FirstName string `json:"first_name"`
Salt string `json:"-"`
LastName string `json:"last_name"`
Phone string `json:"phone"`
Status string `json:"status"`
Notes string `json:"notes"`
PaymentStatus string `json:"payment_status"`
Password string `json:"password"`
Address string `json:"address"`
ID int64 `json:"id"`
RoleID int8 `json:"role_id"`
DriversIDChecked bool `json:"drivers_id_checked"`
}

View File

@@ -0,0 +1,53 @@
package repositories
import (
"database/sql"
"time"
"GoMembership/internal/models"
"GoMembership/pkg/errors"
)
type MembershipRepository interface {
CreateMembership(account *models.Membership) (int64, error)
FindMembershipByUserID(userID int64) (*models.Membership, error)
}
type membershipRepository struct {
db *sql.DB
}
func NewMembershipRepository(db *sql.DB) MembershipRepository {
return &membershipRepository{db}
}
func (repo *membershipRepository) CreateMembership(membership *models.Membership) (int64, error) {
query := "INSERT INTO memberships (user_id, model_id, start_date, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
result, err := repo.db.Exec(query, membership.UserID, membership.MonthlyFee, membership.RentalFee, membership.Model, time.Now(), membership.Status)
if err != nil {
return -1, err
}
lastInsertID, err := result.LastInsertId()
if err != nil {
return -1, err
}
return lastInsertID, err
}
func (repo *membershipRepository) FindMembershipByUserID(userID int64) (*models.Membership, error) {
var membership models.Membership
query := "SELECT id, model_id, start_date, end_date, status FROM memberships where user_id = ?"
err := repo.db.QueryRow(query, userID).Scan(&membership.ID, &membership.MonthlyFee, &membership.RentalFee, &membership.Model, &membership.StartDate, &membership.EndDate, &membership.Status)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.ErrNotFound
}
return nil, err
}
membership.UserID = userID
return &membership, nil
}

View File

@@ -4,12 +4,18 @@ import (
"GoMembership/internal/models"
"GoMembership/pkg/errors"
"database/sql"
"fmt"
"strings"
"time"
)
type UserRepository interface {
CreateUser(user *models.User) (int64, error)
FindUserByID(id int) (*models.User, error)
FindUserByID(id int64) (*models.User, error)
FindUserByEmail(email string) (*models.User, error)
SetVerificationToken(user *models.User, token *string) (int64, error)
IsVerified(userID *int64) (bool, error)
VerifyUserOfToken(token *string) (int64, error)
}
type userRepository struct {
@@ -21,8 +27,24 @@ func NewUserRepository(db *sql.DB) UserRepository {
}
func (repo *userRepository) CreateUser(user *models.User) (int64, error) {
query := "INSERT INTO users (first_name, last_name, email, password, salt, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
result, err := repo.db.Exec(query, user.FirstName, user.LastName, user.Email, user.Password, user.Salt, user.CreatedAt, user.UpdatedAt)
query := "INSERT INTO users (first_name, last_name, email, phone, drivers_id_checked, role_id, payment_status, date_of_birth, address, profile_picture, notes, status, password, salt, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
result, err := repo.db.Exec(query,
user.FirstName,
user.LastName,
user.Email,
user.Phone,
user.DriversIDChecked,
user.RoleID,
user.PaymentStatus,
user.DateOfBirth,
user.Address,
user.ProfilePicture,
user.Notes,
user.Status,
user.Password,
user.Salt,
user.CreatedAt,
user.UpdatedAt)
if err != nil {
return -1, err
}
@@ -31,14 +53,50 @@ func (repo *userRepository) CreateUser(user *models.User) (int64, error) {
if err != nil {
return -1, err
}
return lastInsertID, err
}
func (repo *userRepository) FindUserByID(id int) (*models.User, error) {
func (repo *userRepository) UpdateUser(userID int64, updates map[string]interface{}) error {
if len(updates) == 0 {
return errors.ErrNoData
}
// Construct the query
setClauses := make([]string, 0, len(updates))
args := make([]interface{}, 0, len(updates)+1)
for column, value := range updates {
setClauses = append(setClauses, fmt.Sprintf("%s = ?", column))
args = append(args, value)
}
args = append(args, userID)
query := fmt.Sprintf("UPDATE users SET %s WHERE id = ?", strings.Join(setClauses, ", "))
// Execute the query
_, err := repo.db.Exec(query, args...)
if err != nil {
return err
}
return nil
}
func (repo *userRepository) FindUserByID(id int64) (*models.User, error) {
var user models.User
query := "SELECT id, first_name, last_name, email FROM users WHERE id = ?"
err := repo.db.QueryRow(query, id).Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email)
query := "SELECT id, first_name, last_name, email, phone, drivers_id_checked, role_id, payment_status, date_of_birth, address, profile_picture, notes, status FROM users WHERE id = ?"
err := repo.db.QueryRow(query, id).Scan(&user.ID,
&user.FirstName,
&user.LastName,
&user.Email,
&user.Phone,
&user.DriversIDChecked,
&user.RoleID,
&user.PaymentStatus,
&user.DateOfBirth,
&user.Address,
&user.ProfilePicture,
&user.Notes,
&user.Status)
if err != nil {
if err == sql.ErrNoRows {
return nil, errors.ErrUserNotFound
@@ -60,3 +118,70 @@ func (repo *userRepository) FindUserByEmail(email string) (*models.User, error)
}
return &user, nil
}
func (repo *userRepository) IsVerified(userID *int64) (bool, error) {
var status string
query := "SELECT status FROM users where id = ?"
err := repo.db.QueryRow(query, userID).Scan(&status)
if err != nil {
if err == sql.ErrNoRows {
return false, errors.ErrUserNotFound
}
return false, err
}
return status != "unverified", nil
}
func (repo *userRepository) VerifyUserOfToken(token *string) (int64, error) {
var userID int64
err := repo.db.QueryRow("SELECT user_id FROM email_verifications WHERE verification_token = $1", token).Scan(&userID)
if err == sql.ErrNoRows {
return -1, errors.ErrTokenNotFound
} else if err != nil {
return -1, err
}
verified, err := repo.IsVerified(&userID)
if err != nil {
return -1, err
}
if verified {
return userID, errors.ErrAlreadyVerified
}
update := map[string]interface{}{
"status": "active",
}
err = repo.UpdateUser(userID, update)
if err != nil {
return -1, err
}
query := "UPDATE email_verifications SET verified_at = ? WHERE user_id = ?"
_, err = repo.db.Exec(query, time.Now(), userID)
if err != nil {
return -1, err
}
return userID, nil
}
func (repo *userRepository) SetVerificationToken(user *models.User, token *string) (int64, error) {
// check if user already verified
verified, err := repo.IsVerified(&user.ID)
if err != nil {
return -1, err
}
if verified {
return -1, errors.ErrAlreadyVerified
}
query := "INSERT OR REPLACE INTO email_verifications (user_id, verification_token, created_at) VALUES (?, ?, ?)"
result, err := repo.db.Exec(query, user.ID, token, time.Now())
if err != nil {
return -1, err
}
lastInsertID, err := result.LastInsertId()
if err != nil {
return -1, err
}
return lastInsertID, nil
}

View File

@@ -11,7 +11,7 @@ import (
func RegisterRoutes(router *mux.Router, userController *controllers.UserController) {
logger.Info.Println("Registering /api/register route")
// router.HandleFunc("/", homeHandler)
router.HandleFunc("/backend/api/verify", userController.VerifyMailHandler).Methods("GET")
router.HandleFunc("/backend/api/register", userController.RegisterUser).Methods("POST")
// router.HandleFunc("/login", userController.LoginUser).Methods("POST")

View File

@@ -25,9 +25,11 @@ func Run() {
consentService := services.NewConsentService(consentRepo)
bankAccountRepo := repositories.NewBankAccountRepository(db)
bankAccountService := services.NewBankAccountService(bankAccountRepo)
membershipRepo := repositories.NewMembershipRepository(db)
membershipService := services.NewMembershipService(membershipRepo)
userRepo := repositories.NewUserRepository(db)
userService := services.NewUserService(userRepo)
userController := controllers.NewUserController(userService, emailService, consentService, bankAccountService)
userController := controllers.NewUserController(userService, emailService, consentService, bankAccountService, membershipService)
router := mux.NewRouter()
// router.Handle("/csrf-token", middlewares.GenerateCSRFTokenHandler()).Methods("GET")

View File

@@ -53,14 +53,43 @@ func ParseTemplate(filename string, data interface{}) (string, error) {
return tplBuffer.String(), nil
}
func (s *EmailService) SendWelcomeEmail(user *models.User) error {
func (s *EmailService) SendVerificationEmail(user *models.User, token *string) error {
// Prepare data to be injected into the template
data := struct {
FirstName string
LastName string
Token string
}{
FirstName: user.FirstName,
LastName: user.LastName,
Token: *token,
}
subject := "Nur noch ein kleiner Schritt!"
body, err := ParseTemplate("mail_verification.html", data)
if err != nil {
logger.Error.Print("Couldn't send verification mail")
return err
}
return s.SendEmail(user.Email, subject, body)
}
func (s *EmailService) SendWelcomeEmail(user *models.User, membership *models.Membership) error {
// Prepare data to be injected into the template
data := struct {
FirstName string
MembershipModel string
MembershipID int64
MembershipFee float32
RentalFee float32
}{
FirstName: user.FirstName,
MembershipModel: membership.Model,
MembershipID: membership.ID,
MembershipFee: float32(membership.MonthlyFee),
RentalFee: float32(membership.RentalFee),
}
subject := "Willkommen beim Dörpsmobil Hasloh e.V."

View File

@@ -0,0 +1,29 @@
package services
import (
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"time"
)
type MembershipService interface {
RegisterMembership(membership *models.Membership) (int64, error)
FindMembershipByUserID(userID int64) (*models.Membership, error)
}
type membershipService struct {
repo repositories.MembershipRepository
}
func NewMembershipService(repo repositories.MembershipRepository) MembershipService {
return &membershipService{repo}
}
func (service *membershipService) RegisterMembership(membership *models.Membership) (int64, error) {
membership.StartDate = time.Now()
return service.repo.CreateMembership(membership)
}
func (service *membershipService) FindMembershipByUserID(userID int64) (*models.Membership, error) {
return service.repo.FindMembershipByUserID(userID)
}

View File

@@ -3,7 +3,9 @@ package services
import (
"GoMembership/internal/models"
"GoMembership/internal/repositories"
// "GoMembership/pkg/errors"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
// "crypto/rand"
// "encoding/base64"
// "golang.org/x/crypto/bcrypt"
@@ -11,8 +13,9 @@ import (
)
type UserService interface {
RegisterUser(user *models.User) (int64, error)
// AuthenticateUser(email, password string) (*models.User, error)
RegisterUser(user *models.User) (int64, string, error)
// AuthenticateUser(email, password string) (*models.User, error)A
VerifyUser(token *string) (*models.User, error)
}
type userService struct {
@@ -23,7 +26,7 @@ func NewUserService(repo repositories.UserRepository) UserService {
return &userService{repo}
}
func (service *userService) RegisterUser(user *models.User) (int64, error) {
func (service *userService) RegisterUser(user *models.User) (int64, string, error) {
/* salt := make([]byte, 16)
if _, err := rand.Read(salt); err != nil {
return -1, err
@@ -35,16 +38,45 @@ func (service *userService) RegisterUser(user *models.User) (int64, error) {
return -1, err
}
user.Password = string(hashedPassword) */
user.Password = ""
user.Status = "unverified"
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
return service.repo.CreateUser(user)
id, err := service.repo.CreateUser(user)
if err != nil {
return -1, "", err
}
user.ID = id
token, err := utils.GenerateVerificationToken()
if err != nil {
return -1, "", err
}
logger.Info.Printf("user: %+v", user)
_, err = service.repo.SetVerificationToken(user, &token)
if err != nil {
return -1, "", err
}
return id, token, nil
}
func (service *userService) VerifyUser(token *string) (*models.User, error) {
userID, err := service.repo.VerifyUserOfToken(token)
if err != nil {
return nil, err
}
user, err := service.repo.FindUserByID(userID)
if err != nil {
return nil, err
}
return user, nil
}
/* func HashPassword(password string, salt string) (string, error) {
saltedPassword := password + salt
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(saltedPassword), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(hashedPassword), nil

View File

@@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen beim Dörpsmobil Hasloh e.V.</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 20px;
background-color: #f9f9f9;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
}
h1 {
color: #007b5e;
}
p {
margin-bottom: 1em;
}
.contact-info {
margin-top: 20px;
}
.contact-info strong {
display: block;
margin-bottom: 5px;
}
a {
color: #007b5e;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>Willkommen beim Dörpsmobil Hasloh e.V.</h1>
<p>Hallo {{.FirstName}},</p>
<p>herzlich willkommen beim Dörpsmobil Hasloh e.V.! Vielen Dank für Ihre Registrierung und Ihre Unterstützung unseres Projekts.</p>
<p>Wir freuen uns, Sie als Mitglied begrüßen zu dürfen und wünschen Ihnen stets eine sichere und angenehme Fahrt mit unseren Fahrzeugen.</p>
<p>Für weitere Fragen stehen wir Ihnen gerne zur Verfügung:</p>
<div class="contact-info">
<strong>Kontakt:</strong>
Name: Anke Freitag<br>
Vorsitzende Doerpsmobil-Hasloh e.V.<br>
E-Mail: <a href="mailto:info@doerpsmobil-hasloh.de">info@doerpsmobil-hasloh.de</a><br>
Telefon: +49 174 870 1392
</div>
<p>Besuchen Sie auch unsere Webseite unter <a href="https://carsharing-hasloh.de">https://carsharing-hasloh.de</a> für weitere Informationen.</p>
<p>Mit freundlichen Grüßen,<br>
Ihr Team von Dörpsmobil Hasloh e.V.
</p>
</div>
</body>
</html>

View File

@@ -13,3 +13,7 @@ func GenerateRandomString(length int) (string, error) {
}
return base64.URLEncoding.EncodeToString(bytes), nil
}
func GenerateVerificationToken() (string, error) {
return GenerateRandomString(32)
}

View File

@@ -3,7 +3,12 @@ package errors
import "errors"
var (
ErrNotFound = errors.New("not found")
ErrUserNotFound = errors.New("user not found")
ErrInvalidEmail = errors.New("invalid email")
ErrInvalidCredentials = errors.New("invalid credentials: unauthorized")
ErrAlreadyVerified = errors.New("user is already verified")
ErrTokenNotFound = errors.New("verification token not found")
ErrTokenNotSet = errors.New("verification token has not been set")
ErrNoData = errors.New("no data provided")
)

View File

@@ -1,67 +0,0 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen beim Dörpsmobil Hasloh e.V.</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 20px;
background-color: #f9f9f9;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
}
h1 {
color: #007b5e;
}
p {
margin-bottom: 1em;
}
.contact-info {
margin-top: 20px;
}
.contact-info strong {
display: block;
margin-bottom: 5px;
}
a {
color: #007b5e;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>{{.Firstname}} {{.LastName}} hat sich registriert</h1>
<p>Ein neues Vereinsmitglied hat sich registriert</p>
<p>{{.Firstname}} {{.LastName}} hat sich registriert. Hier sind die Daten:</p>
<br>
<div class="contact-info">
<strong>Registrierungsdaten:</strong>
Name: {{.Firstname}} {{.LastName}} <br>
Email: {{.Email}}<br>
IBAN: {{.IBAN}}<br>
BIC: {{.BIC}}<br>
Mandatsreferenz: {{.MandateReference}}<br>
</div>
<p>Mit freundlichen Grüßen,<br>
der Server
</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,197 @@
<!doctype html>
<html>
<body>
<div
style="
background-color: #f2f5f7;
color: #242424;
font-family: Optima, Candara, &quot;Noto Sans&quot;, source-sans-pro,
sans-serif;
font-size: 16px;
font-weight: 400;
letter-spacing: 0.15008px;
line-height: 1.5;
margin: 0;
padding: 32px 0;
min-height: 100%;
width: 100%;
"
>
<table
align="center"
width="100%"
style="margin: 0 auto; max-width: 600px; background-color: #ffffff"
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
>
<tbody>
<tr style="width: 100%">
<td>
<div style="padding: 24px 24px 24px 24px; text-align: center">
<a
href="https://carsharing-hasloh.de"
style="text-decoration: none"
target="_blank"
><img
alt="Dörpsmobil Hasloh"
src="https://carsharing-hasloh.de/images/CarsharingSH-Hasloh-LOGO.jpeg"
style="
outline: none;
border: none;
text-decoration: none;
vertical-align: middle;
display: inline-block;
max-width: 100%;
"
/></a>
</div>
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
Moin {{.FirstName}} {{.LastName}} 👋,
</div>
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
herzlich willkommen beim Dörpsmobil Hasloh e.V.! Vielen Dank für
Ihre Registrierung und Ihre Unterstützung unseres Projekts.
</div>
<div style="padding: 16px 0px 16px 0px">
<hr
style="
width: 100%;
border: none;
border-top: 1px solid #cccccc;
margin: 0;
"
/>
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Um die Registrierung abschließen zu können bestätigen Sie bitte
noch Ihre Emailadresse indem Sie hier klicken:
</div>
<div style="text-align: center; padding: 16px 24px 16px 24px">
<a
href="https://carsharing-hasloh/backend/api/verify?token={{.Token}}"
style="
color: #ffffff;
font-size: 26px;
font-weight: bold;
background-color: #3e9bfc;
border-radius: 4px;
display: block;
padding: 16px 32px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 32px;
mso-font-width: -100%;
mso-text-raise: 48;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>E-Mail Adresse bestätigen</span
><span
><!--[if mso
]><i
style="letter-spacing: 32px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<div
style="
font-weight: normal;
text-align: center;
padding: 24px 24px 0px 24px;
"
>
Alternativ können Sie auch diesen Link in Ihrem Browser öffnen:
</div>
<div
style="
font-weight: bold;
text-align: center;
padding: 4px 24px 16px 24px;
"
>
https://carsharing-hasloh/backend/api/verify?token={{.Token}}
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Nachdem wir Ihre E-Mail Adresse bestätigen konnten, schicken wir
Ihnen alle weiteren Informationen zu. Wir freuen uns auf die
gemeinsame Zeit mit Ihnen!
</div>
<div style="padding: 16px 0px 16px 0px">
<hr
style="
width: 100%;
border: none;
border-top: 1px solid #cccccc;
margin: 0;
"
/>
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 80px">
Sollte es Probleme geben, möchten wir uns gerne jetzt schon
dafür entschuldigen, wenden Sie sich gerne an uns, wir werden
uns sofort darum kümmern, versprochen! Antworten Sie einfach auf
diese E-Mail.
</div>
<div style="padding: 16px 0px 16px 0px">
<hr
style="
width: 100%;
border: none;
border-top: 1px solid #cccccc;
margin: 0;
"
/>
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Mit Freundlichen Grüßen,
</div>
<div
style="
font-weight: bold;
text-align: left;
padding: 16px 24px 16px 24px;
"
>
Der Vorstand
</div>
<div style="padding: 16px 24px 16px 24px">
<img
alt=""
src="https://carsharing-hasloh.de/images/favicon_hu5543b2b337a87a169e2c722ef0122802_211442_96x0_resize_lanczos_3.png"
height="80"
width="80"
style="
outline: none;
border: none;
text-decoration: none;
object-fit: cover;
height: 80px;
width: 80px;
max-width: 100%;
display: inline-block;
vertical-align: middle;
text-align: center;
border-radius: 80px;
"
/>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>

View File

@@ -1,67 +1,288 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Willkommen beim Dörpsmobil Hasloh e.V.</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
color: #333;
<!doctype html>
<html>
<body>
<div
style="
background-color: #f2f5f7;
color: #242424;
font-family: &quot;Helvetica Neue&quot;, &quot;Arial Nova&quot;,
&quot;Nimbus Sans&quot;, Arial, sans-serif;
font-size: 16px;
font-weight: 400;
letter-spacing: 0.15008px;
line-height: 1.5;
margin: 0;
padding: 20px;
background-color: #f9f9f9;
}
.container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 8px;
}
h1 {
color: #007b5e;
}
p {
margin-bottom: 1em;
}
.contact-info {
margin-top: 20px;
}
.contact-info strong {
display: block;
margin-bottom: 5px;
}
a {
color: #007b5e;
padding: 32px 0;
min-height: 100%;
width: 100%;
"
>
<table
align="center"
width="100%"
style="margin: 0 auto; max-width: 600px; background-color: #ffffff"
role="presentation"
cellspacing="0"
cellpadding="0"
border="0"
>
<tbody>
<tr style="width: 100%">
<td>
<div style="padding: 24px 24px 24px 24px">
<a
href="https://marketbase.app"
style="text-decoration: none"
target="_blank"
><img
alt="Marketbase"
src="https://carsharing-hasloh.de/images/CarsharingSH-Hasloh-LOGO.jpeg"
style="
outline: none;
border: none;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="container">
<h1>Willkommen beim Dörpsmobil Hasloh e.V.</h1>
<p>Hallo {{.FirstName}},</p>
<p>herzlich willkommen beim Dörpsmobil Hasloh e.V.! Vielen Dank für Ihre Registrierung und Ihre Unterstützung unseres Projekts.</p>
<p>Wir freuen uns, Sie als Mitglied begrüßen zu dürfen und wünschen Ihnen stets eine sichere und angenehme Fahrt mit unseren Fahrzeugen.</p>
<p>Für weitere Fragen stehen wir Ihnen gerne zur Verfügung:</p>
<div class="contact-info">
<strong>Kontakt:</strong>
Name: Anke Freitag<br>
Vorsitzende Doerpsmobil-Hasloh e.V.<br>
E-Mail: <a href="mailto:info@doerpsmobil-hasloh.de">info@doerpsmobil-hasloh.de</a><br>
Telefon: +49 174 870 1392
vertical-align: middle;
display: inline-block;
max-width: 100%;
"
/></a>
</div>
<p>Besuchen Sie auch unsere Webseite unter <a href="https://carsharing-hasloh.de">https://carsharing-hasloh.de</a> für weitere Informationen.</p>
<p>Mit freundlichen Grüßen,<br>
Ihr Team von Dörpsmobil Hasloh e.V.
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
Moin {{.FirstName}} 👋,
</div>
<div style="font-weight: normal; padding: 0px 24px 16px 24px">
wir freuen uns sehr, dich als neues Mitglied bei Carsharing
Hasloh begrüßen zu dürfen! Herzlichen Glückwunsch zur
erfolgreichen E-Mail-Verifikation und willkommen in unserem
Verein!
</div>
<div
style="
font-size: 22px;
font-weight: normal;
text-align: center;
padding: 0px 24px 0px 24px;
"
>
Hier einige wichtige Informationen für dich:
</div>
<div style="font-weight: normal; padding: 16px 24px 0px 24px">
<ul>
<li>
<strong>Deine Mitgliedsnummer</strong>: {{.MembershipID}}
</li>
<li>
<strong>Dein gebuchtes Modell</strong>:
<ul>
<li><strong>Name</strong>: {{.MembershipModel}}</li>
<li><strong>Preis/Monat</strong>: {{.MembershipFee}}</li>
<li><strong>Preis/h</strong>: {{.RentalFee}}</li>
</ul>
</li>
<li>
<strong>Mitgliedsbeitrag</strong>: Solange wir noch kein
Fahrzeug im Betrieb haben, zahlst Du sinnvollerweise auch
keinen Mitgliedsbeitrag. Es ist zur Zeit der 1.1.2025 als
Startdatum geplant.
</li>
<li>
<strong>Führerscheinverifikation</strong>: Weitere
Informationen zur Verifikation deines Führerscheins folgen
in Kürze. Du musst nichts weiter tun, wir werden uns bei dir
melden, sobald es notwendig ist.
</li>
<li>
<strong>Moqo App</strong>: Wir werden die Moqo App nutzen,
um das Fahrzeug ausleihen zu können. Wenn Du schon mal einen
ersten Eindruck von dem Buchungsvorgang haben möchtest,
schaue Dir gerne dieses kurze Video an:
</li>
</ul>
</div>
<div style="text-align: center; padding: 0px 24px 16px 24px">
<a
href="https://www.youtube.com/shorts/ZMKUX0uyOps"
style="
color: #ffffff;
font-size: 16px;
font-weight: bold;
background-color: #f45050;
border-radius: 4px;
display: inline-block;
padding: 12px 20px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 20px;
mso-font-width: -100%;
mso-text-raise: 30;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>Moqo App Nutzung</span
><span
><!--[if mso
]><i
style="letter-spacing: 20px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<div style="font-weight: normal; padding: 16px 24px 0px 24px">
<ul>
<li>
<strong>Dörpsmobil</strong>: Wir sind nicht alleine sondern
Mitglied in einem Schleswig-Holstein weiten Netz an
gemeinnützigen Carsharing Anbietern. Für mehr Informationen
zu diesem Netzwerk haben wir auch ein Video vorbereitet:
</li>
</ul>
</div>
<div style="text-align: center; padding: 0px 24px 16px 24px">
<a
href="https://www.youtube.com/watch?v=NSch-2F-ru0"
style="
color: #ffffff;
font-size: 16px;
font-weight: bold;
background-color: #fd5a5a;
border-radius: 4px;
display: inline-block;
padding: 12px 20px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 20px;
mso-font-width: -100%;
mso-text-raise: 30;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>Dörpsmobil SH</span
><span
><!--[if mso
]><i
style="letter-spacing: 20px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<div style="padding: 16px 0px 16px 0px">
<hr
style="
width: 100%;
border: none;
border-top: 1px solid #cccccc;
margin: 0;
"
/>
</div>
<div
style="
font-weight: normal;
text-align: center;
padding: 16px 24px 16px 24px;
"
>
Für mehr Informationen besuche gerne unsere Webseite:
</div>
<div style="text-align: center; padding: 16px 24px 16px 24px">
<a
href="https://carsharing-hasloh.de"
style="
color: #ffffff;
font-size: 16px;
font-weight: bold;
background-color: #67a9ff;
border-radius: 64px;
display: inline-block;
padding: 16px 32px;
text-decoration: none;
"
target="_blank"
><span
><!--[if mso
]><i
style="
letter-spacing: 32px;
mso-font-width: -100%;
mso-text-raise: 48;
"
hidden
>&nbsp;</i
><!
[endif]--></span
><span>Carsharing-Hasloh.de</span
><span
><!--[if mso
]><i
style="letter-spacing: 32px; mso-font-width: -100%"
hidden
>&nbsp;</i
><!
[endif]--></span
></a
>
</div>
<div
style="
font-size: 15px;
font-weight: normal;
text-align: left;
padding: 16px 24px 16px 24px;
"
>
<p>
Solltest du Fragen haben oder Unterstützung benötigen, kannst
du dich jederzeit an unsere Vorsitzende wenden:
</p>
<ul>
<li>
<strong>Anke Freitag</strong>
<ul>
<li>
E-Mail:
<a
href="mailto:vorstand@carsharing-hasloh.de"
target="_blank"
>vorstand@carsharing-hasloh.de</a
>
</li>
<li>Telefon: +49 176 5013 4256</li>
</ul>
</li>
</ul>
</div>
</body>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
Wir danken dir herzlich für dein Vertrauen in uns und freuen uns
darauf, dich hoffentlich bald mit einem Auto begrüßen zu dürfen.
</div>
<div style="font-weight: normal; padding: 16px 24px 16px 24px">
<p>Mit freundlichen Grüßen,</p>
<p>Dein Carsharing Hasloh Team</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>