Compare commits
	
		
			No commits in common. "635f2650e976cd2ee4040e49b7967476a984a4cb" and "578da90b5525cdf51b44d78e6cc410522b05d489" have entirely different histories.
		
	
	
		
			635f2650e9
			...
			578da90b55
		
	
		
							
								
								
									
										25
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								.drone.yml
									
									
									
									
									
								
							| @ -1,25 +0,0 @@ | |||||||
| kind: pipeline |  | ||||||
| name: test-docker-build |  | ||||||
| 
 |  | ||||||
| steps: |  | ||||||
|   - name: golangci-lint |  | ||||||
|     image: git.eeqj.de/sneak/drone-golangci-lint:2024-06-01 |  | ||||||
|   - name: test-docker-build |  | ||||||
|     image: plugins/docker |  | ||||||
|     network_mode: bridge |  | ||||||
|     settings: |  | ||||||
|       repo: sneak/directory |  | ||||||
|       dry_run: true |  | ||||||
|       tags: |  | ||||||
|         - ${DRONE_COMMIT_SHA} |  | ||||||
|         - ${DRONE_BRANCH} |  | ||||||
|   - name: notify |  | ||||||
|     image: plugins/slack |  | ||||||
|     secrets: [SLACK_WEBHOOK] |  | ||||||
|     settings: |  | ||||||
|       webhook: $SLACK_WEBHOOK |  | ||||||
|       link_names: true |  | ||||||
|     when: |  | ||||||
|       status: |  | ||||||
|         - success |  | ||||||
|         - failure |  | ||||||
							
								
								
									
										1
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								Makefile
									
									
									
									
									
								
							| @ -13,6 +13,7 @@ commit: fmt lint | |||||||
| 	git commit -a | 	git commit -a | ||||||
| 
 | 
 | ||||||
| fmt: | fmt: | ||||||
|  | 	gofumpt -l -w . | ||||||
| 	golangci-lint run --fix | 	golangci-lint run --fix | ||||||
| 
 | 
 | ||||||
| lint: | lint: | ||||||
|  | |||||||
| @ -1,45 +0,0 @@ | |||||||
| package assets |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"embed" |  | ||||||
| 	"net/http" |  | ||||||
| 
 |  | ||||||
| 	"github.com/CloudyKit/jet/v6" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| //go:embed templates/*.jet
 |  | ||||||
| var templatesFS embed.FS |  | ||||||
| 
 |  | ||||||
| //go:embed static/*
 |  | ||||||
| var staticFS embed.FS |  | ||||||
| 
 |  | ||||||
| var Views *jet.Set |  | ||||||
| 
 |  | ||||||
| func init() { |  | ||||||
| 	loader := jet.NewInMemLoader() |  | ||||||
| 
 |  | ||||||
| 	// Load templates from embed.FS
 |  | ||||||
| 	templateFiles, err := templatesFS.ReadDir("templates") |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic(err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	for _, tmpl := range templateFiles { |  | ||||||
| 		if tmpl.IsDir() { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 		tmplPath := "templates/" + tmpl.Name() |  | ||||||
| 		tmplContent, err := templatesFS.ReadFile(tmplPath) |  | ||||||
| 		if err != nil { |  | ||||||
| 			panic(err) |  | ||||||
| 		} |  | ||||||
| 		name := tmpl.Name() |  | ||||||
| 		loader.Set(name, string(tmplContent)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	Views = jet.NewSet(loader) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func StaticHandler() http.Handler { |  | ||||||
| 	return http.FileServer(http.FS(staticFS)) |  | ||||||
| } |  | ||||||
							
								
								
									
										1
									
								
								assets/static/tailwind.v2.2.19.min.css
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								assets/static/tailwind.v2.2.19.min.css
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -1,12 +0,0 @@ | |||||||
| {{ block flashes() }} |  | ||||||
| {{ if len(Flashes) > 0 }} |  | ||||||
| <div class="space-y-4 my-8 mx-auto max-w-2xl"> |  | ||||||
|     {{ range Flashes }} |  | ||||||
|     <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded shadow-lg relative" role="alert"> |  | ||||||
|         <span class="block sm:inline">{{ . }}</span> |  | ||||||
|     </div> |  | ||||||
|     {{ end }} |  | ||||||
| </div> |  | ||||||
| {{ end }} |  | ||||||
| {{ end }} |  | ||||||
| 
 |  | ||||||
| @ -1,8 +0,0 @@ | |||||||
| {{ block footer() }} |  | ||||||
| <footer> |  | ||||||
|     <p>Directory <a |  | ||||||
|     href="https://git.eeqj.de/sneak/directory/commit/{{version}}"><code>{{ |  | ||||||
|     version }}</code></a></p> |  | ||||||
| </footer> |  | ||||||
| {{ end }} |  | ||||||
| 
 |  | ||||||
| @ -1,12 +0,0 @@ | |||||||
| <!DOCTYPE html> |  | ||||||
| <html lang="en"> |  | ||||||
| <head> |  | ||||||
|     <meta charset="UTF-8"> |  | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> |  | ||||||
|     <title>{{ block title() }}Default Title{{ end }}</title> |  | ||||||
|     <link rel="stylesheet" href="/static/tailwind.v2.2.19.min.css"> |  | ||||||
| </head> |  | ||||||
| <body> |  | ||||||
|     {{ block body() . }}{{ end }} |  | ||||||
| </body> |  | ||||||
| </html> |  | ||||||
| @ -1,13 +0,0 @@ | |||||||
| {{ extends "pagelayout.jet" }} |  | ||||||
| 
 |  | ||||||
| {{ block title() }}The Directory{{ end }} |  | ||||||
| 
 |  | ||||||
| {{ block pageContent() }} |  | ||||||
| <h2>Directory</h2> |  | ||||||
| 
 |  | ||||||
| <h3>Tags</h3> |  | ||||||
| <ul> |  | ||||||
|     <li><a href="/tags/tag1">Tag1</a></li> |  | ||||||
| </ul> |  | ||||||
| 
 |  | ||||||
| {{ end }} |  | ||||||
| @ -1,29 +0,0 @@ | |||||||
| {{ extends "pagelayout.jet" }} |  | ||||||
| 
 |  | ||||||
| {{ block pageContent() }} |  | ||||||
| <div class="flex items-center justify-center min-h-screen bg-gray-100"> |  | ||||||
|     <div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-md mx-4"> |  | ||||||
|         <h2 class="text-2xl font-bold mb-6 text-center">Login</h2> |  | ||||||
|         <form action="/login" method="post"> |  | ||||||
|             <div class="mb-4"> |  | ||||||
|                 <label for="username" class="block text-gray-700">Username:</label> |  | ||||||
|                 <input type="text" id="username" name="username" required class="mt-1 w-full p-2 border border-gray-300 rounded"> |  | ||||||
|             </div> |  | ||||||
|             <div class="mb-4"> |  | ||||||
|                 <label for="password" class="block text-gray-700">Password:</label> |  | ||||||
|                 <input type="password" id="password" name="password" required class="mt-1 w-full p-2 border border-gray-300 rounded"> |  | ||||||
|             </div> |  | ||||||
|             <div class="flex items-center justify-center"> |  | ||||||
|                 <input type="submit" value="Login" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer"> |  | ||||||
|             </div> |  | ||||||
|         </form> |  | ||||||
|     </div> |  | ||||||
| </div> |  | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
|     .min-h-screen { |  | ||||||
|         min-height: calc(100vh - 4rem); /* Adjust based on navbar height */ |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
| {{ end }} |  | ||||||
| 
 |  | ||||||
| @ -1,39 +0,0 @@ | |||||||
| {{ block navbar() }} |  | ||||||
| <nav class="bg-gray-100 border-b border-gray-300 fixed top-0 left-0 right-0 w-full z-50"> |  | ||||||
|     <div class="container mx-auto px-4 flex justify-between items-center h-16"> |  | ||||||
|         <ul class="flex space-x-4"> |  | ||||||
|             <li><a href="/" class="text-black hover:text-blue-500">Home</a></li> |  | ||||||
|             <li><a href="/status" class="text-black hover:text-blue-500">Status</a></li> |  | ||||||
|         </ul> |  | ||||||
|         <div class="relative"> |  | ||||||
|             <input type="checkbox" id="dropdown-toggle" class="hidden"> |  | ||||||
|             <label for="dropdown-toggle" class="cursor-pointer p-2 border border-gray-300 rounded bg-white">☰</label> |  | ||||||
|             <ul class="absolute right-0 mt-2 w-48 bg-white border border-gray-300 rounded shadow-lg hidden"> |  | ||||||
|                 {{ if isset("LoggedInUser") && LoggedInUser != nil }} |  | ||||||
|                     <li><a href="/users/{{LoggedInUser.ID}}" class="block px-4 py-2 text-black hover:bg-gray-100">Profile</a></li> |  | ||||||
|                     <li><a href="/logout" class="block px-4 py-2 text-black hover:bg-gray-100">Logout</a></li> |  | ||||||
|                     {{ if LoggedInUser.IsSuperAdmin() }} |  | ||||||
|                         <li><a href="/users/new" class="block px-4 py-2 text-black hover:bg-gray-100">Create User</a></li> |  | ||||||
|                     {{ end }} |  | ||||||
|                     <li class="block px-4 py-2 bg-gray-100 border-t border-gray-300">Welcome,  |  | ||||||
|                         <a href="/users/{{LoggedInUser.ID}}"><code>{{ LoggedInUser.Username }}</code></a> |  | ||||||
|                     </li> |  | ||||||
|                 {{ else }} |  | ||||||
|                     <li><a href="/login" class="block px-4 py-2 text-black hover:bg-gray-100">Login</a></li> |  | ||||||
|                 {{ end }} |  | ||||||
|             </ul> |  | ||||||
|         </div> |  | ||||||
|     </div> |  | ||||||
| </nav> |  | ||||||
| 
 |  | ||||||
| <style> |  | ||||||
|     body, html { |  | ||||||
|         margin: 0; |  | ||||||
|         padding: 0; |  | ||||||
|     } |  | ||||||
|     #dropdown-toggle:checked + label + ul { |  | ||||||
|         display: block; |  | ||||||
|     } |  | ||||||
| </style> |  | ||||||
| {{ end }} |  | ||||||
| 
 |  | ||||||
| @ -1,25 +0,0 @@ | |||||||
| {{ extends "html.jet" }} |  | ||||||
| 
 |  | ||||||
| {{ block body() . }} |  | ||||||
|     <!-- pagelayout.jet --> |  | ||||||
|     {{ include "navbar.jet" . }} |  | ||||||
|     <div class="content"> |  | ||||||
|     {{ include "flashes.jet" . }} |  | ||||||
|     {{ block pageContent() . }}default page content{{ end }} |  | ||||||
|     </div> |  | ||||||
|     {{ include "footer.jet" . }} |  | ||||||
|     <style> |  | ||||||
|         body, html { |  | ||||||
|             margin: 0; |  | ||||||
|             padding: 0; |  | ||||||
|         } |  | ||||||
|         .content { |  | ||||||
|             margin-top: 4rem; /* Adjust this value based on your navbar height */ |  | ||||||
|             padding: 1em 0; /* Only top and bottom padding */ |  | ||||||
|             background-color: #f8f9fa; /* Ensure background color spans full width */ |  | ||||||
|             width: 100%; /* Ensure the content spans full width */ |  | ||||||
|             box-sizing: border-box; /* Include padding in the element's total width and height */ |  | ||||||
|         } |  | ||||||
|     </style> |  | ||||||
| {{ end }} |  | ||||||
| 
 |  | ||||||
							
								
								
									
										8
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								go.mod
									
									
									
									
									
								
							| @ -19,17 +19,12 @@ require ( | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect |  | ||||||
| 	github.com/CloudyKit/jet/v6 v6.2.0 // indirect |  | ||||||
| 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect | 	github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect | ||||||
| 	github.com/beorn7/perks v1.0.1 // indirect | 	github.com/beorn7/perks v1.0.1 // indirect | ||||||
| 	github.com/cespare/xxhash/v2 v2.3.0 // indirect | 	github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||||||
| 	github.com/fsnotify/fsnotify v1.7.0 // indirect | 	github.com/fsnotify/fsnotify v1.7.0 // indirect | ||||||
| 	github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect | 	github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect | ||||||
| 	github.com/hashicorp/hcl v1.0.0 // indirect | 	github.com/hashicorp/hcl v1.0.0 // indirect | ||||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect |  | ||||||
| 	github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect |  | ||||||
| 	github.com/jackc/pgx/v5 v5.4.3 // indirect |  | ||||||
| 	github.com/jinzhu/inflection v1.0.0 // indirect | 	github.com/jinzhu/inflection v1.0.0 // indirect | ||||||
| 	github.com/jinzhu/now v1.1.5 // indirect | 	github.com/jinzhu/now v1.1.5 // indirect | ||||||
| 	github.com/magiconair/properties v1.8.7 // indirect | 	github.com/magiconair/properties v1.8.7 // indirect | ||||||
| @ -37,7 +32,6 @@ require ( | |||||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||||
| 	github.com/mattn/go-sqlite3 v1.14.17 // indirect | 	github.com/mattn/go-sqlite3 v1.14.17 // indirect | ||||||
| 	github.com/mitchellh/mapstructure v1.5.0 // indirect | 	github.com/mitchellh/mapstructure v1.5.0 // indirect | ||||||
| 	github.com/oklog/ulid/v2 v2.1.0 // indirect |  | ||||||
| 	github.com/pelletier/go-toml/v2 v2.2.1 // indirect | 	github.com/pelletier/go-toml/v2 v2.2.1 // indirect | ||||||
| 	github.com/prometheus/client_model v0.6.1 // indirect | 	github.com/prometheus/client_model v0.6.1 // indirect | ||||||
| 	github.com/prometheus/common v0.53.0 // indirect | 	github.com/prometheus/common v0.53.0 // indirect | ||||||
| @ -52,12 +46,10 @@ require ( | |||||||
| 	go.uber.org/dig v1.17.1 // indirect | 	go.uber.org/dig v1.17.1 // indirect | ||||||
| 	go.uber.org/multierr v1.10.0 // indirect | 	go.uber.org/multierr v1.10.0 // indirect | ||||||
| 	go.uber.org/zap v1.26.0 // indirect | 	go.uber.org/zap v1.26.0 // indirect | ||||||
| 	golang.org/x/crypto v0.22.0 // indirect |  | ||||||
| 	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect | 	golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect | ||||||
| 	golang.org/x/sys v0.19.0 // indirect | 	golang.org/x/sys v0.19.0 // indirect | ||||||
| 	golang.org/x/text v0.14.0 // indirect | 	golang.org/x/text v0.14.0 // indirect | ||||||
| 	google.golang.org/protobuf v1.33.0 // indirect | 	google.golang.org/protobuf v1.33.0 // indirect | ||||||
| 	gopkg.in/ini.v1 v1.67.0 // indirect | 	gopkg.in/ini.v1 v1.67.0 // indirect | ||||||
| 	gopkg.in/yaml.v3 v3.0.1 // indirect | 	gopkg.in/yaml.v3 v3.0.1 // indirect | ||||||
| 	gorm.io/driver/postgres v1.5.7 // indirect |  | ||||||
| ) | ) | ||||||
|  | |||||||
							
								
								
									
										19
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								go.sum
									
									
									
									
									
								
							| @ -1,9 +1,5 @@ | |||||||
| github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8 h1:nMpu1t4amK3vJWBibQ5X/Nv0aXL+b69TQf2uK5PH7Go= | github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8 h1:nMpu1t4amK3vJWBibQ5X/Nv0aXL+b69TQf2uK5PH7Go= | ||||||
| github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= | github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs= | ||||||
| github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= |  | ||||||
| github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= |  | ||||||
| github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= |  | ||||||
| github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= |  | ||||||
| github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= | ||||||
| github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= | github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= | ||||||
| github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= | ||||||
| @ -36,12 +32,6 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsD | |||||||
| github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= | github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= | ||||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||||||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||||||
| github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= |  | ||||||
| github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= |  | ||||||
| github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= |  | ||||||
| github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= |  | ||||||
| github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= |  | ||||||
| github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= |  | ||||||
| github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= | ||||||
| github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= | ||||||
| github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= | ||||||
| @ -64,9 +54,6 @@ github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6 | |||||||
| github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= | github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= | ||||||
| github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= | ||||||
| github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= | ||||||
| github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= |  | ||||||
| github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= |  | ||||||
| github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= |  | ||||||
| github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= | github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= | ||||||
| github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= | github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= | ||||||
| github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= | github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= | ||||||
| @ -110,8 +97,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS | |||||||
| github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
| github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= | ||||||
| github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= | ||||||
| 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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
| github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= | ||||||
| github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= | ||||||
| @ -129,8 +114,6 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= | |||||||
| go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | ||||||
| go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= | go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= | ||||||
| go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= | go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= | ||||||
| golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= |  | ||||||
| golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= |  | ||||||
| golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= | golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= | ||||||
| golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= | golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= | ||||||
| golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| @ -150,8 +133,6 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | |||||||
| gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | ||||||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | ||||||
| gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= |  | ||||||
| gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= |  | ||||||
| gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= | gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= | ||||||
| gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= | gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= | ||||||
| gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= | gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= | ||||||
|  | |||||||
| @ -26,7 +26,7 @@ type ConfigParams struct { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type Config struct { | type Config struct { | ||||||
| 	DBDSN            string | 	DBURL            string | ||||||
| 	Debug            bool | 	Debug            bool | ||||||
| 	MaintenanceMode  bool | 	MaintenanceMode  bool | ||||||
| 	DevelopmentMode  bool | 	DevelopmentMode  bool | ||||||
| @ -59,7 +59,7 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) { | |||||||
| 	viper.SetDefault("DEV_ADMIN_USERNAME", "") | 	viper.SetDefault("DEV_ADMIN_USERNAME", "") | ||||||
| 	viper.SetDefault("DEV_ADMIN_PASSWORD", "") | 	viper.SetDefault("DEV_ADMIN_PASSWORD", "") | ||||||
| 	viper.SetDefault("PORT", "8080") | 	viper.SetDefault("PORT", "8080") | ||||||
| 	viper.SetDefault("DB_DSN", "") | 	viper.SetDefault("DBURL", "") | ||||||
| 	viper.SetDefault("SENTRY_DSN", "") | 	viper.SetDefault("SENTRY_DSN", "") | ||||||
| 	viper.SetDefault("METRICS_USERNAME", "") | 	viper.SetDefault("METRICS_USERNAME", "") | ||||||
| 	viper.SetDefault("METRICS_PASSWORD", "") | 	viper.SetDefault("METRICS_PASSWORD", "") | ||||||
| @ -76,7 +76,7 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	s := &Config{ | 	s := &Config{ | ||||||
| 		DBDSN:            viper.GetString("DB_DSN"), | 		DBURL:            viper.GetString("DBURL"), | ||||||
| 		Debug:            viper.GetBool("debug"), | 		Debug:            viper.GetBool("debug"), | ||||||
| 		Port:             viper.GetInt("PORT"), | 		Port:             viper.GetInt("PORT"), | ||||||
| 		SentryDSN:        viper.GetString("SENTRY_DSN"), | 		SentryDSN:        viper.GetString("SENTRY_DSN"), | ||||||
|  | |||||||
| @ -2,17 +2,27 @@ package database | |||||||
| 
 | 
 | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
|  | 	"os" | ||||||
|  | 
 | ||||||
| 	"database/sql" | 	"database/sql" | ||||||
| 	"errors" |  | ||||||
| 
 | 
 | ||||||
| 	"github.com/rs/zerolog" | 	"github.com/rs/zerolog" | ||||||
|  | 	"github.com/rs/zerolog/log" | ||||||
| 	"go.uber.org/fx" | 	"go.uber.org/fx" | ||||||
| 	"gorm.io/driver/postgres" | 	"gorm.io/driver/sqlite" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| 	"sneak.berlin/go/directory/internal/config" | 	"sneak.berlin/go/directory/internal/config" | ||||||
| 	"sneak.berlin/go/directory/internal/globals" | 	"sneak.berlin/go/directory/internal/globals" | ||||||
| 	"sneak.berlin/go/directory/internal/logger" | 	"sneak.berlin/go/directory/internal/logger" | ||||||
|  | 	"sneak.berlin/go/util" | ||||||
| 
 | 
 | ||||||
|  | 	// spooky action at a distance!
 | ||||||
|  | 	// this populates the environment
 | ||||||
|  | 	// from a ./.env file automatically
 | ||||||
|  | 	// for development configuration.
 | ||||||
|  | 	// .env contents should be things like
 | ||||||
|  | 	// `DBURL=postgres://user:pass@.../`
 | ||||||
|  | 	// (without the backticks, of course)
 | ||||||
| 	_ "github.com/joho/godotenv/autoload" | 	_ "github.com/joho/godotenv/autoload" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -29,6 +39,8 @@ type Database struct { | |||||||
| 	params *DatabaseParams | 	params *DatabaseParams | ||||||
| 	DB     *gorm.DB | 	DB     *gorm.DB | ||||||
| 	SQLDB  *sql.DB | 	SQLDB  *sql.DB | ||||||
|  | 	dbdir  string | ||||||
|  | 	dbfn   string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) { | func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) { | ||||||
| @ -41,21 +53,11 @@ func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) { | |||||||
| 	lc.Append(fx.Hook{ | 	lc.Append(fx.Hook{ | ||||||
| 		OnStart: func(ctx context.Context) error { | 		OnStart: func(ctx context.Context) error { | ||||||
| 			s.log.Info().Msg("Database OnStart Hook") | 			s.log.Info().Msg("Database OnStart Hook") | ||||||
| 			err := s.Connect() | 			// FIXME connect to db
 | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			return nil | 			return nil | ||||||
| 		}, | 		}, | ||||||
| 		OnStop: func(ctx context.Context) error { | 		OnStop: func(ctx context.Context) error { | ||||||
| 			if s.SQLDB != nil { | 			// FIXME disconnect from db
 | ||||||
| 				err := s.SQLDB.Close() |  | ||||||
| 				if err != nil { |  | ||||||
| 					s.log.Error().Err(err).Msg("failed to close database connection") |  | ||||||
| 					return err |  | ||||||
| 				} |  | ||||||
| 			} |  | ||||||
| 			s.log.Info().Msg("Database connection closed") |  | ||||||
| 			return nil | 			return nil | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
| @ -78,49 +80,36 @@ func (d *Database) Get() (*gorm.DB, error) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (d *Database) Connect() error { | func (d *Database) Connect() error { | ||||||
| 	dsn := d.params.Config.DatabaseDSN | 	// FIXME make this get the data dir path from config
 | ||||||
| 	if dsn == "" { | 	d.dbdir = os.Getenv("HOME") | ||||||
| 		err := errors.New("database DSN is empty") | 	err := util.Mkdirp(d.dbdir) | ||||||
| 		d.log.Error().Err(err).Msg("failed to get database DSN from config") | 	if err != nil { | ||||||
|  | 		d.log.Error(). | ||||||
|  | 			Err(err). | ||||||
|  | 			Str("dbdir", d.dbdir). | ||||||
|  | 			Msg("unable to create directory") | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 
 | 	d.dbfn = d.dbdir + "/" + d.params.Globals.Appname + ".db" | ||||||
| 	d.log.Info(). | 	d.log.Info(). | ||||||
| 		Str("dsn", dsn). | 		Str("file", d.dbfn). | ||||||
| 		Msg("connecting to database") | 		Msg("opening store db") | ||||||
| 
 | 
 | ||||||
| 	sqlDB, err := sql.Open("postgres", dsn) | 	// Open the database using the pure Go SQLite driver
 | ||||||
|  | 	sqlDB, err := sql.Open("sqlite", d.dbfn+"?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		d.log.Error().Err(err).Msg("failed to open database") | 		d.log.Error().Err(err).Msg("failed to open database") | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	d.SQLDB = sqlDB | 	d.SQLDB = sqlDB | ||||||
| 
 | 
 | ||||||
| 	// Ping the database to ensure the connection is valid
 | 	// Use the generic Gorm SQL driver to integrate it
 | ||||||
| 	err = d.Ping() | 	db, err := gorm.Open(sqlite.Dialector{Conn: d.SQLDB}, &gorm.Config{}) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		d.log.Error().Err(err).Msg("failed to ping database") | 		log.Error().Err(err).Msg("failed to connect database") | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	db, err := gorm.Open(postgres.New(postgres.Config{ |  | ||||||
| 		Conn: d.SQLDB, |  | ||||||
| 	}), &gorm.Config{}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		d.log.Error().Err(err).Msg("failed to connect database") |  | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	d.DB = db | 	d.DB = db | ||||||
| 
 | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 |  | ||||||
| func (d *Database) Ping() error { |  | ||||||
| 	err := d.SQLDB.Ping() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	d.log.Info().Msg("successfully pinged the database") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
|  | |||||||
| @ -1,63 +0,0 @@ | |||||||
| package store |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/rs/zerolog" |  | ||||||
| 	"github.com/rs/zerolog/log" |  | ||||||
| 	"go.uber.org/fx" |  | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"sneak.berlin/go/directory/internal/config" |  | ||||||
| 	"sneak.berlin/go/directory/internal/database" |  | ||||||
| 	"sneak.berlin/go/directory/internal/logger" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	UsersTableName          string = "users" |  | ||||||
| 	UserAttributesTableName string = "userattributes" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type StoreParams struct { |  | ||||||
| 	fx.In |  | ||||||
| 	Logger   *logger.Logger |  | ||||||
| 	Config   *config.Config |  | ||||||
| 	Database *database.Database |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| type Store struct { |  | ||||||
| 	db     *gorm.DB |  | ||||||
| 	logger *logger.Logger |  | ||||||
| 	log    *zerolog.Logger |  | ||||||
| 	params *StoreParams |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func New(lc fx.Lifecycle, params *StoreParams) (*Store, error) { |  | ||||||
| 	s := new(Store) |  | ||||||
| 
 |  | ||||||
| 	s.logger = params.Logger |  | ||||||
| 	s.log = params.Logger.Get() |  | ||||||
| 
 |  | ||||||
| 	var err error |  | ||||||
| 	s.db, err = params.Database.Get() |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Err(err).Msg("failed to get database object") |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Auto migrate the schema
 |  | ||||||
| 	err = s.db.AutoMigrate(&User{}, &UserAttribute{}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Err(err).Msg("failed to migrate database schema") |  | ||||||
| 		return nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return s, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Store) Close() { |  | ||||||
| 	log.Info().Msg("closing store db") |  | ||||||
| 	db, err := s.db.DB() |  | ||||||
| 	if err != nil { |  | ||||||
| 		log.Error().Err(err).Msg("failed to get generic database object from gorm") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
| 	db.Close() |  | ||||||
| } |  | ||||||
| @ -1,206 +0,0 @@ | |||||||
| package store |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"crypto/rand" |  | ||||||
| 	"errors" |  | ||||||
| 	"net/http" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/oklog/ulid/v2" |  | ||||||
| 	"github.com/rs/zerolog/log" |  | ||||||
| 	"golang.org/x/crypto/bcrypt" |  | ||||||
| 	"gorm.io/gorm" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type User struct { |  | ||||||
| 	ID                    string `gorm:"primaryKey"` |  | ||||||
| 	Username              string `gorm:"unique"` |  | ||||||
| 	Email                 *string |  | ||||||
| 	PasswordHash          string |  | ||||||
| 	CreatedAt             *time.Time |  | ||||||
| 	LastLogin             *time.Time |  | ||||||
| 	LastSeen              *time.Time |  | ||||||
| 	LastFailedAuth        *time.Time |  | ||||||
| 	RecentFailedAuthCount int |  | ||||||
| 	LastSeenIP            *string |  | ||||||
| 	Disabled              bool |  | ||||||
| 	Attributes            []UserAttribute `gorm:"foreignKey:UserID"` |  | ||||||
| 	db                    *gorm.DB        `gorm:"-"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (User) TableName() string { |  | ||||||
| 	return UsersTableName |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) IsDisabled() bool { |  | ||||||
| 	return u.Disabled |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Store) AddUser(username, password string) (*User, error) { |  | ||||||
| 	var hashedPassword string |  | ||||||
| 	if password != "" { |  | ||||||
| 		hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) |  | ||||||
| 		if err != nil { |  | ||||||
| 			log.Error().Err(err).Msg("unable to hash password") |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
| 		hashedPassword = string(hashedPasswordBytes) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	userID := ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader).String() |  | ||||||
| 	createdAt := time.Now() |  | ||||||
| 	user := &User{ |  | ||||||
| 		ID:           userID, |  | ||||||
| 		Username:     username, |  | ||||||
| 		PasswordHash: hashedPassword, |  | ||||||
| 		CreatedAt:    &createdAt, |  | ||||||
| 		db:           s.db, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	result := s.db.Create(user) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error(). |  | ||||||
| 			Str("username", username). |  | ||||||
| 			Err(result.Error). |  | ||||||
| 			Msg("unable to insert user into db") |  | ||||||
| 		return nil, result.Error |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	log.Debug(). |  | ||||||
| 		Str("username", username). |  | ||||||
| 		Msg("user added to db") |  | ||||||
| 	return user, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Store) FindUserByUsername(username string) (*User, error) { |  | ||||||
| 	if len(username) < 3 { |  | ||||||
| 		return nil, errors.New("usernames are at least 3 characters") |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var user User |  | ||||||
| 	result := s.db.Where("username = ? AND disabled = ?", username, false).First(&user) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error().Err(result.Error).Msg("unable to find user") |  | ||||||
| 		return nil, result.Error |  | ||||||
| 	} |  | ||||||
| 	user.db = s.db |  | ||||||
| 	return &user, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Store) FindUserByID(userID string) (*User, error) { |  | ||||||
| 	var user User |  | ||||||
| 	result := s.db.Where("id = ? and disabled = ?", userID, false).First(&user) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error().Err(result.Error).Msg("unable to find user") |  | ||||||
| 		return nil, result.Error |  | ||||||
| 	} |  | ||||||
| 	user.db = s.db |  | ||||||
| 	return &user, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Store) LogFailedAuth(UserID string, r *http.Request) { |  | ||||||
| 	// FIXME implement
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Store) LogSuccessfulAuth(UserID string, r *http.Request) { |  | ||||||
| 	now := time.Now() |  | ||||||
| 	u, err := s.FindUserByID(UserID) |  | ||||||
| 	if err != nil { |  | ||||||
| 		panic("unable to find user") |  | ||||||
| 	} |  | ||||||
| 	s.db.Model(u).Update("LastSeen", now) |  | ||||||
| 	//s.db.Model(u).Update("LastSeenIP", FIXME)
 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (s *Store) AuthenticateUser(username, password, rootPasswordHash string, r *http.Request) (bool, *User, error) { |  | ||||||
| 
 |  | ||||||
| 	if username == "root" { |  | ||||||
| 		user, err := s.FindUserByUsername(username) |  | ||||||
| 		if err != nil { |  | ||||||
| 			// we probably need to create the root user in the db
 |  | ||||||
| 			// and set 'user' because it's returned
 |  | ||||||
| 			user, err = s.AddUser("root", "*") |  | ||||||
| 			if err != nil { |  | ||||||
| 				log.Error().Err(err).Msg("unable to create root user") |  | ||||||
| 				return false, nil, err |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// we also ignore if the root user in the database is disabled,
 |  | ||||||
| 		// because root can always log in using the env var password
 |  | ||||||
| 
 |  | ||||||
| 		// even if the root user exists in the db, we still use the root
 |  | ||||||
| 		// password from the environment var, and ignore the root password
 |  | ||||||
| 		// hash in the database
 |  | ||||||
| 		err = bcrypt.CompareHashAndPassword([]byte(rootPasswordHash), []byte(password)) |  | ||||||
| 		if err != nil { |  | ||||||
| 			s.LogFailedAuth(user.ID, r) |  | ||||||
| 			return false, nil, nil |  | ||||||
| 		} |  | ||||||
| 		s.LogSuccessfulAuth(user.ID, r) |  | ||||||
| 		return true, user, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// FIXME update user record when auth fails
 |  | ||||||
| 	user, err := s.FindUserByUsername(username) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if user == nil { |  | ||||||
| 		return false, nil, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if len(password) < 8 { |  | ||||||
| 		s.LogFailedAuth(user.ID, r) |  | ||||||
| 		return false, nil, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// how long are bcrypt hashes anyway?
 |  | ||||||
| 	if len(user.PasswordHash) < 10 { |  | ||||||
| 		s.LogFailedAuth(user.ID, r) |  | ||||||
| 		return false, nil, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if user.Disabled { |  | ||||||
| 		s.LogFailedAuth(user.ID, r) |  | ||||||
| 		return false, nil, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) |  | ||||||
| 	if err != nil { |  | ||||||
| 		if err == bcrypt.ErrMismatchedHashAndPassword { |  | ||||||
| 			s.LogFailedAuth(user.ID, r) |  | ||||||
| 			return false, nil, nil |  | ||||||
| 		} |  | ||||||
| 		log.Error().Err(err).Msg("password comparison error") |  | ||||||
| 		return false, nil, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	s.LogSuccessfulAuth(user.ID, r) |  | ||||||
| 	return true, user, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) Disable() error { |  | ||||||
| 	u.Disabled = true |  | ||||||
| 	result := u.db.Save(u) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error(). |  | ||||||
| 			Str("email", *u.Email). |  | ||||||
| 			Err(result.Error). |  | ||||||
| 			Msg("unable to disable user") |  | ||||||
| 		return result.Error |  | ||||||
| 	} |  | ||||||
| 	log.Debug(). |  | ||||||
| 		Str("email", *u.Email). |  | ||||||
| 		Msg("user disabled") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) IsAdmin() bool { |  | ||||||
| 	return u.HasAttribute("admin") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) IsSuperAdmin() bool { |  | ||||||
| 	return u.HasAttribute("superadmin") |  | ||||||
| } |  | ||||||
| @ -1,87 +0,0 @@ | |||||||
| package store |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"strings" |  | ||||||
| 
 |  | ||||||
| 	"github.com/rs/zerolog/log" |  | ||||||
| 	"gorm.io/gorm" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type UserAttribute struct { |  | ||||||
| 	ID     uint     `gorm:"primaryKey"` |  | ||||||
| 	UserID string   `gorm:"not null"` |  | ||||||
| 	Value  string   `gorm:"not null"` |  | ||||||
| 	db     *gorm.DB `gorm:"-"` |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (UserAttribute) TableName() string { |  | ||||||
| 	return UserAttributesTableName |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) AddAttribute(value string) error { |  | ||||||
| 	value = strings.ToLower(value) |  | ||||||
| 	if u.HasAttribute(value) { |  | ||||||
| 		log.Debug().Str("attribute", value).Msg("attribute already exists") |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	attribute := &UserAttribute{ |  | ||||||
| 		UserID: u.ID, |  | ||||||
| 		Value:  value, |  | ||||||
| 		db:     u.db, |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	result := u.db.Create(attribute) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error().Err(result.Error).Msg("unable to add user attribute") |  | ||||||
| 		return result.Error |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	log.Debug(). |  | ||||||
| 		Str("email", *u.Email). |  | ||||||
| 		Str("attribute", value). |  | ||||||
| 		Msg("user attribute added") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) DeleteAttribute(value string) error { |  | ||||||
| 	value = strings.ToLower(value) |  | ||||||
| 	result := u.db.Where("user_id = ? AND value = ?", u.ID, value).Delete(&UserAttribute{}) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error().Err(result.Error).Msg("unable to delete user attribute") |  | ||||||
| 		return result.Error |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	log.Debug(). |  | ||||||
| 		Str("email", *u.Email). |  | ||||||
| 		Str("attribute", value). |  | ||||||
| 		Msg("user attribute deleted") |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) HasAttribute(value string) bool { |  | ||||||
| 	value = strings.ToLower(value) |  | ||||||
| 	var count int64 |  | ||||||
| 	result := u.db.Model(&UserAttribute{}).Where("user_id = ? AND value = ?", u.ID, value).Count(&count) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error().Err(result.Error).Msg("unable to check user attribute") |  | ||||||
| 		return false |  | ||||||
| 	} |  | ||||||
| 	return count > 0 |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) GetAttributes() ([]string, error) { |  | ||||||
| 	var attributes []UserAttribute |  | ||||||
| 	result := u.db.Where("user_id = ?", u.ID).Find(&attributes) |  | ||||||
| 	if result.Error != nil { |  | ||||||
| 		log.Error().Err(result.Error).Msg("unable to get user attributes") |  | ||||||
| 		return nil, result.Error |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var attributeValues []string |  | ||||||
| 	for _, attr := range attributes { |  | ||||||
| 		attributeValues = append(attributeValues, attr.Value) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return attributeValues, nil |  | ||||||
| } |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user