Add: CreateBackendAccess function

This commit is contained in:
Alex
2025-03-03 17:52:19 +01:00
parent 8f737282f2
commit c810e48451
9 changed files with 355 additions and 9 deletions

View File

@@ -187,6 +187,37 @@ export const actions = {
return fail(400, { errors: errors }); return fail(400, { errors: errors });
} }
const response = await res.json();
console.log('Server success response:', response);
throw redirect(303, `${base}/auth/admin/users`);
},
grantBackendAccess: async ({ request, fetch, cookies }) => {
let formData = await request.formData();
const rawData = formDataToObject(formData);
const processedData = processUserFormData(rawData);
console.dir(processedData);
const apiURL = `${BASE_API_URI}/auth/users/activate`;
/** @type {RequestInit} */
const requestOptions = {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Cookie: `jwt=${cookies.get('jwt')}`
},
body: JSON.stringify(processedData)
};
const res = await fetch(apiURL, requestOptions);
if (!res.ok) {
const response = await res.json();
const errors = formatError(response.errors);
return fail(400, { errors: errors });
}
const response = await res.json(); const response = await res.json();
console.log('Server success response:', response); console.log('Server success response:', response);
throw redirect(303, `${base}/auth/admin/users`); throw redirect(303, `${base}/auth/admin/users`);

View File

@@ -234,6 +234,41 @@
<th>{$t('user.id')}</th> <th>{$t('user.id')}</th>
<td>{user.id}</td> <td>{user.id}</td>
</tr> </tr>
<tr>
<td>
<form
method="POST"
action="?/grantBackendAccess"
use:enhance={() => {
return async ({ result }) => {
if (result.type === 'success' || result.type === 'redirect') {
await applyAction(result);
}
};
}}
on:submit|preventDefault={(/** @type {SubmitEvent} */ e) => {
if (
!confirm(
$t('dialog.backend_access', {
values: {
firstname: user.first_name || '',
lastname: user.last_name || ''
}
})
)
) {
e.preventDefault(); // Cancel form submission if user declines
}
}}
>
<input type="hidden" name="user[id]" value={user.id} />
<button class="button-dark" type="submit">
<i class="fas fa-unlock-keyhole"></i>
{$t('grant_backend_access')}
</button>
</form>
</td>
</tr>
<tr> <tr>
<th>{$t('name')}</th> <th>{$t('name')}</th>
<td>{user.first_name} {user.last_name}</td> <td>{user.first_name} {user.last_name}</td>
@@ -568,6 +603,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
flex-wrap: wrap; /* Allows wrapping on small screens */
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.container { .container {

View File

@@ -11,6 +11,7 @@ const (
AwaitingPaymentStatus AwaitingPaymentStatus
MailVerificationSubject = "Nur noch ein kleiner Schritt!" MailVerificationSubject = "Nur noch ein kleiner Schritt!"
MailChangePasswordSubject = "Passwort Änderung angefordert" MailChangePasswordSubject = "Passwort Änderung angefordert"
MailGrantBackendAccessSubject = "Dein Dörpsmobil Hasloh e.V. Zugang"
MailRegistrationSubject = "Neues Mitglied hat sich registriert" MailRegistrationSubject = "Neues Mitglied hat sich registriert"
MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V." MailWelcomeSubject = "Willkommen beim Dörpsmobil Hasloh e.V."
MailContactSubject = "Jemand hat das Kontaktformular gefunden" MailContactSubject = "Jemand hat das Kontaktformular gefunden"
@@ -62,15 +63,17 @@ var VerificationTypes = struct {
} }
var Priviliges = struct { var Priviliges = struct {
View int8 View int8
Create int8 Create int8
Update int8 Update int8
Delete int8 Delete int8
AccessControl int8
}{ }{
View: 2, View: 2,
Update: 4, Update: 4,
Create: 4, Create: 4,
Delete: 4, Delete: 4,
AccessControl: 8,
} }
var Roles = struct { var Roles = struct {

View File

@@ -4,6 +4,7 @@ import (
"GoMembership/internal/constants" "GoMembership/internal/constants"
"GoMembership/internal/utils" "GoMembership/internal/utils"
"GoMembership/pkg/errors" "GoMembership/pkg/errors"
"fmt"
"net/http" "net/http"
"strconv" "strconv"
@@ -12,6 +13,55 @@ import (
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
) )
func (uc *UserController) CreatePasswordHandler(c *gin.Context) {
requestUser, err := uc.ExtractUserFromContext(c)
if err != nil {
utils.RespondWithError(c, err, "Error extracting user from context in UpdateHandler", http.StatusBadRequest, errors.Responses.Fields.User, errors.Responses.Keys.NoAuthToken)
return
}
if !utils.HasPrivilige(requestUser, constants.Priviliges.AccessControl) {
utils.RespondWithError(c, errors.ErrNotAuthorized, fmt.Sprintf("Not allowed to handle all users. RoleID(%v)<Privilige(%v)", requestUser.RoleID, constants.Priviliges.View), http.StatusForbidden, errors.Responses.Fields.User, errors.Responses.Keys.Unauthorized)
return
}
//
// Expected data from the user
var input struct {
User struct {
ID uint `json:"id" binding:"required,numeric"`
} `json:"user"`
}
if err := c.ShouldBindJSON(&input); err != nil {
utils.HandleValidationError(c, err)
return
}
// find user
db_user, err := uc.Service.GetUserByID(input.User.ID)
if err != nil {
utils.RespondWithError(c, err, "couldn't get user by id", http.StatusNotFound, errors.Responses.Fields.User, errors.Responses.Keys.NotFound)
return
}
// create token
token, err := uc.Service.HandlePasswordChangeRequest(db_user)
if err != nil {
utils.RespondWithError(c, err, "couldn't handle password change request", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
// send email
if err := uc.EmailService.SendGrantBackendAccessEmail(db_user, &token); err != nil {
utils.RespondWithError(c, err, "Couldn't send grant backend access email", http.StatusInternalServerError, errors.Responses.Fields.General, errors.Responses.Keys.InternalServerError)
return
}
c.JSON(http.StatusAccepted, gin.H{
"message": "password_change_requested",
})
}
func (uc *UserController) RequestPasswordChangeHandler(c *gin.Context) { func (uc *UserController) RequestPasswordChangeHandler(c *gin.Context) {
// Expected data from the user // Expected data from the user

View File

@@ -141,7 +141,7 @@ func (uc *UserController) DeleteUser(c *gin.Context) {
type deleteData struct { type deleteData struct {
User struct { User struct {
ID uint `json:"id"` ID uint `json:"id" binding:"required,numeric"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
} `json:"user"` } `json:"user"`
} }

View File

@@ -25,6 +25,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
userRouter.POST("/users", userController.RegisterUser) userRouter.POST("/users", userController.RegisterUser)
userRouter.GET("/users", userController.GetAllUsers) userRouter.GET("/users", userController.GetAllUsers)
userRouter.DELETE("/users", userController.DeleteUser) userRouter.DELETE("/users", userController.DeleteUser)
userRouter.PATCH("/users/activate", userController.CreatePasswordHandler)
userRouter.GET("/subscriptions", membershipcontroller.GetSubscriptions) userRouter.GET("/subscriptions", membershipcontroller.GetSubscriptions)
userRouter.PUT("/subscriptions", membershipcontroller.UpdateHandler) userRouter.PUT("/subscriptions", membershipcontroller.UpdateHandler)
userRouter.POST("/subscriptions", membershipcontroller.RegisterSubscription) userRouter.POST("/subscriptions", membershipcontroller.RegisterSubscription)

View File

@@ -87,6 +87,37 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
return s.SendEmail(user.Email, subject, body, "", "") return s.SendEmail(user.Email, subject, body, "", "")
} }
func (s *EmailService) SendGrantBackendAccessEmail(user *models.User, token *string) error {
// Prepare data to be injected into the template
data := struct {
FirstName string
LastName string
Token string
BASEURL string
FRONTEND_PATH string
UserID uint
}{
FirstName: user.FirstName,
LastName: user.LastName,
Token: *token,
FRONTEND_PATH: config.Site.FrontendPath,
BASEURL: config.Site.BaseURL,
UserID: user.ID,
}
subject := constants.MailGrantBackendAccessSubject
htmlBody, err := ParseTemplate("mail_grant_backend_access.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't send grant backend access mail")
return err
}
plainBody, err := ParseTemplate("mail_grant_backend_access.txt.tmpl", data)
if err != nil {
logger.Error.Print("Couldn't parse password mail")
return err
}
return s.SendEmail(user.Email, subject, htmlBody, plainBody, "")
}
func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string) error { func (s *EmailService) SendChangePasswordEmail(user *models.User, token *string) error {
// Prepare data to be injected into the template // Prepare data to be injected into the template

View File

@@ -0,0 +1,177 @@
<!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="{{.BASEURL}}"
style="text-decoration: none"
target="_blank"
><img
alt="Dörpsmobil Hasloh"
src="{{.BASEURL}}/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">
hiermit erhältst Du Zugnag zu Deinem Dörpsmobil Hasloh Account.
</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; text-align: center; font-family: Arial, sans-serif; color: #333;">
<p style="margin-bottom: 16px;">
Mit diesem Link kannst Du Dich dann in Deinen <strong>Dörpsmobil Hasloh</strong> Account anmelden:
</p>
<p style="margin: 16px 0; font-size: 18px;">
<a href="{{.BASEURL}}{{.FRONTEND_PATH}}/"
style="display: inline-block; padding: 12px 20px; background-color: #007bff; color: white; text-decoration: none; font-weight: bold; border-radius: 5px;">
{{.BASEURL}}{{.FRONTEND_PATH}}/
</a>
</p>
<p style="margin-top: 16px;">
Dafür musst Du aber zunächst noch Dein Passwort erstellen.
</p>
</div>
<div style="text-align: center; padding: 16px 24px 16px 24px">
<a
href=" {{.BASEURL}}{{.FRONTEND_PATH}}/auth/password/change/{{.UserID}}?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>
Passwort erstellen
</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 kannst Du auch diesen Link in Deinem Browser öffnen:
</div>
<div
style="
font-weight: bold;
text-align: center;
padding: 4px 24px 16px 24px;
"
>
{{.BASEURL}}{{.FRONTEND_PATH}}/auth/password/change/{{.UserID}}?token={{.Token}}
</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="{{.BASEURL}}/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

@@ -0,0 +1,17 @@
Moin {{.FirstName}} {{.LastName}} 👋,
hiermit erhältst Du Zugnag zu Deinem Dörpsmobil Hasloh Account.
Mit diesem Link kannst Du Dich dann in Deinen Dörpsmobil Hasloh Account anmelden:
{{.BASEURL}}{{.FRONTEND_PATH}}/
einloggen.
Dafür musst Du aber zunächst noch Dein Passwort erstellen:
{{.BASEURL}}{{.FRONTEND_PATH}}/auth/password/change/{{.UserID}}?token={{.Token}}
Mit Freundlichen Grüßen,
Der Vorstand