mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-03-29 02:36:35 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2b3b7647d | ||
|
|
025378d14e | ||
|
|
e033ba6d45 | ||
|
|
e09562824a | ||
|
|
08f7fd16a9 | ||
|
|
be45eed125 |
@@ -23,6 +23,9 @@ jobs:
|
|||||||
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
username: ${{ secrets.DOCKER_REGISTRY_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
password: ${{ secrets.DOCKER_REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Download GeoLite2 City database
|
||||||
|
run: MAXMIND_LICENSE_KEY=${{ secrets.MAXMIND_LICENSE_KEY }} sh scripts/download-ip-database.sh
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v4
|
uses: docker/build-push-action@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,4 +34,5 @@ vite.config.ts.timestamp-*
|
|||||||
# Application specific
|
# Application specific
|
||||||
data
|
data
|
||||||
/frontend/tests/.auth
|
/frontend/tests/.auth
|
||||||
pocket-id-backend
|
pocket-id-backend
|
||||||
|
/backend/GeoLite2-City.mmdb
|
||||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -1,3 +1,28 @@
|
|||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.7.1...v) (2024-10-04)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add location based on ip to the audit log ([025378d](https://github.com/stonith404/pocket-id/commit/025378d14edd2d72da76e90799a0ccdd42cf672c))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.7.0...v) (2024-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* initials don't get displayed if Gravatar avatar doesn't exist ([e095628](https://github.com/stonith404/pocket-id/commit/e09562824a794bc7d240e9d229709d4b389db7d5))
|
||||||
|
|
||||||
|
## [](https://github.com/stonith404/pocket-id/compare/v0.6.0...v) (2024-10-03)
|
||||||
|
|
||||||
|
|
||||||
|
### ⚠ BREAKING CHANGES
|
||||||
|
|
||||||
|
* add ability to set light and dark mode logo
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* add ability to set light and dark mode logo ([be45eed](https://github.com/stonith404/pocket-id/commit/be45eed125e33e9930572660a034d5f12dc310ce))
|
||||||
|
|
||||||
## [](https://github.com/stonith404/pocket-id/compare/v0.5.3...v) (2024-10-02)
|
## [](https://github.com/stonith404/pocket-id/compare/v0.5.3...v) (2024-10-02)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ COPY --from=frontend-builder /app/frontend/package.json ./frontend/package.json
|
|||||||
|
|
||||||
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
|
COPY --from=backend-builder /app/backend/pocket-id-backend ./backend/pocket-id-backend
|
||||||
COPY --from=backend-builder /app/backend/migrations ./backend/migrations
|
COPY --from=backend-builder /app/backend/migrations ./backend/migrations
|
||||||
|
COPY --from=backend-builder /app/backend/GeoLite2-City.mmdb ./backend/GeoLite2-City.mmdb
|
||||||
COPY --from=backend-builder /app/backend/email-templates ./backend/email-templates
|
COPY --from=backend-builder /app/backend/email-templates ./backend/email-templates
|
||||||
COPY --from=backend-builder /app/backend/images ./backend/images
|
COPY --from=backend-builder /app/backend/images ./backend/images
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ Required tools:
|
|||||||
cd ..
|
cd ..
|
||||||
pm2 start pocket-id-backend --name pocket-id-backend
|
pm2 start pocket-id-backend --name pocket-id-backend
|
||||||
|
|
||||||
|
# Optional: Download the GeoLite2 city database.
|
||||||
|
# If not downloaded the ip location in the audit log will be empty.
|
||||||
|
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh
|
||||||
|
|
||||||
# Start the frontend
|
# Start the frontend
|
||||||
cd ../frontend
|
cd ../frontend
|
||||||
npm install
|
npm install
|
||||||
@@ -94,7 +98,6 @@ You may need the following information:
|
|||||||
- **Userinfo URL**: `https://<your-domain>/api/oidc/userinfo`
|
- **Userinfo URL**: `https://<your-domain>/api/oidc/userinfo`
|
||||||
- **Certificate URL**: `https://<your-domain>/.well-known/jwks.json`
|
- **Certificate URL**: `https://<your-domain>/.well-known/jwks.json`
|
||||||
- **OIDC Discovery URL**: `https://<your-domain>/.well-known/openid-configuration`
|
- **OIDC Discovery URL**: `https://<your-domain>/.well-known/openid-configuration`
|
||||||
- **PKCE**: `false` as this is not supported yet.
|
|
||||||
- **Scopes**: At least `openid email`. Optionally you can add `profile` and `groups`.
|
- **Scopes**: At least `openid email`. Optionally you can add `profile` and `groups`.
|
||||||
|
|
||||||
### Proxy Services with Pocket ID
|
### Proxy Services with Pocket ID
|
||||||
@@ -132,6 +135,9 @@ docker compose up -d
|
|||||||
cd ..
|
cd ..
|
||||||
pm2 start pocket-id-backend --name pocket-id-backend
|
pm2 start pocket-id-backend --name pocket-id-backend
|
||||||
|
|
||||||
|
# Optional: Update the GeoLite2 city database
|
||||||
|
MAXMIND_LICENSE_KEY=<your-key> sh scripts/download-ip-database.sh
|
||||||
|
|
||||||
# Start the frontend
|
# Start the frontend
|
||||||
cd ../frontend
|
cd ../frontend
|
||||||
npm install
|
npm install
|
||||||
|
|||||||
@@ -9,9 +9,15 @@
|
|||||||
<div class="content">
|
<div class="content">
|
||||||
<h2>New Sign-In Detected</h2>
|
<h2>New Sign-In Detected</h2>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
{{ if and .Data.City .Data.Country }}
|
||||||
|
<div>
|
||||||
|
<p class="label">Approximate Location</p>
|
||||||
|
<p>{{ .Data.City }}, {{ .Data.Country }}</p>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
<div>
|
<div>
|
||||||
<p class="label">IP Address</p>
|
<p class="label">IP Address</p>
|
||||||
<p>{{ .Data.IPAddress}}</p>
|
<p>{{ .Data.IPAddress }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="label">Device</p>
|
<p class="label">Device</p>
|
||||||
@@ -19,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p class="label">Sign-In Time</p>
|
<p class="label">Sign-In Time</p>
|
||||||
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}</p>
|
<p>{{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC" }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="message">
|
<p class="message">
|
||||||
@@ -27,4 +33,4 @@
|
|||||||
safely ignore this message. If not, please review your account and security settings.
|
safely ignore this message. If not, please review your account and security settings.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
@@ -2,6 +2,9 @@
|
|||||||
New Sign-In Detected
|
New Sign-In Detected
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
{{ if and .Data.City .Data.Country }}
|
||||||
|
Approximate Location: {{ .Data.City }}, {{ .Data.Country }}
|
||||||
|
{{ end }}
|
||||||
IP Address: {{ .Data.IPAddress }}
|
IP Address: {{ .Data.IPAddress }}
|
||||||
Device: {{ .Data.Device }}
|
Device: {{ .Data.Device }}
|
||||||
Time: {{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}
|
Time: {{ .Data.DateTime.Format "2006-01-02 15:04:05 UTC"}}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mileusna/useragent v1.3.4
|
github.com/mileusna/useragent v1.3.4
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1
|
||||||
golang.org/x/crypto v0.26.0
|
golang.org/x/crypto v0.26.0
|
||||||
golang.org/x/time v0.6.0
|
golang.org/x/time v0.6.0
|
||||||
gorm.io/driver/sqlite v1.5.6
|
gorm.io/driver/sqlite v1.5.6
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1 h1:UihPOz+oIJ5X0JsO7wEkL50fheCODsoZ9r86mJWfNMc=
|
||||||
|
github.com/oschwald/maxminddb-golang/v2 v2.0.0-beta.1/go.mod h1:vPpFrres6g9B5+meBwAd9xnp335KFcLEFW7EqJxBHy0=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
|||||||
3
backend/images/logoDark.svg
Normal file
3
backend/images/logoDark.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015">
|
||||||
|
<path fill="white" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 434 B |
3
backend/images/logoLight.svg
Normal file
3
backend/images/logoLight.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" id="a" viewBox="0 0 1015 1015">
|
||||||
|
<path fill="black" d="M506.6,0c209.52,0,379.98,170.45,379.98,379.96,0,82.33-25.9,160.68-74.91,226.54-48.04,64.59-113.78,111.51-190.13,135.71l-21.1,6.7-50.29-248.04,13.91-6.73c45.41-21.95,74.76-68.71,74.76-119.11,0-72.91-59.31-132.23-132.21-132.23s-132.23,59.32-132.23,132.23c0,50.4,29.36,97.16,74.77,119.11l13.65,6.61-81.01,499.24h-226.36V0h351.18Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 434 B |
@@ -5,24 +5,53 @@ import (
|
|||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// initApplicationImages copies the images from the images directory to the application-images directory
|
||||||
func initApplicationImages() {
|
func initApplicationImages() {
|
||||||
dirPath := common.EnvConfig.UploadPath + "/application-images"
|
dirPath := common.EnvConfig.UploadPath + "/application-images"
|
||||||
|
|
||||||
files, err := os.ReadDir(dirPath)
|
sourceFiles, err := os.ReadDir("./images")
|
||||||
if err != nil && !os.IsNotExist(err) {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
log.Fatalf("Error reading directory: %v", err)
|
log.Fatalf("Error reading directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip if files already exist
|
destinationFiles, err := os.ReadDir(dirPath)
|
||||||
if len(files) > 1 {
|
if err != nil && !os.IsNotExist(err) {
|
||||||
return
|
log.Fatalf("Error reading directory: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy files from source to destination
|
// Copy images from the images directory to the application-images directory if they don't already exist
|
||||||
err = utils.CopyDirectory("./images", dirPath)
|
for _, sourceFile := range sourceFiles {
|
||||||
if err != nil {
|
if sourceFile.IsDir() || imageAlreadyExists(sourceFile.Name(), destinationFiles) {
|
||||||
log.Fatalf("Error copying directory: %v", err)
|
continue
|
||||||
|
}
|
||||||
|
srcFilePath := "./images/" + sourceFile.Name()
|
||||||
|
destFilePath := dirPath + "/" + sourceFile.Name()
|
||||||
|
|
||||||
|
err := utils.CopyFile(srcFilePath, destFilePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Error copying file: %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageAlreadyExists(fileName string, destinationFiles []os.DirEntry) bool {
|
||||||
|
for _, destinationFile := range destinationFiles {
|
||||||
|
sourceFileWithoutExtension := getImageNameWithoutExtension(fileName)
|
||||||
|
destinationFileWithoutExtension := getImageNameWithoutExtension(destinationFile.Name())
|
||||||
|
|
||||||
|
if sourceFileWithoutExtension == destinationFileWithoutExtension {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImageNameWithoutExtension(fileName string) string {
|
||||||
|
splitted := strings.Split(fileName, ".")
|
||||||
|
return strings.Join(splitted[:len(splitted)-1], ".")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,8 +91,20 @@ func (acc *AppConfigController) updateAppConfigHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) getLogoHandler(c *gin.Context) {
|
func (acc *AppConfigController) getLogoHandler(c *gin.Context) {
|
||||||
imageType := acc.appConfigService.DbConfig.LogoImageType.Value
|
lightLogo := c.DefaultQuery("light", "true") == "true"
|
||||||
acc.getImage(c, "logo", imageType)
|
|
||||||
|
var imageName string
|
||||||
|
var imageType string
|
||||||
|
|
||||||
|
if lightLogo {
|
||||||
|
imageName = "logoLight"
|
||||||
|
imageType = acc.appConfigService.DbConfig.LogoLightImageType.Value
|
||||||
|
} else {
|
||||||
|
imageName = "logoDark"
|
||||||
|
imageType = acc.appConfigService.DbConfig.LogoDarkImageType.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.getImage(c, imageName, imageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) getFaviconHandler(c *gin.Context) {
|
func (acc *AppConfigController) getFaviconHandler(c *gin.Context) {
|
||||||
@@ -105,8 +117,20 @@ func (acc *AppConfigController) getBackgroundImageHandler(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
|
func (acc *AppConfigController) updateLogoHandler(c *gin.Context) {
|
||||||
imageType := acc.appConfigService.DbConfig.LogoImageType.Value
|
lightLogo := c.DefaultQuery("light", "true") == "true"
|
||||||
acc.updateImage(c, "logo", imageType)
|
|
||||||
|
var imageName string
|
||||||
|
var imageType string
|
||||||
|
|
||||||
|
if lightLogo {
|
||||||
|
imageName = "logoLight"
|
||||||
|
imageType = acc.appConfigService.DbConfig.LogoLightImageType.Value
|
||||||
|
} else {
|
||||||
|
imageName = "logoDark"
|
||||||
|
imageType = acc.appConfigService.DbConfig.LogoDarkImageType.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.updateImage(c, imageName, imageType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
|
func (acc *AppConfigController) updateFaviconHandler(c *gin.Context) {
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ type AuditLogDto struct {
|
|||||||
|
|
||||||
Event model.AuditLogEvent `json:"event"`
|
Event model.AuditLogEvent `json:"event"`
|
||||||
IpAddress string `json:"ipAddress"`
|
IpAddress string `json:"ipAddress"`
|
||||||
|
Country string `json:"country"`
|
||||||
|
City string `json:"city"`
|
||||||
Device string `json:"device"`
|
Device string `json:"device"`
|
||||||
UserID string `json:"userID"`
|
UserID string `json:"userID"`
|
||||||
Data model.AuditLogData `json:"data"`
|
Data model.AuditLogData `json:"data"`
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ type AppConfigVariable struct {
|
|||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
AppName AppConfigVariable
|
AppName AppConfigVariable
|
||||||
BackgroundImageType AppConfigVariable
|
BackgroundImageType AppConfigVariable
|
||||||
LogoImageType AppConfigVariable
|
LogoLightImageType AppConfigVariable
|
||||||
|
LogoDarkImageType AppConfigVariable
|
||||||
SessionDuration AppConfigVariable
|
SessionDuration AppConfigVariable
|
||||||
|
|
||||||
EmailEnabled AppConfigVariable
|
EmailEnabled AppConfigVariable
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ type AuditLog struct {
|
|||||||
|
|
||||||
Event AuditLogEvent
|
Event AuditLogEvent
|
||||||
IpAddress string
|
IpAddress string
|
||||||
|
Country string
|
||||||
|
City string
|
||||||
UserAgent string
|
UserAgent string
|
||||||
UserID string
|
UserID string
|
||||||
Data AuditLogData
|
Data AuditLogData
|
||||||
|
|||||||
@@ -47,8 +47,14 @@ var defaultDbConfig = model.AppConfig{
|
|||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
Value: "jpg",
|
Value: "jpg",
|
||||||
},
|
},
|
||||||
LogoImageType: model.AppConfigVariable{
|
LogoLightImageType: model.AppConfigVariable{
|
||||||
Key: "logoImageType",
|
Key: "logoLightImageType",
|
||||||
|
Type: "string",
|
||||||
|
IsInternal: true,
|
||||||
|
Value: "svg",
|
||||||
|
},
|
||||||
|
LogoDarkImageType: model.AppConfigVariable{
|
||||||
|
Key: "logoDarkImageType",
|
||||||
Type: "string",
|
Type: "string",
|
||||||
IsInternal: true,
|
IsInternal: true,
|
||||||
Value: "svg",
|
Value: "svg",
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
userAgentParser "github.com/mileusna/useragent"
|
userAgentParser "github.com/mileusna/useragent"
|
||||||
|
"github.com/oschwald/maxminddb-golang/v2"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/model"
|
"github.com/stonith404/pocket-id/backend/internal/model"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils"
|
"github.com/stonith404/pocket-id/backend/internal/utils"
|
||||||
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
"github.com/stonith404/pocket-id/backend/internal/utils/email"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"log"
|
"log"
|
||||||
|
"net/netip"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuditLogService struct {
|
type AuditLogService struct {
|
||||||
@@ -21,9 +23,16 @@ func NewAuditLogService(db *gorm.DB, appConfigService *AppConfigService, emailSe
|
|||||||
|
|
||||||
// Create creates a new audit log entry in the database
|
// Create creates a new audit log entry in the database
|
||||||
func (s *AuditLogService) Create(event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData) model.AuditLog {
|
func (s *AuditLogService) Create(event model.AuditLogEvent, ipAddress, userAgent, userID string, data model.AuditLogData) model.AuditLog {
|
||||||
|
country, city, err := s.GetIpLocation(ipAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get IP location: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
auditLog := model.AuditLog{
|
auditLog := model.AuditLog{
|
||||||
Event: event,
|
Event: event,
|
||||||
IpAddress: ipAddress,
|
IpAddress: ipAddress,
|
||||||
|
Country: country,
|
||||||
|
City: city,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Data: data,
|
Data: data,
|
||||||
@@ -61,6 +70,8 @@ func (s *AuditLogService) CreateNewSignInWithEmail(ipAddress, userAgent, userID
|
|||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
}, NewLoginTemplate, &NewLoginTemplateData{
|
}, NewLoginTemplate, &NewLoginTemplateData{
|
||||||
IPAddress: ipAddress,
|
IPAddress: ipAddress,
|
||||||
|
Country: createdAuditLog.Country,
|
||||||
|
City: createdAuditLog.City,
|
||||||
Device: s.DeviceStringFromUserAgent(userAgent),
|
Device: s.DeviceStringFromUserAgent(userAgent),
|
||||||
DateTime: createdAuditLog.CreatedAt.UTC(),
|
DateTime: createdAuditLog.CreatedAt.UTC(),
|
||||||
})
|
})
|
||||||
@@ -86,3 +97,29 @@ func (s *AuditLogService) DeviceStringFromUserAgent(userAgent string) string {
|
|||||||
ua := userAgentParser.Parse(userAgent)
|
ua := userAgentParser.Parse(userAgent)
|
||||||
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
return ua.Name + " on " + ua.OS + " " + ua.OSVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *AuditLogService) GetIpLocation(ipAddress string) (country, city string, err error) {
|
||||||
|
db, err := maxminddb.Open("GeoLite2-City.mmdb")
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
addr := netip.MustParseAddr(ipAddress)
|
||||||
|
|
||||||
|
var record struct {
|
||||||
|
City struct {
|
||||||
|
Names map[string]string `maxminddb:"names"`
|
||||||
|
} `maxminddb:"city"`
|
||||||
|
Country struct {
|
||||||
|
Names map[string]string `maxminddb:"names"`
|
||||||
|
} `maxminddb:"country"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.Lookup(addr).Decode(&record)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return record.Country.Names["en"], record.City.Names["en"], nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ var NewLoginTemplate = email.Template[NewLoginTemplateData]{
|
|||||||
|
|
||||||
type NewLoginTemplateData struct {
|
type NewLoginTemplateData struct {
|
||||||
IPAddress string
|
IPAddress string
|
||||||
|
Country string
|
||||||
|
City string
|
||||||
Device string
|
Device string
|
||||||
DateTime time.Time
|
DateTime time.Time
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func CopyDirectory(srcDir, destDir string) error {
|
|||||||
srcFilePath := filepath.Join(srcDir, file.Name())
|
srcFilePath := filepath.Join(srcDir, file.Name())
|
||||||
destFilePath := filepath.Join(destDir, file.Name())
|
destFilePath := filepath.Join(destDir, file.Name())
|
||||||
|
|
||||||
err := copyFile(srcFilePath, destFilePath)
|
err := CopyFile(srcFilePath, destFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ func CopyDirectory(srcDir, destDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFile(srcFilePath, destFilePath string) error {
|
func CopyFile(srcFilePath, destFilePath string) error {
|
||||||
srcFile, err := os.Open(srcFilePath)
|
srcFile, err := os.Open(srcFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE audit_logs DROP COLUMN country;
|
||||||
|
ALTER TABLE audit_logs DROP COLUMN city;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE audit_logs ADD COLUMN country TEXT;
|
||||||
|
ALTER TABLE audit_logs ADD COLUMN city TEXT;
|
||||||
@@ -97,16 +97,4 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff');
|
src: url('/fonts/PlayfairDisplay-Bold.woff') format('woff');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer components {
|
|
||||||
.application-images-grid {
|
|
||||||
@apply flex flex-wrap justify-between gap-x-5 gap-y-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1127px) {
|
|
||||||
.application-images-grid {
|
|
||||||
justify-content: flex-start;
|
|
||||||
@apply gap-x-20;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
let gravatarURL: string | undefined = $state();
|
let gravatarURL: string | undefined = $state();
|
||||||
if ($userStore) {
|
if ($userStore) {
|
||||||
createSHA256hash($userStore.email).then((email) => {
|
createSHA256hash($userStore.email).then((email) => {
|
||||||
gravatarURL = `https://www.gravatar.com/avatar/${email}`;
|
gravatarURL = `https://www.gravatar.com/avatar/${email}?d=404`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,10 @@
|
|||||||
<img class={$$restProps.class} src="/api/application-configuration/logo" alt="Logo" />
|
<script lang="ts">
|
||||||
|
import { mode } from 'mode-watcher';
|
||||||
|
import type { HTMLAttributes } from 'svelte/elements';
|
||||||
|
|
||||||
|
let { ...props }: HTMLAttributes<HTMLImageElement> = $props();
|
||||||
|
|
||||||
|
const isDarkMode = $derived($mode === 'dark');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<img {...props} src="/api/application-configuration/logo?light={!isDarkMode}" alt="Logo" />
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import type {
|
import type { AllAppConfig, AppConfigRawResponse } from '$lib/types/application-configuration';
|
||||||
AllAppConfig,
|
|
||||||
AppConfigRawResponse
|
|
||||||
} from '$lib/types/application-configuration';
|
|
||||||
import APIService from './api-service';
|
import APIService from './api-service';
|
||||||
|
|
||||||
export default class AppConfigService extends APIService {
|
export default class AppConfigService extends APIService {
|
||||||
@@ -33,11 +30,13 @@ export default class AppConfigService extends APIService {
|
|||||||
await this.api.put(`/application-configuration/favicon`, formData);
|
await this.api.put(`/application-configuration/favicon`, formData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateLogo(logo: File) {
|
async updateLogo(logo: File, light = true) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', logo!);
|
formData.append('file', logo!);
|
||||||
|
|
||||||
await this.api.put(`/application-configuration/logo`, formData);
|
await this.api.put(`/application-configuration/logo`, formData, {
|
||||||
|
params: { light }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateBackgroundImage(backgroundImage: File) {
|
async updateBackgroundImage(backgroundImage: File) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ export type AuditLog = {
|
|||||||
id: string;
|
id: string;
|
||||||
event: string;
|
event: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
|
country?: string;
|
||||||
|
city?: string;
|
||||||
device: string;
|
device: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
data: any;
|
data: any;
|
||||||
|
|||||||
@@ -28,17 +28,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateImages(
|
async function updateImages(
|
||||||
logo: File | null,
|
logoLight: File | null,
|
||||||
|
logoDark: File | null,
|
||||||
backgroundImage: File | null,
|
backgroundImage: File | null,
|
||||||
favicon: File | null
|
favicon: File | null
|
||||||
) {
|
) {
|
||||||
const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve();
|
const faviconPromise = favicon ? appConfigService.updateFavicon(favicon) : Promise.resolve();
|
||||||
const logoPromise = logo ? appConfigService.updateLogo(logo) : Promise.resolve();
|
const lightLogoPromise = logoLight ? appConfigService.updateLogo(logoLight, true) : Promise.resolve();
|
||||||
|
const darkLogoPromise = logoDark ? appConfigService.updateLogo(logoDark, false) : Promise.resolve();
|
||||||
const backgroundImagePromise = backgroundImage
|
const backgroundImagePromise = backgroundImage
|
||||||
? appConfigService.updateBackgroundImage(backgroundImage)
|
? appConfigService.updateBackgroundImage(backgroundImage)
|
||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
|
|
||||||
await Promise.all([logoPromise, backgroundImagePromise, faviconPromise])
|
await Promise.all([lightLogoPromise, darkLogoPromise, backgroundImagePromise, faviconPromise])
|
||||||
.then(() => toast.success('Images updated successfully'))
|
.then(() => toast.success('Images updated successfully'))
|
||||||
.catch(axiosErrorToast);
|
.catch(axiosErrorToast);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
image = $bindable(),
|
image = $bindable(),
|
||||||
imageURL,
|
imageURL,
|
||||||
accept = 'image/png, image/jpeg, image/svg+xml',
|
accept = 'image/png, image/jpeg, image/svg+xml',
|
||||||
|
forceColorScheme,
|
||||||
...restProps
|
...restProps
|
||||||
}: HTMLAttributes<HTMLDivElement> & {
|
}: HTMLAttributes<HTMLDivElement> & {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
label: string;
|
label: string;
|
||||||
image: File | null;
|
image: File | null;
|
||||||
imageURL: string;
|
imageURL: string;
|
||||||
|
forceColorScheme?: 'light' | 'dark';
|
||||||
accept?: string;
|
accept?: string;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
@@ -37,10 +39,16 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {...restProps}>
|
<div class="flex flex-col items-start md:flex-row md:items-center" {...restProps}>
|
||||||
<Label for={id}>{label}</Label>
|
<Label class="w-52" for={id}>{label}</Label>
|
||||||
<FileInput {id} variant="secondary" {accept} onchange={onImageChange}>
|
<FileInput {id} variant="secondary" {accept} onchange={onImageChange}>
|
||||||
<div class="bg-muted group relative flex items-center rounded">
|
<div
|
||||||
|
class="{forceColorScheme === 'light'
|
||||||
|
? 'bg-[#F1F1F5]'
|
||||||
|
: forceColorScheme === 'dark'
|
||||||
|
? 'bg-[#27272A]'
|
||||||
|
: 'bg-muted'} group relative flex items-center rounded"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
class={cn(
|
class={cn(
|
||||||
'h-full w-full rounded object-cover p-3 transition-opacity duration-200 group-hover:opacity-10',
|
'h-full w-full rounded object-cover p-3 transition-opacity duration-200 group-hover:opacity-10',
|
||||||
|
|||||||
@@ -5,15 +5,21 @@
|
|||||||
let {
|
let {
|
||||||
callback
|
callback
|
||||||
}: {
|
}: {
|
||||||
callback: (logo: File | null, backgroundImage: File | null, favicon: File | null) => void;
|
callback: (
|
||||||
|
logoLight: File | null,
|
||||||
|
logoDark: File | null,
|
||||||
|
backgroundImage: File | null,
|
||||||
|
favicon: File | null
|
||||||
|
) => void;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let logo = $state<File | null>(null);
|
let logoLight = $state<File | null>(null);
|
||||||
|
let logoDark = $state<File | null>(null);
|
||||||
let backgroundImage = $state<File | null>(null);
|
let backgroundImage = $state<File | null>(null);
|
||||||
let favicon = $state<File | null>(null);
|
let favicon = $state<File | null>(null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="application-images-grid">
|
<div class="flex flex-col gap-8">
|
||||||
<ApplicationImage
|
<ApplicationImage
|
||||||
id="favicon"
|
id="favicon"
|
||||||
imageClass="h-14 w-14 p-2"
|
imageClass="h-14 w-14 p-2"
|
||||||
@@ -23,15 +29,23 @@
|
|||||||
accept="image/x-icon"
|
accept="image/x-icon"
|
||||||
/>
|
/>
|
||||||
<ApplicationImage
|
<ApplicationImage
|
||||||
id="logo"
|
id="logo-light"
|
||||||
imageClass="h-32 w-32"
|
imageClass="h-32 w-32"
|
||||||
label="Logo"
|
label="Light Mode Logo"
|
||||||
bind:image={logo}
|
bind:image={logoLight}
|
||||||
imageURL="/api/application-configuration/logo"
|
imageURL="/api/application-configuration/logo?light=true"
|
||||||
|
forceColorScheme="light"
|
||||||
|
/>
|
||||||
|
<ApplicationImage
|
||||||
|
id="logo-dark"
|
||||||
|
imageClass="h-32 w-32"
|
||||||
|
label="Dark Mode Logo"
|
||||||
|
bind:image={logoDark}
|
||||||
|
imageURL="/api/application-configuration/logo?light=false"
|
||||||
|
forceColorScheme="dark"
|
||||||
/>
|
/>
|
||||||
<ApplicationImage
|
<ApplicationImage
|
||||||
id="background-image"
|
id="background-image"
|
||||||
class="basis-full lg:basis-auto"
|
|
||||||
imageClass="h-[350px] max-w-[500px]"
|
imageClass="h-[350px] max-w-[500px]"
|
||||||
label="Background Image"
|
label="Background Image"
|
||||||
bind:image={backgroundImage}
|
bind:image={backgroundImage}
|
||||||
@@ -39,5 +53,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<Button class="mt-5" onclick={() => callback(logo, backgroundImage, favicon)}>Save</Button>
|
<Button class="mt-5" onclick={() => callback(logoLight, logoDark, backgroundImage, favicon)}
|
||||||
|
>Save</Button
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
|
'Token URL': `https://${$page.url.hostname}/api/oidc/token`,
|
||||||
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
|
'Userinfo URL': `https://${$page.url.hostname}/api/oidc/userinfo`,
|
||||||
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
|
'Certificate URL': `https://${$page.url.hostname}/.well-known/jwks.json`,
|
||||||
PKCE: 'Disabled'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
async function updateClient(updatedClient: OidcClientCreateWithLogo) {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.Head>Time</Table.Head>
|
<Table.Head>Time</Table.Head>
|
||||||
<Table.Head>Event</Table.Head>
|
<Table.Head>Event</Table.Head>
|
||||||
|
<Table.Head>Approximate Location</Table.Head>
|
||||||
<Table.Head>IP Address</Table.Head>
|
<Table.Head>IP Address</Table.Head>
|
||||||
<Table.Head>Device</Table.Head>
|
<Table.Head>Device</Table.Head>
|
||||||
<Table.Head>Client</Table.Head>
|
<Table.Head>Client</Table.Head>
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<Badge variant="outline">{toFriendlyEventString(auditLog.event)}</Badge>
|
<Badge variant="outline">{toFriendlyEventString(auditLog.event)}</Badge>
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
|
<Table.Cell>{auditLog.city && auditLog.country ? `${auditLog.city}, ${auditLog.country}` : 'Unknown'}</Table.Cell>
|
||||||
<Table.Cell>{auditLog.ipAddress}</Table.Cell>
|
<Table.Cell>{auditLog.ipAddress}</Table.Cell>
|
||||||
<Table.Cell>{auditLog.device}</Table.Cell>
|
<Table.Cell>{auditLog.device}</Table.Cell>
|
||||||
<Table.Cell>{auditLog.data.clientName}</Table.Cell>
|
<Table.Cell>{auditLog.data.clientName}</Table.Cell>
|
||||||
|
|||||||
@@ -52,7 +52,8 @@ test('Update application images', async ({ page }) => {
|
|||||||
await page.goto('/settings/admin/application-configuration');
|
await page.goto('/settings/admin/application-configuration');
|
||||||
|
|
||||||
await page.getByLabel('Favicon').setInputFiles('tests/assets/w3-schools-favicon.ico');
|
await page.getByLabel('Favicon').setInputFiles('tests/assets/w3-schools-favicon.ico');
|
||||||
await page.getByLabel('Logo').setInputFiles('tests/assets/pingvin-share-logo.png');
|
await page.getByLabel('Light Mode Logo').setInputFiles('tests/assets/pingvin-share-logo.png');
|
||||||
|
await page.getByLabel('Dark Mode Logo').setInputFiles('tests/assets/nextcloud-logo.png');
|
||||||
await page.getByLabel('Background Image').setInputFiles('tests/assets/clouds.jpg');
|
await page.getByLabel('Background Image').setInputFiles('tests/assets/clouds.jpg');
|
||||||
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
await page.getByRole('button', { name: 'Save' }).nth(1).click();
|
||||||
|
|
||||||
@@ -62,9 +63,11 @@ test('Update application images', async ({ page }) => {
|
|||||||
.get('/api/application-configuration/favicon')
|
.get('/api/application-configuration/favicon')
|
||||||
.then((res) => expect.soft(res.status()).toBe(200));
|
.then((res) => expect.soft(res.status()).toBe(200));
|
||||||
await page.request
|
await page.request
|
||||||
.get('/api/application-configuration/logo')
|
.get('/api/application-configuration/logo?light=true')
|
||||||
|
.then((res) => expect.soft(res.status()).toBe(200));
|
||||||
|
await page.request
|
||||||
|
.get('/api/application-configuration/logo?light=false')
|
||||||
.then((res) => expect.soft(res.status()).toBe(200));
|
.then((res) => expect.soft(res.status()).toBe(200));
|
||||||
|
|
||||||
await page.request
|
await page.request
|
||||||
.get('/api/application-configuration/background-image')
|
.get('/api/application-configuration/background-image')
|
||||||
.then((res) => expect.soft(res.status()).toBe(200));
|
.then((res) => expect.soft(res.status()).toBe(200));
|
||||||
|
|||||||
31
scripts/download-ip-database.sh
Normal file
31
scripts/download-ip-database.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if the license key environment variable is set
|
||||||
|
if [ -z "$MAXMIND_LICENSE_KEY" ]; then
|
||||||
|
echo "Error: MAXMIND_LICENSE_KEY environment variable is not set."
|
||||||
|
echo "Please set it using 'export MAXMIND_LICENSE_KEY=your_license_key' and try again."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo $MAXMIND_LICENSE_KEY
|
||||||
|
# GeoLite2 City Database URL
|
||||||
|
URL="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz"
|
||||||
|
|
||||||
|
# Download directory
|
||||||
|
DOWNLOAD_DIR="./geolite2_db"
|
||||||
|
TARGET_PATH=./backend/GeoLite2-City.mmdb
|
||||||
|
mkdir -p $DOWNLOAD_DIR
|
||||||
|
|
||||||
|
# Download the database
|
||||||
|
echo "Downloading GeoLite2 City database..."
|
||||||
|
curl -L -o "$DOWNLOAD_DIR/GeoLite2-City.tar.gz" "$URL"
|
||||||
|
|
||||||
|
# Extract the downloaded file
|
||||||
|
echo "Extracting GeoLite2 City database..."
|
||||||
|
tar -xzf "$DOWNLOAD_DIR/GeoLite2-City.tar.gz" -C $DOWNLOAD_DIR --strip-components=1
|
||||||
|
|
||||||
|
mv "$DOWNLOAD_DIR/GeoLite2-City.mmdb" $TARGET_PATH
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
rm -rf "$DOWNLOAD_DIR"
|
||||||
|
|
||||||
|
echo "GeoLite2 City database downloaded and extracted to $TARGET_PATH"
|
||||||
Reference in New Issue
Block a user