mirror of
				https://github.com/NginxProxyManager/nginx-proxy-manager.git
				synced 2025-10-30 23:33:34 +00:00 
			
		
		
		
	Adds LDAP auth support
This commit is contained in:
		| @@ -10,6 +10,24 @@ | |||||||
| 				"$ref": "file://./paths/get.json" | 				"$ref": "file://./paths/get.json" | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
|  | 		"/auth": { | ||||||
|  | 			"get": { | ||||||
|  | 				"$ref": "file://./paths/auth/get.json" | ||||||
|  | 			}, | ||||||
|  | 			"post": { | ||||||
|  | 				"$ref": "file://./paths/auth/post.json" | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		"/auth/refresh": { | ||||||
|  | 			"post": { | ||||||
|  | 				"$ref": "file://./paths/auth/refresh/post.json" | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  | 		"/auth/sse": { | ||||||
|  | 			"post": { | ||||||
|  | 				"$ref": "file://./paths/auth/sse/post.json" | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
| 		"/certificates": { | 		"/certificates": { | ||||||
| 			"get": { | 			"get": { | ||||||
| 				"$ref": "file://./paths/certificates/get.json" | 				"$ref": "file://./paths/certificates/get.json" | ||||||
| @@ -155,19 +173,6 @@ | |||||||
| 				"$ref": "file://./paths/streams/streamID/delete.json" | 				"$ref": "file://./paths/streams/streamID/delete.json" | ||||||
| 			} | 			} | ||||||
| 		}, | 		}, | ||||||
| 		"/tokens": { |  | ||||||
| 			"get": { |  | ||||||
| 				"$ref": "file://./paths/tokens/get.json" |  | ||||||
| 			}, |  | ||||||
| 			"post": { |  | ||||||
| 				"$ref": "file://./paths/tokens/post.json" |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		"/tokens/sse": { |  | ||||||
| 			"post": { |  | ||||||
| 				"$ref": "file://./paths/tokens/sse/post.json" |  | ||||||
| 			} |  | ||||||
| 		}, |  | ||||||
| 		"/upstreams": { | 		"/upstreams": { | ||||||
| 			"get": { | 			"get": { | ||||||
| 				"$ref": "file://./paths/upstreams/get.json" | 				"$ref": "file://./paths/upstreams/get.json" | ||||||
| @@ -219,6 +224,9 @@ | |||||||
| 	}, | 	}, | ||||||
| 	"components": { | 	"components": { | ||||||
| 		"schemas": { | 		"schemas": { | ||||||
|  | 			"AuthConfigObject": { | ||||||
|  | 				"$ref": "file://./components/AuthConfigObject.json" | ||||||
|  | 			}, | ||||||
| 			"CertificateAuthorityList": { | 			"CertificateAuthorityList": { | ||||||
| 				"$ref": "file://./components/CertificateAuthorityList.json" | 				"$ref": "file://./components/CertificateAuthorityList.json" | ||||||
| 			}, | 			}, | ||||||
|   | |||||||
							
								
								
									
										13
									
								
								backend/embed/api_docs/components/AuthConfigObject.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								backend/embed/api_docs/components/AuthConfigObject.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | { | ||||||
|  | 	"type": "array", | ||||||
|  | 	"description": "AuthConfigObject", | ||||||
|  | 	"minItems": 1, | ||||||
|  | 	"items": { | ||||||
|  | 		"type": "string", | ||||||
|  | 		"enum": [ | ||||||
|  | 			"local", | ||||||
|  | 			"ldap", | ||||||
|  | 			"oidc" | ||||||
|  | 		] | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -7,7 +7,6 @@ | |||||||
| 		"created_at", | 		"created_at", | ||||||
| 		"updated_at", | 		"updated_at", | ||||||
| 		"name", | 		"name", | ||||||
| 		"nickname", |  | ||||||
| 		"email", | 		"email", | ||||||
| 		"is_disabled" | 		"is_disabled" | ||||||
| 	], | 	], | ||||||
| @@ -29,12 +28,7 @@ | |||||||
| 		"name": { | 		"name": { | ||||||
| 			"type": "string", | 			"type": "string", | ||||||
| 			"minLength": 2, | 			"minLength": 2, | ||||||
| 			"maxLength": 100 | 			"maxLength": 50 | ||||||
| 		}, |  | ||||||
| 		"nickname": { |  | ||||||
| 			"type": "string", |  | ||||||
| 			"minLength": 2, |  | ||||||
| 			"maxLength": 100 |  | ||||||
| 		}, | 		}, | ||||||
| 		"email": { | 		"email": { | ||||||
| 			"type": "string", | 			"type": "string", | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								backend/embed/api_docs/paths/auth/get.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								backend/embed/api_docs/paths/auth/get.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | { | ||||||
|  | 	"operationId": "getAuthConfig", | ||||||
|  | 	"summary": "Returns auth configuration", | ||||||
|  | 	"tags": ["Auth"], | ||||||
|  | 	"responses": { | ||||||
|  | 		"200": { | ||||||
|  | 			"description": "200 response", | ||||||
|  | 			"content": { | ||||||
|  | 				"application/json": { | ||||||
|  | 					"schema": { | ||||||
|  | 						"type": "object", | ||||||
|  | 						"required": ["result"], | ||||||
|  | 						"properties": { | ||||||
|  | 							"result": { | ||||||
|  | 								"$ref": "#/components/schemas/AuthConfigObject" | ||||||
|  | 							} | ||||||
|  | 						} | ||||||
|  | 					}, | ||||||
|  | 					"examples": { | ||||||
|  | 						"default": { | ||||||
|  | 							"value": "todo" | ||||||
|  | 						} | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
| 	"operationId": "requestToken", | 	"operationId": "requestToken", | ||||||
| 	"summary": "Request a new access token from credentials", | 	"summary": "Request a new access token from credentials", | ||||||
| 	"tags": ["Tokens"], | 	"tags": ["Auth"], | ||||||
| 	"requestBody": { | 	"requestBody": { | ||||||
| 		"description": "Credentials Payload", | 		"description": "Credentials Payload", | ||||||
| 		"required": true, | 		"required": true, | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
| 	"operationId": "refreshToken", | 	"operationId": "refreshToken", | ||||||
| 	"summary": "Refresh your access token", | 	"summary": "Refresh your access token", | ||||||
| 	"tags": ["Tokens"], | 	"tags": ["Auth"], | ||||||
| 	"responses": { | 	"responses": { | ||||||
| 		"200": { | 		"200": { | ||||||
| 			"description": "200 response", | 			"description": "200 response", | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
| 	"operationId": "requestSSEToken", | 	"operationId": "requestSSEToken", | ||||||
| 	"summary": "Request a new SSE token", | 	"summary": "Request a new SSE token", | ||||||
| 	"tags": ["Tokens"], | 	"tags": ["Auth"], | ||||||
| 	"responses": { | 	"responses": { | ||||||
| 		"200": { | 		"200": { | ||||||
| 			"description": "200 response", | 			"description": "200 response", | ||||||
| @@ -28,7 +28,7 @@ | |||||||
| 				"type": "string" | 				"type": "string" | ||||||
| 			}, | 			}, | ||||||
| 			"description": "The sorting of the list", | 			"description": "The sorting of the list", | ||||||
| 			"example": "name,nickname.desc,email.asc" | 			"example": "name,email.asc" | ||||||
| 		} | 		} | ||||||
| 	], | 	], | ||||||
| 	"responses": { | 	"responses": { | ||||||
| @@ -57,10 +57,6 @@ | |||||||
| 											"field": "name", | 											"field": "name", | ||||||
| 											"direction": "ASC" | 											"direction": "ASC" | ||||||
| 										}, | 										}, | ||||||
| 										{ |  | ||||||
| 											"field": "nickname", |  | ||||||
| 											"direction": "DESC" |  | ||||||
| 										}, |  | ||||||
| 										{ | 										{ | ||||||
| 											"field": "email", | 											"field": "email", | ||||||
| 											"direction": "ASC" | 											"direction": "ASC" | ||||||
| @@ -70,7 +66,6 @@ | |||||||
| 										{ | 										{ | ||||||
| 											"id": 1, | 											"id": 1, | ||||||
| 											"name": "Jamie Curnow", | 											"name": "Jamie Curnow", | ||||||
| 											"nickname": "James", |  | ||||||
| 											"email": "jc@jc21.com", | 											"email": "jc@jc21.com", | ||||||
| 											"created_at": 1578010090000, | 											"created_at": 1578010090000, | ||||||
| 											"updated_at": 1578010095000, | 											"updated_at": 1578010095000, | ||||||
| @@ -81,7 +76,6 @@ | |||||||
| 										{ | 										{ | ||||||
| 											"id": 2, | 											"id": 2, | ||||||
| 											"name": "John Doe", | 											"name": "John Doe", | ||||||
| 											"nickname": "John", |  | ||||||
| 											"email": "johdoe@example.com", | 											"email": "johdoe@example.com", | ||||||
| 											"created_at": 1578010100000, | 											"created_at": 1578010100000, | ||||||
| 											"updated_at": 1578010105000, | 											"updated_at": 1578010105000, | ||||||
| @@ -95,7 +89,6 @@ | |||||||
| 										{ | 										{ | ||||||
| 											"id": 3, | 											"id": 3, | ||||||
| 											"name": "Jane Doe", | 											"name": "Jane Doe", | ||||||
| 											"nickname": "Jane", |  | ||||||
| 											"email": "janedoe@example.com", | 											"email": "janedoe@example.com", | ||||||
| 											"created_at": 1578010110000, | 											"created_at": 1578010110000, | ||||||
| 											"updated_at": 1578010115000, | 											"updated_at": 1578010115000, | ||||||
|   | |||||||
| @@ -31,7 +31,6 @@ | |||||||
| 								"result": { | 								"result": { | ||||||
| 									"id": 1, | 									"id": 1, | ||||||
| 									"name": "Jamie Curnow", | 									"name": "Jamie Curnow", | ||||||
| 									"nickname": "James", |  | ||||||
| 									"email": "jc@jc21.com", | 									"email": "jc@jc21.com", | ||||||
| 									"created_at": 1578010100000, | 									"created_at": 1578010100000, | ||||||
| 									"updated_at": 1578010100000, | 									"updated_at": 1578010100000, | ||||||
|   | |||||||
| @@ -43,7 +43,6 @@ | |||||||
| 								"result": { | 								"result": { | ||||||
| 									"id": 1, | 									"id": 1, | ||||||
| 									"name": "Jamie Curnow", | 									"name": "Jamie Curnow", | ||||||
| 									"nickname": "James", |  | ||||||
| 									"email": "jc@jc21.com", | 									"email": "jc@jc21.com", | ||||||
| 									"created_at": 1578010100000, | 									"created_at": 1578010100000, | ||||||
| 									"updated_at": 1578010105000, | 									"updated_at": 1578010105000, | ||||||
|   | |||||||
| @@ -52,7 +52,6 @@ | |||||||
| 								"result": { | 								"result": { | ||||||
| 									"id": 1, | 									"id": 1, | ||||||
| 									"name": "Jamie Curnow", | 									"name": "Jamie Curnow", | ||||||
| 									"nickname": "James", |  | ||||||
| 									"email": "jc@jc21.com", | 									"email": "jc@jc21.com", | ||||||
| 									"created_at": 1578010100000, | 									"created_at": 1578010100000, | ||||||
| 									"updated_at": 1578010110000, | 									"updated_at": 1578010110000, | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ CREATE TABLE IF NOT EXISTS `user` | |||||||
| 	`updated_at` BIGINT NOT NULL DEFAULT 0, | 	`updated_at` BIGINT NOT NULL DEFAULT 0, | ||||||
| 	`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism | 	`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism | ||||||
| 	`name` VARCHAR(50) NOT NULL, | 	`name` VARCHAR(50) NOT NULL, | ||||||
| 	`nickname` VARCHAR(50) NOT NULL, |  | ||||||
| 	`email` VARCHAR(255) NOT NULL, | 	`email` VARCHAR(255) NOT NULL, | ||||||
| 	`is_system` BOOLEAN NOT NULL DEFAULT FALSE, | 	`is_system` BOOLEAN NOT NULL DEFAULT FALSE, | ||||||
| 	`is_disabled` BOOLEAN NOT NULL DEFAULT FALSE | 	`is_disabled` BOOLEAN NOT NULL DEFAULT FALSE | ||||||
| @@ -45,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `auth` | |||||||
| 	`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism | 	`is_deleted` INT NOT NULL DEFAULT 0, -- int on purpose, gormism | ||||||
| 	`user_id` INT NOT NULL, | 	`user_id` INT NOT NULL, | ||||||
| 	`type` VARCHAR(50) NOT NULL, | 	`type` VARCHAR(50) NOT NULL, | ||||||
|  | 	`identity` VARCHAR(255) NOT NULL, | ||||||
| 	`secret` VARCHAR(255) NOT NULL, | 	`secret` VARCHAR(255) NOT NULL, | ||||||
| 	FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, | 	FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, | ||||||
| 	UNIQUE (`user_id`, `type`) | 	UNIQUE (`user_id`, `type`) | ||||||
|   | |||||||
| @@ -37,6 +37,27 @@ INSERT INTO `setting` ( | |||||||
| 	"default-site", | 	"default-site", | ||||||
| 	"What to show users who hit your Nginx server by default", | 	"What to show users who hit your Nginx server by default", | ||||||
| 	'"welcome"' -- remember this is json | 	'"welcome"' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), | ||||||
|  | 	ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), | ||||||
|  | 	"auth-methods", | ||||||
|  | 	"Which methods are enabled for authentication", | ||||||
|  | 	'["local"]' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), | ||||||
|  | 	ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), | ||||||
|  | 	"oidc-auth", | ||||||
|  | 	"Configuration for OIDC authentication", | ||||||
|  | 	'{}' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), | ||||||
|  | 	ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000), | ||||||
|  | 	"ldap-auth", | ||||||
|  | 	"Configuration for LDAP authentication", | ||||||
|  | 	'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json | ||||||
| ); | ); | ||||||
|  |  | ||||||
| -- Default Certificate Authorities | -- Default Certificate Authorities | ||||||
|   | |||||||
| @@ -15,7 +15,6 @@ CREATE TABLE "user" ( | |||||||
| 	"updated_at" BIGINT NOT NULL DEFAULT 0, | 	"updated_at" BIGINT NOT NULL DEFAULT 0, | ||||||
| 	"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism | 	"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism | ||||||
| 	"name" VARCHAR(50) NOT NULL, | 	"name" VARCHAR(50) NOT NULL, | ||||||
| 	"nickname" VARCHAR(50) NOT NULL, |  | ||||||
| 	"email" VARCHAR(255) NOT NULL, | 	"email" VARCHAR(255) NOT NULL, | ||||||
| 	"is_system" BOOLEAN NOT NULL DEFAULT FALSE, | 	"is_system" BOOLEAN NOT NULL DEFAULT FALSE, | ||||||
| 	"is_disabled" BOOLEAN NOT NULL DEFAULT FALSE | 	"is_disabled" BOOLEAN NOT NULL DEFAULT FALSE | ||||||
| @@ -39,6 +38,7 @@ CREATE TABLE "auth" ( | |||||||
| 	"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism | 	"is_deleted" INTEGER NOT NULL DEFAULT 0, -- int on purpose, gormism | ||||||
| 	"user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, | 	"user_id" INTEGER NOT NULL REFERENCES "user"("id") ON DELETE CASCADE, | ||||||
| 	"type" VARCHAR(50) NOT NULL, | 	"type" VARCHAR(50) NOT NULL, | ||||||
|  | 	"identity" VARCHAR(255) NOT NULL, | ||||||
| 	"secret" VARCHAR(255) NOT NULL, | 	"secret" VARCHAR(255) NOT NULL, | ||||||
| 	UNIQUE ("user_id", "type") | 	UNIQUE ("user_id", "type") | ||||||
| ); | ); | ||||||
|   | |||||||
| @@ -37,6 +37,27 @@ INSERT INTO "setting" ( | |||||||
| 	'default-site', | 	'default-site', | ||||||
| 	'What to show users who hit your Nginx server by default', | 	'What to show users who hit your Nginx server by default', | ||||||
| 	'"welcome"' -- remember this is json | 	'"welcome"' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, | ||||||
|  | 	EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, | ||||||
|  | 	'auth-methods', | ||||||
|  | 	'Which methods are enabled for authentication', | ||||||
|  | 	'["local"]' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, | ||||||
|  | 	EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, | ||||||
|  | 	'oidc-auth', | ||||||
|  | 	'Configuration for OIDC authentication', | ||||||
|  | 	'{}' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, | ||||||
|  | 	EXTRACT(EPOCH FROM TIMESTAMP '2011-05-17 10:40:28.876944') * 1000, | ||||||
|  | 	'ldap-auth', | ||||||
|  | 	'Configuration for LDAP authentication', | ||||||
|  | 	'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json | ||||||
| ); | ); | ||||||
|  |  | ||||||
| -- Default Certificate Authorities | -- Default Certificate Authorities | ||||||
|   | |||||||
| @@ -17,7 +17,6 @@ CREATE TABLE IF NOT EXISTS `user` | |||||||
| 	`updated_at` INTEGER NOT NULL DEFAULT 0, | 	`updated_at` INTEGER NOT NULL DEFAULT 0, | ||||||
| 	`is_deleted` INTEGER NOT NULL DEFAULT 0, | 	`is_deleted` INTEGER NOT NULL DEFAULT 0, | ||||||
| 	`name` TEXT NOT NULL, | 	`name` TEXT NOT NULL, | ||||||
| 	`nickname` TEXT NOT NULL, |  | ||||||
| 	`email` TEXT NOT NULL, | 	`email` TEXT NOT NULL, | ||||||
| 	`is_system` INTEGER NOT NULL DEFAULT 0, | 	`is_system` INTEGER NOT NULL DEFAULT 0, | ||||||
| 	`is_disabled` INTEGER NOT NULL DEFAULT 0 | 	`is_disabled` INTEGER NOT NULL DEFAULT 0 | ||||||
| @@ -45,6 +44,7 @@ CREATE TABLE IF NOT EXISTS `auth` | |||||||
| 	`is_deleted` INTEGER NOT NULL DEFAULT 0, | 	`is_deleted` INTEGER NOT NULL DEFAULT 0, | ||||||
| 	`user_id` INTEGER NOT NULL, | 	`user_id` INTEGER NOT NULL, | ||||||
| 	`type` TEXT NOT NULL, | 	`type` TEXT NOT NULL, | ||||||
|  | 	`identity` TEXT NOT NULL, | ||||||
| 	`secret` TEXT NOT NULL, | 	`secret` TEXT NOT NULL, | ||||||
| 	FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, | 	FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE, | ||||||
| 	UNIQUE (`user_id`, `type`) | 	UNIQUE (`user_id`, `type`) | ||||||
|   | |||||||
| @@ -36,6 +36,27 @@ INSERT INTO `setting` ( | |||||||
| 	"default-site", | 	"default-site", | ||||||
| 	"What to show users who hit your Nginx server by default", | 	"What to show users who hit your Nginx server by default", | ||||||
| 	'"welcome"' -- remember this is json | 	'"welcome"' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	unixepoch() * 1000, | ||||||
|  | 	unixepoch() * 1000, | ||||||
|  | 	"auth-methods", | ||||||
|  | 	"Which methods are enabled for authentication", | ||||||
|  | 	'["local"]' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	unixepoch() * 1000, | ||||||
|  | 	unixepoch() * 1000, | ||||||
|  | 	"oidc-auth", | ||||||
|  | 	"Configuration for OIDC authentication", | ||||||
|  | 	'{}' -- remember this is json | ||||||
|  | ), | ||||||
|  | ( | ||||||
|  | 	unixepoch() * 1000, | ||||||
|  | 	unixepoch() * 1000, | ||||||
|  | 	"ldap-auth", | ||||||
|  | 	"Configuration for LDAP authentication", | ||||||
|  | 	'{"host": "", "dn": "", "sync_by": "uid"}' -- remember this is json | ||||||
| ); | ); | ||||||
|  |  | ||||||
| -- Default Certificate Authorities | -- Default Certificate Authorities | ||||||
|   | |||||||
| @@ -32,14 +32,17 @@ require ( | |||||||
|  |  | ||||||
| require ( | require ( | ||||||
| 	filippo.io/edwards25519 v1.1.0 // indirect | 	filippo.io/edwards25519 v1.1.0 // indirect | ||||||
|  | 	github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect | ||||||
| 	github.com/alexflint/go-scalar v1.2.0 // indirect | 	github.com/alexflint/go-scalar v1.2.0 // indirect | ||||||
| 	github.com/davecgh/go-spew v1.1.1 // indirect | 	github.com/davecgh/go-spew v1.1.1 // indirect | ||||||
| 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect | 	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect | ||||||
| 	github.com/dustin/go-humanize v1.0.1 // indirect | 	github.com/dustin/go-humanize v1.0.1 // indirect | ||||||
| 	github.com/glebarez/go-sqlite v1.21.2 // indirect | 	github.com/glebarez/go-sqlite v1.21.2 // indirect | ||||||
|  | 	github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect | ||||||
|  | 	github.com/go-ldap/ldap/v3 v3.4.8 // indirect | ||||||
| 	github.com/go-sql-driver/mysql v1.8.1 // indirect | 	github.com/go-sql-driver/mysql v1.8.1 // indirect | ||||||
| 	github.com/goccy/go-json v0.10.2 // indirect | 	github.com/goccy/go-json v0.10.2 // indirect | ||||||
| 	github.com/google/uuid v1.3.0 // indirect | 	github.com/google/uuid v1.6.0 // indirect | ||||||
| 	github.com/jackc/pgpassfile v1.0.0 // indirect | 	github.com/jackc/pgpassfile v1.0.0 // indirect | ||||||
| 	github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect | 	github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect | ||||||
| 	github.com/jackc/pgx/v5 v5.5.5 // indirect | 	github.com/jackc/pgx/v5 v5.5.5 // indirect | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= | ||||||
| filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= | ||||||
|  | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= | ||||||
|  | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= | ||||||
| github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= | github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= | ||||||
| github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= | github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= | ||||||
|  | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= | ||||||
| github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= | github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y= | ||||||
| github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= | github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= | ||||||
| github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= | github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= | ||||||
| @@ -26,12 +29,16 @@ github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9g | |||||||
| github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= | github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= | ||||||
| github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= | github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= | ||||||
| github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= | github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= | ||||||
|  | github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA= | ||||||
|  | github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= | ||||||
| github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= | github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= | ||||||
| github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= | ||||||
| github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= | ||||||
| github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= | ||||||
| github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A= | github.com/go-chi/jwtauth/v5 v5.3.1 h1:1ePWrjVctvp1tyBq5b/2ER8Th/+RbYc7x4qNsc5rh5A= | ||||||
| github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80= | github.com/go-chi/jwtauth/v5 v5.3.1/go.mod h1:6Fl2RRmWXs3tJYE1IQGX81FsPoGqDwq9c15j52R5q80= | ||||||
|  | github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ= | ||||||
|  | github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk= | ||||||
| github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= | ||||||
| github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= | ||||||
| github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= | ||||||
| @@ -47,6 +54,12 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu | |||||||
| github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= | github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= | ||||||
| github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= | ||||||
| github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= | ||||||
|  | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
|  | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||||
|  | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= | ||||||
|  | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||||
|  | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= | ||||||
| github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= | 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/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= | ||||||
| github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= | github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= | ||||||
| @@ -61,6 +74,12 @@ github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec h1:KKntwkZlM2w/88QiDyA | |||||||
| github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s= | github.com/jc21/go-sse v0.0.0-20230307071053-2e6b1dbcb7ec/go.mod h1:4v5Xmm0eYuaWqKJ63XUV5YfQPoxtId3DgDytbnWhi+s= | ||||||
| github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k= | github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760 h1:7wxq2DIgtO36KLrFz1RldysO0WVvcYsD49G9tyAs01k= | ||||||
| github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= | github.com/jc21/jsref v0.0.0-20210608024405-a97debfc4760/go.mod h1:yIq2t51OJgVsdRlPY68NAnyVdBH0kYXxDTFtUxOap80= | ||||||
|  | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= | ||||||
|  | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= | ||||||
|  | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= | ||||||
|  | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= | ||||||
|  | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= | ||||||
|  | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= | ||||||
| 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.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= | ||||||
| @@ -136,40 +155,90 @@ github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr | |||||||
| github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= | github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= | ||||||
| github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= | ||||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||||
|  | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||||
|  | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= | ||||||
| github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= | ||||||
| github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= | ||||||
| github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= | ||||||
| github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | ||||||
| github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= | 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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | ||||||
| github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= | ||||||
| github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= | ||||||
| github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk= | github.com/vrischmann/envconfig v1.3.0 h1:4XIvQTXznxmWMnjouj0ST5lFo/WAYf5Exgl3x82crEk= | ||||||
| github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI= | github.com/vrischmann/envconfig v1.3.0/go.mod h1:bbvxFYJdRSpXrhS63mBFtKJzkDiNkyArOLXtY6q0kuI= | ||||||
| github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= | github.com/wacul/ptr v1.0.0/go.mod h1:BD0gjsZrCwtoR+yWDB9v2hQ8STlq9tT84qKfa+3txOc= | ||||||
|  | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | ||||||
| github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w= | github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04 h1:qXafrlZL1WsJW5OokjraLLRURHiw0OzKHD/RNdspp4w= | ||||||
| github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4= | github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04/go.mod h1:FiwNQxz6hGoNFBC4nIx+CxZhI3nne5RmIOlT/MXcSD4= | ||||||
| gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 h1:NPHauobrOymc80Euu+e0tsMyXcdtLCX5bQPKX5zsI38= | gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85 h1:NPHauobrOymc80Euu+e0tsMyXcdtLCX5bQPKX5zsI38= | ||||||
| gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= | gitlab.com/jc21com/sqlite v1.22.2-0.20230527022643-b56cedb3bc85/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= | ||||||
| go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= | ||||||
| go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= | ||||||
|  | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||||
|  | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||||
|  | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= | ||||||
|  | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||||
|  | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= | ||||||
| golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= | golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= | ||||||
| golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= | golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= | ||||||
|  | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||||
|  | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||||
| golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= | ||||||
| golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||||
|  | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||||
|  | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | ||||||
|  | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||||
|  | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||||
|  | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||||
|  | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||||
|  | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= | ||||||
|  | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= | ||||||
|  | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
|  | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||||
| golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= | ||||||
| golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | ||||||
|  | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||||
| golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
|  | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20210326220804-49726bf1d181/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||||
| golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| 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= | ||||||
|  | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||||
|  | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
|  | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
| golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= | ||||||
| golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||||
|  | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||||
|  | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||||
|  | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||||||
|  | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= | ||||||
|  | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||||||
|  | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= | ||||||
|  | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||||
|  | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||||
|  | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||||
|  | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||||
|  | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||||
|  | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||||
| golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= | ||||||
| golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= | ||||||
|  | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||||
|  | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | ||||||
|  | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||||
|  | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||||||
| golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= | ||||||
| golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= | ||||||
|  | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||||
| gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||||
|   | |||||||
| @@ -3,97 +3,244 @@ package handler | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	h "npm/internal/api/http" | ||||||
|  | 	"npm/internal/errors" | ||||||
|  | 	"npm/internal/logger" | ||||||
|  | 	"slices" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	c "npm/internal/api/context" | 	c "npm/internal/api/context" | ||||||
| 	h "npm/internal/api/http" |  | ||||||
| 	"npm/internal/entity/auth" | 	"npm/internal/entity/auth" | ||||||
|  | 	"npm/internal/entity/setting" | ||||||
| 	"npm/internal/entity/user" | 	"npm/internal/entity/user" | ||||||
| 	"npm/internal/errors" | 	njwt "npm/internal/jwt" | ||||||
| 	"npm/internal/logger" |  | ||||||
|  |  | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| type setAuthModel struct { | // tokenPayload is the structure we expect from a incoming login request | ||||||
| 	// The json tags are required, as the change password form decodes into this object | type tokenPayload struct { | ||||||
| 	Type     string `json:"type"` | 	Type     string `json:"type"` | ||||||
|  | 	Identity string `json:"identity"` | ||||||
| 	Secret   string `json:"secret"` | 	Secret   string `json:"secret"` | ||||||
| 	CurrentSecret string `json:"current_secret"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // SetAuth sets a auth method. This can be used for "me" and `2` for example | // GetAuthConfig is anonymous and returns the types of authentication | ||||||
| // Route: POST /users/:userID/auth | // enabled for this site | ||||||
| func SetAuth() func(http.ResponseWriter, *http.Request) { | func GetAuthConfig() func(http.ResponseWriter, *http.Request) { | ||||||
| 	return func(w http.ResponseWriter, r *http.Request) { | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		val, err := setting.GetAuthMethods() | ||||||
|  | 		if err == gorm.ErrRecordNotFound { | ||||||
|  | 			h.ResultResponseJSON(w, r, http.StatusOK, nil) | ||||||
|  | 			return | ||||||
|  | 		} else if err != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		h.ResultResponseJSON(w, r, http.StatusOK, val) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewToken Also known as a Login, requesting a new token with credentials | ||||||
|  | // Route: POST /auth | ||||||
|  | func NewToken() func(http.ResponseWriter, *http.Request) { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		// Read the bytes from the body | ||||||
| 		bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) | 		bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) | ||||||
|  |  | ||||||
| 		var newAuth setAuthModel | 		var payload tokenPayload | ||||||
| 		err := json.Unmarshal(bodyBytes, &newAuth) | 		err := json.Unmarshal(bodyBytes, &payload) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		userID, isSelf, userIDErr := getUserIDFromRequest(r) | 		// Check that this auth type is enabled | ||||||
| 		if userIDErr != nil { | 		if authMethods, err := setting.GetAuthMethods(); err == gorm.ErrRecordNotFound { | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) | 			h.ResultResponseJSON(w, r, http.StatusOK, nil) | ||||||
|  | 			return | ||||||
|  | 		} else if err != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} else if !slices.Contains(authMethods, payload.Type) { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidAuthType.Error(), nil) | ||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Load user | 		switch payload.Type { | ||||||
| 		thisUser, thisUserErr := user.GetByID(userID) | 		case "ldap": | ||||||
| 		if thisUserErr == gorm.ErrRecordNotFound { | 			newTokenLDAP(w, r, payload) | ||||||
| 			h.NotFound(w, r) | 		case "oidc": | ||||||
| 			return | 			newTokenOIDC(w, r, payload) | ||||||
| 		} else if thisUserErr != nil { | 		case "local": | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil) | 			newTokenLocal(w, r, payload) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newTokenLocal(w http.ResponseWriter, r *http.Request, payload tokenPayload) { | ||||||
|  | 	// Find user by email | ||||||
|  | 	userObj, userErr := user.GetByEmail(payload.Identity) | ||||||
|  | 	if userErr != nil { | ||||||
|  | 		logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), userErr.Error()) | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 		if thisUser.IsSystem { | 	if userObj.IsDisabled { | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil) | 		h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 		// Load existing auth for user | 	// Get Auth | ||||||
| 		userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type) | 	authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type) | ||||||
| 		if userAuthErr != nil { | 	if authErr != nil { | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil) | 		logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error()) | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 		if isSelf { | 	// Verify Auth | ||||||
| 			// confirm that the current_secret given is valid for the one stored in the database | 	validateErr := authObj.ValidateSecret(payload.Secret) | ||||||
| 			validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret) |  | ||||||
| 	if validateErr != nil { | 	if validateErr != nil { | ||||||
| 				logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error()) | 		logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error()) | ||||||
| 		// Sleep for 1 second to prevent brute force password guessing | 		// Sleep for 1 second to prevent brute force password guessing | ||||||
| 		time.Sleep(time.Second) | 		time.Sleep(time.Second) | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) | ||||||
| 				h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if response, err := njwt.Generate(&userObj, false); err != nil { | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
|  | 	} else { | ||||||
|  | 		h.ResultResponseJSON(w, r, http.StatusOK, response) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| 		if newAuth.Type == auth.TypePassword { | func newTokenLDAP(w http.ResponseWriter, r *http.Request, payload tokenPayload) { | ||||||
| 			err := userAuth.SetPassword(newAuth.Secret) | 	// Get LDAP settings | ||||||
|  | 	ldapSettings, err := setting.GetLDAPSettings() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 				logger.Error("SetPasswordError", err) | 		logger.Error("LDAP settings not found", err) | ||||||
| 				h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) | 		h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if err = userAuth.Save(); err != nil { |  | ||||||
| 			logger.Error("AuthSaveError", err) |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 		userAuth.Secret = "" | 	// Lets try to authenticate with LDAP | ||||||
|  | 	ldapUser, err := auth.LDAPAuthenticate(payload.Identity, payload.Secret) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("LDAP Auth Error", err) | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 		// todo: add to audit-log | 	// Get Auth by identity | ||||||
|  | 	authObj, authErr := auth.GetByIdenityType(ldapUser.Username, payload.Type) | ||||||
|  | 	if authErr == gorm.ErrRecordNotFound { | ||||||
|  | 		// Auth is not found for this identity. We can create it | ||||||
|  | 		if !ldapSettings.AutoCreateUser { | ||||||
|  | 			// LDAP Login was successful, but user does not have an auth record | ||||||
|  | 			// and auto create is disabled. Showing account disabled error | ||||||
|  | 			// for the time being | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrUserDisabled.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		h.ResultResponseJSON(w, r, http.StatusOK, userAuth) | 		// Attempt to find user by email | ||||||
|  | 		foundUser, err := user.GetByEmail(ldapUser.Email) | ||||||
|  | 		if err == gorm.ErrRecordNotFound { | ||||||
|  | 			// User not found, create user | ||||||
|  | 			foundUser, err = user.CreateFromLDAPUser(ldapUser) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.Error("user.CreateFromLDAPUser", err) | ||||||
|  | 				h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			logger.Info("Created user from LDAP: %s, %s", ldapUser.Username, foundUser.Email) | ||||||
|  | 		} else if err != nil { | ||||||
|  | 			logger.Error("user.GetByEmail", err) | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Create auth record and attach to this user | ||||||
|  | 		authObj = auth.Model{ | ||||||
|  | 			UserID:   foundUser.ID, | ||||||
|  | 			Type:     auth.TypeLDAP, | ||||||
|  | 			Identity: ldapUser.Username, | ||||||
|  | 		} | ||||||
|  | 		if err := authObj.Save(); err != nil { | ||||||
|  | 			logger.Error("auth.Save", err) | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		logger.Info("Created LDAP auth for user: %s, %s", ldapUser.Username, foundUser.Email) | ||||||
|  | 	} else if authErr != nil { | ||||||
|  | 		logger.Error("auth.GetByIdenityType", err) | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusInternalServerError, authErr.Error(), nil) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	userObj, userErr := user.GetByID(authObj.UserID) | ||||||
|  | 	if userErr != nil { | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusBadRequest, userErr.Error(), nil) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if userObj.IsDisabled { | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if response, err := njwt.Generate(&userObj, false); err != nil { | ||||||
|  | 		h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
|  | 	} else { | ||||||
|  | 		h.ResultResponseJSON(w, r, http.StatusOK, response) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newTokenOIDC(w http.ResponseWriter, r *http.Request, _ tokenPayload) { | ||||||
|  | 	h.ResultErrorJSON(w, r, http.StatusInternalServerError, "NOT YET SUPPORTED", nil) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // RefreshToken an existing token by given them a new one with the same claims | ||||||
|  | // Route: POST /auth/refresh | ||||||
|  | func RefreshToken() func(http.ResponseWriter, *http.Request) { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		// TODO: Use your own methods to verify an existing user is | ||||||
|  | 		// able to refresh their token and then give them a new one | ||||||
|  | 		userObj, _ := user.GetByEmail("jc@jc21.com") | ||||||
|  | 		if response, err := njwt.Generate(&userObj, false); err != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
|  | 		} else { | ||||||
|  | 			h.ResultResponseJSON(w, r, http.StatusOK, response) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewSSEToken will generate and return a very short lived token for | ||||||
|  | // use by the /sse/* endpoint. It requires an app token to generate this | ||||||
|  | // Route: POST /auth/sse | ||||||
|  | func NewSSEToken() func(http.ResponseWriter, *http.Request) { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		userID := r.Context().Value(c.UserIDCtxKey).(uint) | ||||||
|  |  | ||||||
|  | 		// Find user | ||||||
|  | 		userObj, userErr := user.GetByID(userID) | ||||||
|  | 		if userErr != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if userObj.IsDisabled { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if response, err := njwt.Generate(&userObj, true); err != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) | ||||||
|  | 		} else { | ||||||
|  | 			h.ResultResponseJSON(w, r, http.StatusOK, response) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -64,6 +64,12 @@ func CreateSetting() func(http.ResponseWriter, *http.Request) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		// Check if the setting already exists | ||||||
|  | 		if _, err := setting.GetByName(newSetting.Name); err == nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Setting with name '%s' already exists", newSetting.Name), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if err = newSetting.Save(); err != nil { | 		if err = newSetting.Save(); err != nil { | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil) | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil) | ||||||
| 			return | 			return | ||||||
| @@ -75,6 +81,7 @@ func CreateSetting() func(http.ResponseWriter, *http.Request) { | |||||||
|  |  | ||||||
| // UpdateSetting updates a setting | // UpdateSetting updates a setting | ||||||
| // Route: PUT /settings/{name} | // Route: PUT /settings/{name} | ||||||
|  | // TODO: Add validation for the setting value, for system settings they should be validated against the setting name and type | ||||||
| func UpdateSetting() func(http.ResponseWriter, *http.Request) { | func UpdateSetting() func(http.ResponseWriter, *http.Request) { | ||||||
| 	return func(w http.ResponseWriter, r *http.Request) { | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
| 		settingName := chi.URLParam(r, "name") | 		settingName := chi.URLParam(r, "name") | ||||||
| @@ -85,13 +92,12 @@ func UpdateSetting() func(http.ResponseWriter, *http.Request) { | |||||||
| 			h.NotFound(w, r) | 			h.NotFound(w, r) | ||||||
| 		case nil: | 		case nil: | ||||||
| 			bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) | 			bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) | ||||||
| 			err := json.Unmarshal(bodyBytes, &setting) | 			if err := json.Unmarshal(bodyBytes, &setting); err != nil { | ||||||
| 			if err != nil { |  | ||||||
| 				h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) | 				h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err = setting.Save(); err != nil { | 			if err := setting.Save(); err != nil { | ||||||
| 				h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) | 				h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
|   | |||||||
| @@ -1,116 +0,0 @@ | |||||||
| package handler |  | ||||||
|  |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"net/http" |  | ||||||
| 	h "npm/internal/api/http" |  | ||||||
| 	"npm/internal/errors" |  | ||||||
| 	"npm/internal/logger" |  | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	c "npm/internal/api/context" |  | ||||||
| 	"npm/internal/entity/auth" |  | ||||||
| 	"npm/internal/entity/user" |  | ||||||
| 	njwt "npm/internal/jwt" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // tokenPayload is the structure we expect from a incoming login request |  | ||||||
| type tokenPayload struct { |  | ||||||
| 	Type     string `json:"type"` |  | ||||||
| 	Identity string `json:"identity"` |  | ||||||
| 	Secret   string `json:"secret"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // NewToken Also known as a Login, requesting a new token with credentials |  | ||||||
| // Route: POST /tokens |  | ||||||
| func NewToken() func(http.ResponseWriter, *http.Request) { |  | ||||||
| 	return func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		// Read the bytes from the body |  | ||||||
| 		bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) |  | ||||||
|  |  | ||||||
| 		var payload tokenPayload |  | ||||||
| 		err := json.Unmarshal(bodyBytes, &payload) |  | ||||||
| 		if err != nil { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Find user |  | ||||||
| 		userObj, userErr := user.GetByEmail(payload.Identity) |  | ||||||
| 		if userErr != nil { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if userObj.IsDisabled { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Get Auth |  | ||||||
| 		authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type) |  | ||||||
| 		if authErr != nil { |  | ||||||
| 			logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error()) |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		// Verify Auth |  | ||||||
| 		validateErr := authObj.ValidateSecret(payload.Secret) |  | ||||||
| 		if validateErr != nil { |  | ||||||
| 			logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error()) |  | ||||||
| 			// Sleep for 1 second to prevent brute force password guessing |  | ||||||
| 			time.Sleep(time.Second) |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if response, err := njwt.Generate(&userObj, false); err != nil { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) |  | ||||||
| 		} else { |  | ||||||
| 			h.ResultResponseJSON(w, r, http.StatusOK, response) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // RefreshToken an existing token by given them a new one with the same claims |  | ||||||
| // Route: GET /tokens |  | ||||||
| func RefreshToken() func(http.ResponseWriter, *http.Request) { |  | ||||||
| 	return func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		// TODO: Use your own methods to verify an existing user is |  | ||||||
| 		// able to refresh their token and then give them a new one |  | ||||||
| 		userObj, _ := user.GetByEmail("jc@jc21.com") |  | ||||||
| 		if response, err := njwt.Generate(&userObj, false); err != nil { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) |  | ||||||
| 		} else { |  | ||||||
| 			h.ResultResponseJSON(w, r, http.StatusOK, response) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // NewSSEToken will generate and return a very short lived token for |  | ||||||
| // use by the /sse/* endpoint. It requires an app token to generate this |  | ||||||
| // Route: POST /tokens/sse |  | ||||||
| func NewSSEToken() func(http.ResponseWriter, *http.Request) { |  | ||||||
| 	return func(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 		userID := r.Context().Value(c.UserIDCtxKey).(uint) |  | ||||||
|  |  | ||||||
| 		// Find user |  | ||||||
| 		userObj, userErr := user.GetByID(userID) |  | ||||||
| 		if userErr != nil { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if userObj.IsDisabled { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil) |  | ||||||
| 			return |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		if response, err := njwt.Generate(&userObj, true); err != nil { |  | ||||||
| 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil) |  | ||||||
| 		} else { |  | ||||||
| 			h.ResultResponseJSON(w, r, http.StatusOK, response) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| @@ -3,6 +3,7 @@ package handler | |||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	c "npm/internal/api/context" | 	c "npm/internal/api/context" | ||||||
| 	h "npm/internal/api/http" | 	h "npm/internal/api/http" | ||||||
| @@ -17,6 +18,13 @@ import ( | |||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | type setAuthModel struct { | ||||||
|  | 	// The json tags are required, as the change password form decodes into this object | ||||||
|  | 	Type          string `json:"type"` | ||||||
|  | 	Secret        string `json:"secret"` | ||||||
|  | 	CurrentSecret string `json:"current_secret"` | ||||||
|  | } | ||||||
|  |  | ||||||
| // GetUsers returns all users | // GetUsers returns all users | ||||||
| // Route: GET /users | // Route: GET /users | ||||||
| func GetUsers() func(http.ResponseWriter, *http.Request) { | func GetUsers() func(http.ResponseWriter, *http.Request) { | ||||||
| @@ -188,7 +196,7 @@ func CreateUser() func(http.ResponseWriter, *http.Request) { | |||||||
| 		// newUser has been saved, now save their auth | 		// newUser has been saved, now save their auth | ||||||
| 		if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 { | 		if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 { | ||||||
| 			newUser.Auth.UserID = newUser.ID | 			newUser.Auth.UserID = newUser.ID | ||||||
| 			if newUser.Auth.Type == auth.TypePassword { | 			if newUser.Auth.Type == auth.TypeLocal { | ||||||
| 				err = newUser.Auth.SetPassword(newUser.Auth.Secret) | 				err = newUser.Auth.SetPassword(newUser.Auth.Secret) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					logger.Error("SetPasswordError", err) | 					logger.Error("SetPasswordError", err) | ||||||
| @@ -247,3 +255,79 @@ func getUserIDFromRequest(r *http.Request) (uint, bool, error) { | |||||||
| 	} | 	} | ||||||
| 	return userID, self, nil | 	return userID, self, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // SetAuth sets a auth method. This can be used for "me" and `2` for example | ||||||
|  | // Route: POST /users/:userID/auth | ||||||
|  | func SetAuth() func(http.ResponseWriter, *http.Request) { | ||||||
|  | 	return func(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 		bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) | ||||||
|  |  | ||||||
|  | 		var newAuth setAuthModel | ||||||
|  | 		err := json.Unmarshal(bodyBytes, &newAuth) | ||||||
|  | 		if err != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		userID, isSelf, userIDErr := getUserIDFromRequest(r) | ||||||
|  | 		if userIDErr != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Load user | ||||||
|  | 		thisUser, thisUserErr := user.GetByID(userID) | ||||||
|  | 		if thisUserErr == gorm.ErrRecordNotFound { | ||||||
|  | 			h.NotFound(w, r) | ||||||
|  | 			return | ||||||
|  | 		} else if thisUserErr != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if thisUser.IsSystem { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Load existing auth for user | ||||||
|  | 		userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type) | ||||||
|  | 		if userAuthErr != nil { | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if isSelf { | ||||||
|  | 			// confirm that the current_secret given is valid for the one stored in the database | ||||||
|  | 			validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret) | ||||||
|  | 			if validateErr != nil { | ||||||
|  | 				logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error()) | ||||||
|  | 				// Sleep for 1 second to prevent brute force password guessing | ||||||
|  | 				time.Sleep(time.Second) | ||||||
|  |  | ||||||
|  | 				h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if newAuth.Type == auth.TypeLocal { | ||||||
|  | 			err := userAuth.SetPassword(newAuth.Secret) | ||||||
|  | 			if err != nil { | ||||||
|  | 				logger.Error("SetPasswordError", err) | ||||||
|  | 				h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if err = userAuth.Save(); err != nil { | ||||||
|  | 			logger.Error("AuthSaveError", err) | ||||||
|  | 			h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		userAuth.Secret = "" | ||||||
|  |  | ||||||
|  | 		// todo: add to audit-log | ||||||
|  |  | ||||||
|  | 		h.ResultResponseJSON(w, r, http.StatusOK, userAuth) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -4,9 +4,10 @@ import ( | |||||||
| 	"io" | 	"io" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/http/httptest" | 	"net/http/httptest" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
| 	"npm/internal/entity/user" | 	"npm/internal/entity/user" | ||||||
| 	"npm/internal/model" | 	"npm/internal/model" | ||||||
| 	"testing" |  | ||||||
|  |  | ||||||
| 	"github.com/qri-io/jsonschema" | 	"github.com/qri-io/jsonschema" | ||||||
| 	"github.com/stretchr/testify/assert" | 	"github.com/stretchr/testify/assert" | ||||||
| @@ -36,9 +37,8 @@ func TestResultResponseJSON(t *testing.T) { | |||||||
| 				ModelBase: model.ModelBase{ID: 10}, | 				ModelBase: model.ModelBase{ID: 10}, | ||||||
| 				Email:     "me@example.com", | 				Email:     "me@example.com", | ||||||
| 				Name:      "John Doe", | 				Name:      "John Doe", | ||||||
| 				Nickname:  "Jonny", |  | ||||||
| 			}, | 			}, | ||||||
| 			want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"nickname\":\"Jonny\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}", | 			want: "{\"result\":{\"id\":10,\"created_at\":0,\"updated_at\":0,\"name\":\"John Doe\",\"email\":\"me@example.com\",\"is_disabled\":false,\"gravatar_url\":\"\"}}", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			name:   "error response", | 			name:   "error response", | ||||||
|   | |||||||
| @@ -74,12 +74,13 @@ func applyRoutes(r chi.Router) chi.Router { | |||||||
| 		r.With(middleware.EnforceSetup(), middleware.Enforce()). | 		r.With(middleware.EnforceSetup(), middleware.Enforce()). | ||||||
| 			Get("/config", handler.Config()) | 			Get("/config", handler.Config()) | ||||||
|  |  | ||||||
| 		// Tokens | 		// Auth | ||||||
| 		r.With(middleware.EnforceSetup()).Route("/tokens", func(r chi.Router) { | 		r.With(middleware.EnforceSetup()).Route("/auth", func(r chi.Router) { | ||||||
|  | 			r.Get("/", handler.GetAuthConfig()) | ||||||
| 			r.With(middleware.EnforceRequestSchema(schema.GetToken())). | 			r.With(middleware.EnforceRequestSchema(schema.GetToken())). | ||||||
| 				Post("/", handler.NewToken()) | 				Post("/", handler.NewToken()) | ||||||
| 			r.With(middleware.Enforce()). | 			r.With(middleware.Enforce()). | ||||||
| 				Get("/", handler.RefreshToken()) | 				Post("/refresh", handler.RefreshToken()) | ||||||
| 			r.With(middleware.Enforce()). | 			r.With(middleware.Enforce()). | ||||||
| 				Post("/sse", handler.NewSSEToken()) | 				Post("/sse", handler.NewSSEToken()) | ||||||
| 		}) | 		}) | ||||||
|   | |||||||
| @@ -64,6 +64,9 @@ const anyType = ` | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"type": "integer" | 			"type": "integer" | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"type": "string" | ||||||
| 		} | 		} | ||||||
| 	] | 	] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ func CreateUser() string { | |||||||
| 			], | 			], | ||||||
| 			"properties": { | 			"properties": { | ||||||
| 				"name": %s, | 				"name": %s, | ||||||
| 				"nickname": %s, |  | ||||||
| 				"email": %s, | 				"email": %s, | ||||||
| 				"is_disabled": { | 				"is_disabled": { | ||||||
| 					"type": "boolean" | 					"type": "boolean" | ||||||
| @@ -30,7 +29,7 @@ func CreateUser() string { | |||||||
| 					"properties": { | 					"properties": { | ||||||
| 						"type": { | 						"type": { | ||||||
| 							"type": "string", | 							"type": "string", | ||||||
| 							"pattern": "^password$" | 							"pattern": "^local$" | ||||||
| 						}, | 						}, | ||||||
| 						"secret": %s | 						"secret": %s | ||||||
| 					} | 					} | ||||||
| @@ -38,5 +37,5 @@ func CreateUser() string { | |||||||
| 				"capabilities": %s | 				"capabilities": %s | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), stringMinMax(8, 255), capabilties()) | 	`, stringMinMax(2, 50), stringMinMax(5, 150), stringMinMax(8, 255), capabilties()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ func GetToken() string { | |||||||
| 			"properties": { | 			"properties": { | ||||||
| 				"type": { | 				"type": { | ||||||
| 					"type": "string", | 					"type": "string", | ||||||
| 					"pattern": "^password$" | 					"enum": ["local", "ldap", "oidc"] | ||||||
| 				}, | 				}, | ||||||
| 				"identity": %s, | 				"identity": %s, | ||||||
| 				"secret": %s | 				"secret": %s | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package schema | |||||||
| import "fmt" | import "fmt" | ||||||
|  |  | ||||||
| // SetAuth is the schema for incoming data validation | // SetAuth is the schema for incoming data validation | ||||||
|  | // Only local auth is supported for setting a password | ||||||
| func SetAuth() string { | func SetAuth() string { | ||||||
| 	return fmt.Sprintf(` | 	return fmt.Sprintf(` | ||||||
| 		{ | 		{ | ||||||
| @@ -15,7 +16,7 @@ func SetAuth() string { | |||||||
| 			"properties": { | 			"properties": { | ||||||
| 				"type": { | 				"type": { | ||||||
| 					"type": "string", | 					"type": "string", | ||||||
| 					"pattern": "^password$" | 					"pattern": "^local$" | ||||||
| 				}, | 				}, | ||||||
| 				"secret": %s, | 				"secret": %s, | ||||||
| 				"current_secret": %s | 				"current_secret": %s | ||||||
|   | |||||||
| @@ -11,7 +11,6 @@ func UpdateUser() string { | |||||||
| 			"minProperties": 1, | 			"minProperties": 1, | ||||||
| 			"properties": { | 			"properties": { | ||||||
| 				"name": %s, | 				"name": %s, | ||||||
| 				"nickname": %s, |  | ||||||
| 				"email": %s, | 				"email": %s, | ||||||
| 				"is_disabled": { | 				"is_disabled": { | ||||||
| 					"type": "boolean" | 					"type": "boolean" | ||||||
| @@ -19,5 +18,5 @@ func UpdateUser() string { | |||||||
| 				"capabilities": %s | 				"capabilities": %s | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), capabilties()) | 	`, stringMinMax(2, 50), stringMinMax(5, 150), capabilties()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ func (s *testsuite) SetupTest() { | |||||||
| 	}).AddRow( | 	}).AddRow( | ||||||
| 		10, | 		10, | ||||||
| 		100, | 		100, | ||||||
| 		TypePassword, | 		TypeLocal, | ||||||
| 		"abc123", | 		"abc123", | ||||||
| 	) | 	) | ||||||
| } | } | ||||||
| @@ -54,7 +54,7 @@ func TestExampleTestSuite(t *testing.T) { | |||||||
| func assertModel(t *testing.T, m Model) { | func assertModel(t *testing.T, m Model) { | ||||||
| 	assert.Equal(t, uint(10), m.ID) | 	assert.Equal(t, uint(10), m.ID) | ||||||
| 	assert.Equal(t, uint(100), m.UserID) | 	assert.Equal(t, uint(100), m.UserID) | ||||||
| 	assert.Equal(t, TypePassword, m.Type) | 	assert.Equal(t, TypeLocal, m.Type) | ||||||
| 	assert.Equal(t, "abc123", m.Secret) | 	assert.Equal(t, "abc123", m.Secret) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -83,10 +83,10 @@ func (s *testsuite) TestGetByUserIDType() { | |||||||
|  |  | ||||||
| 	s.mock. | 	s.mock. | ||||||
| 		ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE user_id = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)). | 		ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "auth" WHERE user_id = $1 AND type = $2 AND "auth"."is_deleted" = $3 ORDER BY "auth"."id" LIMIT $4`)). | ||||||
| 		WithArgs(100, TypePassword, 0, 1). | 		WithArgs(100, TypeLocal, 0, 1). | ||||||
| 		WillReturnRows(s.singleRow) | 		WillReturnRows(s.singleRow) | ||||||
|  |  | ||||||
| 	m, err := GetByUserIDType(100, TypePassword) | 	m, err := GetByUserIDType(100, TypeLocal) | ||||||
| 	require.NoError(s.T(), err) | 	require.NoError(s.T(), err) | ||||||
| 	require.NoError(s.T(), s.mock.ExpectationsWereMet()) | 	require.NoError(s.T(), s.mock.ExpectationsWereMet()) | ||||||
| 	assertModel(s.T(), m) | 	assertModel(s.T(), m) | ||||||
| @@ -103,7 +103,7 @@ func (s *testsuite) TestSave() { | |||||||
| 			sqlmock.AnyArg(), | 			sqlmock.AnyArg(), | ||||||
| 			0, | 			0, | ||||||
| 			100, | 			100, | ||||||
| 			TypePassword, | 			TypeLocal, | ||||||
| 			"abc123", | 			"abc123", | ||||||
| 		). | 		). | ||||||
| 		WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) | 		WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("11")) | ||||||
| @@ -112,7 +112,7 @@ func (s *testsuite) TestSave() { | |||||||
| 	// New model | 	// New model | ||||||
| 	m := Model{ | 	m := Model{ | ||||||
| 		UserID: 100, | 		UserID: 100, | ||||||
| 		Type:   TypePassword, | 		Type:   TypeLocal, | ||||||
| 		Secret: "abc123", | 		Secret: "abc123", | ||||||
| 	} | 	} | ||||||
| 	err := m.Save() | 	err := m.Save() | ||||||
| @@ -127,7 +127,7 @@ func (s *testsuite) TestSetPassword() { | |||||||
| 	m := Model{UserID: 100} | 	m := Model{UserID: 100} | ||||||
| 	err := m.SetPassword("abc123") | 	err := m.SetPassword("abc123") | ||||||
| 	require.NoError(s.T(), err) | 	require.NoError(s.T(), err) | ||||||
| 	assert.Equal(s.T(), TypePassword, m.Type) | 	assert.Equal(s.T(), TypeLocal, m.Type) | ||||||
| 	assert.Greater(s.T(), len(m.Secret), 15) | 	assert.Greater(s.T(), len(m.Secret), 15) | ||||||
|  |  | ||||||
| } | } | ||||||
| @@ -143,10 +143,10 @@ func (s *testsuite) TestValidateSecret() { | |||||||
| 	require.NoError(s.T(), err) | 	require.NoError(s.T(), err) | ||||||
| 	err = m.ValidateSecret("this is not the password") | 	err = m.ValidateSecret("this is not the password") | ||||||
| 	assert.NotNil(s.T(), err) | 	assert.NotNil(s.T(), err) | ||||||
| 	assert.Equal(s.T(), "Invalid Password", err.Error()) | 	assert.Equal(s.T(), "Invalid Credentials", err.Error()) | ||||||
|  |  | ||||||
| 	m.Type = "not a valid type" | 	m.Type = "not a valid type" | ||||||
| 	err = m.ValidateSecret("abc123") | 	err = m.ValidateSecret("abc123") | ||||||
| 	assert.NotNil(s.T(), err) | 	assert.NotNil(s.T(), err) | ||||||
| 	assert.Equal(s.T(), "Could not validate Secret, auth type is not a Password", err.Error()) | 	assert.Equal(s.T(), "Could not validate Secret, auth type is not Local", err.Error()) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										96
									
								
								backend/internal/entity/auth/ldap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								backend/internal/entity/auth/ldap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | |||||||
|  | package auth | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"npm/internal/entity/setting" | ||||||
|  | 	"npm/internal/logger" | ||||||
|  |  | ||||||
|  | 	ldap3 "github.com/go-ldap/ldap/v3" | ||||||
|  | 	"github.com/rotisserie/eris" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // LDAPUser is the LDAP User | ||||||
|  | type LDAPUser struct { | ||||||
|  | 	Username string `json:"username"` | ||||||
|  | 	Name     string `json:"name"` | ||||||
|  | 	Email    string `json:"email"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // LDAPAuthenticate will use ldap to authenticate with user/pass | ||||||
|  | func LDAPAuthenticate(identity, password string) (*LDAPUser, error) { | ||||||
|  | 	ldapSettings, err := setting.GetLDAPSettings() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	dn := strings.Replace(ldapSettings.UserDN, "{{USERNAME}}", identity, 1) | ||||||
|  | 	conn, err := ldapConnect(ldapSettings.Host, dn, password) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	// nolint: errcheck, gosec | ||||||
|  | 	defer conn.Close() | ||||||
|  | 	return ldapSearchUser(conn, ldapSettings, identity) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Attempt ldap connection | ||||||
|  | func ldapConnect(host, dn, password string) (*ldap3.Conn, error) { | ||||||
|  | 	var conn *ldap3.Conn | ||||||
|  | 	var err error | ||||||
|  |  | ||||||
|  | 	if conn, err = ldap3.DialURL(fmt.Sprintf("ldap://%s", host)); err != nil { | ||||||
|  | 		logger.Error("LdapError", err) | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.Debug("LDAP Logging in with: %s", dn) | ||||||
|  | 	if err := conn.Bind(dn, password); err != nil { | ||||||
|  | 		if !strings.Contains(err.Error(), "Invalid Credentials") { | ||||||
|  | 			logger.Error("LDAPAuthError", err) | ||||||
|  | 		} | ||||||
|  | 		// nolint: gosec, errcheck | ||||||
|  | 		conn.Close() | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger.Debug("LDAP Login Successful") | ||||||
|  | 	return conn, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func ldapSearchUser(l *ldap3.Conn, ldapSettings setting.LDAPSettings, username string) (*LDAPUser, error) { | ||||||
|  | 	// Search for the given username | ||||||
|  | 	searchRequest := ldap3.NewSearchRequest( | ||||||
|  | 		ldapSettings.BaseDN, | ||||||
|  | 		ldap3.ScopeWholeSubtree, | ||||||
|  | 		ldap3.NeverDerefAliases, | ||||||
|  | 		0, | ||||||
|  | 		0, | ||||||
|  | 		false, | ||||||
|  | 		strings.Replace(ldapSettings.SelfFilter, "{{USERNAME}}", username, 1), | ||||||
|  | 		nil, // []string{"name"}, | ||||||
|  | 		nil, | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	sr, err := l.Search(searchRequest) | ||||||
|  | 	if err != nil { | ||||||
|  | 		logger.Error("LdapError", err) | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(sr.Entries) < 1 { | ||||||
|  | 		return nil, eris.New("No user found in LDAP search") | ||||||
|  | 	} else if len(sr.Entries) > 1 { | ||||||
|  | 		j, _ := json.Marshal(sr) | ||||||
|  | 		logger.Debug("LDAP Search Results: %s", j) | ||||||
|  | 		return nil, eris.Errorf("Too many LDAP results returned in LDAP search: %d", len(sr.Entries)) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &LDAPUser{ | ||||||
|  | 		Username: strings.ToLower(username), | ||||||
|  | 		Name:     sr.Entries[0].GetAttributeValue(ldapSettings.NameProperty), | ||||||
|  | 		Email:    strings.ToLower(sr.Entries[0].GetAttributeValue(ldapSettings.EmailProperty)), | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
| @@ -11,7 +11,7 @@ func GetByID(id int) (Model, error) { | |||||||
| 	return m, err | 	return m, err | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetByUserIDType finds a user by email | // GetByUserIDType finds a user by id and type | ||||||
| func GetByUserIDType(userID uint, authType string) (Model, error) { | func GetByUserIDType(userID uint, authType string) (Model, error) { | ||||||
| 	var auth Model | 	var auth Model | ||||||
| 	db := database.GetDB() | 	db := database.GetDB() | ||||||
| @@ -21,3 +21,14 @@ func GetByUserIDType(userID uint, authType string) (Model, error) { | |||||||
| 		First(&auth) | 		First(&auth) | ||||||
| 	return auth, result.Error | 	return auth, result.Error | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // GetByUserIDType finds a user by id and type | ||||||
|  | func GetByIdenityType(identity string, authType string) (Model, error) { | ||||||
|  | 	var auth Model | ||||||
|  | 	db := database.GetDB() | ||||||
|  | 	result := db. | ||||||
|  | 		Where("identity = ?", identity). | ||||||
|  | 		Where("type = ?", authType). | ||||||
|  | 		First(&auth) | ||||||
|  | 	return auth, result.Error | ||||||
|  | } | ||||||
|   | |||||||
| @@ -8,16 +8,19 @@ import ( | |||||||
| 	"golang.org/x/crypto/bcrypt" | 	"golang.org/x/crypto/bcrypt" | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | // Auth types | ||||||
| const ( | const ( | ||||||
| 	// TypePassword is the Password Type | 	TypeLocal = "local" | ||||||
| 	TypePassword = "password" | 	TypeLDAP  = "ldap" | ||||||
|  | 	TypeOIDC  = "oidc" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // Model is the model | // Model is the model | ||||||
| type Model struct { | type Model struct { | ||||||
| 	model.ModelBase | 	model.ModelBase | ||||||
| 	UserID   uint   `json:"user_id" gorm:"column:user_id"` | 	UserID   uint   `json:"user_id" gorm:"column:user_id"` | ||||||
| 	Type   string `json:"type" gorm:"column:type;default:password"` | 	Type     string `json:"type" gorm:"column:type;default:local"` | ||||||
|  | 	Identity string `json:"identity,omitempty" gorm:"column:identity"` | ||||||
| 	Secret   string `json:"secret,omitempty" gorm:"column:secret"` | 	Secret   string `json:"secret,omitempty" gorm:"column:secret"` | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -48,7 +51,7 @@ func (m *Model) SetPassword(password string) error { | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	m.Type = TypePassword | 	m.Type = TypeLocal | ||||||
| 	m.Secret = string(hash) | 	m.Secret = string(hash) | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -56,13 +59,13 @@ func (m *Model) SetPassword(password string) error { | |||||||
|  |  | ||||||
| // ValidateSecret will check if a given secret matches the encrypted secret | // ValidateSecret will check if a given secret matches the encrypted secret | ||||||
| func (m *Model) ValidateSecret(secret string) error { | func (m *Model) ValidateSecret(secret string) error { | ||||||
| 	if m.Type != TypePassword { | 	if m.Type != TypeLocal { | ||||||
| 		return eris.New("Could not validate Secret, auth type is not a Password") | 		return eris.New("Could not validate Secret, auth type is not Local") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret)) | 	err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return eris.New("Invalid Password") | 		return eris.New("Invalid Credentials") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								backend/internal/entity/setting/auth_methods.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								backend/internal/entity/setting/auth_methods.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | package setting | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GetAuthMethods returns the authentication methods enabled for this site | ||||||
|  | func GetAuthMethods() ([]string, error) { | ||||||
|  | 	var l []string | ||||||
|  | 	var m Model | ||||||
|  | 	if err := m.LoadByName("auth-methods"); err != nil { | ||||||
|  | 		return l, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := json.Unmarshal([]byte(m.Value.String()), &l); err != nil { | ||||||
|  | 		return l, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return l, nil | ||||||
|  | } | ||||||
							
								
								
									
										32
									
								
								backend/internal/entity/setting/ldap.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								backend/internal/entity/setting/ldap.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | package setting | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"encoding/json" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // LDAPSettings are the settings for LDAP that come from | ||||||
|  | // the `ldap-auth` setting value | ||||||
|  | type LDAPSettings struct { | ||||||
|  | 	Host           string `json:"host"` | ||||||
|  | 	BaseDN         string `json:"base_dn"` | ||||||
|  | 	UserDN         string `json:"user_dn"` | ||||||
|  | 	EmailProperty  string `json:"email_property"` | ||||||
|  | 	NameProperty   string `json:"name_property"` | ||||||
|  | 	SelfFilter     string `json:"self_filter"` | ||||||
|  | 	AutoCreateUser bool   `json:"auto_create_user"` | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // GetLDAPSettings will return the LDAP settings | ||||||
|  | func GetLDAPSettings() (LDAPSettings, error) { | ||||||
|  | 	var l LDAPSettings | ||||||
|  | 	var m Model | ||||||
|  | 	if err := m.LoadByName("ldap-auth"); err != nil { | ||||||
|  | 		return l, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := json.Unmarshal([]byte(m.Value.String()), &l); err != nil { | ||||||
|  | 		return l, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return l, nil | ||||||
|  | } | ||||||
| @@ -41,14 +41,12 @@ func (s *testsuite) SetupTest() { | |||||||
| 	s.singleRow = sqlmock.NewRows([]string{ | 	s.singleRow = sqlmock.NewRows([]string{ | ||||||
| 		"id", | 		"id", | ||||||
| 		"name", | 		"name", | ||||||
| 		"nickname", |  | ||||||
| 		"email", | 		"email", | ||||||
| 		"is_disabled", | 		"is_disabled", | ||||||
| 		"is_system", | 		"is_system", | ||||||
| 	}).AddRow( | 	}).AddRow( | ||||||
| 		10, | 		10, | ||||||
| 		"John Doe", | 		"John Doe", | ||||||
| 		"Jonny", |  | ||||||
| 		"jon@example.com", | 		"jon@example.com", | ||||||
| 		false, | 		false, | ||||||
| 		false, | 		false, | ||||||
| @@ -74,14 +72,12 @@ func (s *testsuite) SetupTest() { | |||||||
| 	s.listRows = sqlmock.NewRows([]string{ | 	s.listRows = sqlmock.NewRows([]string{ | ||||||
| 		"id", | 		"id", | ||||||
| 		"name", | 		"name", | ||||||
| 		"nickname", |  | ||||||
| 		"email", | 		"email", | ||||||
| 		"is_disabled", | 		"is_disabled", | ||||||
| 		"is_system", | 		"is_system", | ||||||
| 	}).AddRow( | 	}).AddRow( | ||||||
| 		10, | 		10, | ||||||
| 		"John Doe", | 		"John Doe", | ||||||
| 		"Jonny", |  | ||||||
| 		"jon@example.com", | 		"jon@example.com", | ||||||
| 		false, | 		false, | ||||||
| 		false, | 		false, | ||||||
| @@ -104,7 +100,6 @@ func TestExampleTestSuite(t *testing.T) { | |||||||
| func assertModel(t *testing.T, m Model) { | func assertModel(t *testing.T, m Model) { | ||||||
| 	assert.Equal(t, uint(10), m.ID) | 	assert.Equal(t, uint(10), m.ID) | ||||||
| 	assert.Equal(t, "John Doe", m.Name) | 	assert.Equal(t, "John Doe", m.Name) | ||||||
| 	assert.Equal(t, "Jonny", m.Nickname) |  | ||||||
| 	assert.Equal(t, "jon@example.com", m.Email) | 	assert.Equal(t, "jon@example.com", m.Email) | ||||||
| 	assert.Equal(t, false, m.IsDisabled) | 	assert.Equal(t, false, m.IsDisabled) | ||||||
| 	assert.Equal(t, false, m.IsSystem) | 	assert.Equal(t, false, m.IsSystem) | ||||||
| @@ -182,7 +177,7 @@ func (s *testsuite) TestSave() { | |||||||
| 		WillReturnRows(s.singleRow) | 		WillReturnRows(s.singleRow) | ||||||
|  |  | ||||||
| 	s.mock.ExpectBegin() | 	s.mock.ExpectBegin() | ||||||
| 	s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "user" ("created_at","updated_at","is_deleted","name","nickname","email","is_disabled","is_system") VALUES ($1,$2,$3,$4,$5,$6,$7,$8) RETURNING "id"`)). | 	s.mock.ExpectQuery(regexp.QuoteMeta(`INSERT INTO "user" ("created_at","updated_at","is_deleted","name","email","is_disabled","is_system") VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING "id"`)). | ||||||
| 		WithArgs( | 		WithArgs( | ||||||
| 			sqlmock.AnyArg(), | 			sqlmock.AnyArg(), | ||||||
| 			sqlmock.AnyArg(), | 			sqlmock.AnyArg(), | ||||||
| @@ -199,7 +194,6 @@ func (s *testsuite) TestSave() { | |||||||
| 	// New model, as system | 	// New model, as system | ||||||
| 	m := Model{ | 	m := Model{ | ||||||
| 		Name:     "John Doe", | 		Name:     "John Doe", | ||||||
| 		Nickname: "Jonny", |  | ||||||
| 		Email:    "JON@example.com", // mixed case on purpose | 		Email:    "JON@example.com", // mixed case on purpose | ||||||
| 		IsSystem: true, | 		IsSystem: true, | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -2,8 +2,10 @@ package user | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"npm/internal/database" | 	"npm/internal/database" | ||||||
| 	"npm/internal/entity" | 	"npm/internal/entity" | ||||||
|  | 	"npm/internal/entity/auth" | ||||||
| 	"npm/internal/logger" | 	"npm/internal/logger" | ||||||
| 	"npm/internal/model" | 	"npm/internal/model" | ||||||
| ) | ) | ||||||
| @@ -104,3 +106,14 @@ func GetCapabilities(userID uint) ([]string, error) { | |||||||
| 	} | 	} | ||||||
| 	return capabilities, nil | 	return capabilities, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // CreateFromLDAPUser will create a user from an LDAP user object | ||||||
|  | func CreateFromLDAPUser(ldapUser *auth.LDAPUser) (Model, error) { | ||||||
|  | 	user := Model{ | ||||||
|  | 		Email: ldapUser.Email, | ||||||
|  | 		Name:  ldapUser.Name, | ||||||
|  | 	} | ||||||
|  | 	err := user.Save() | ||||||
|  | 	user.generateGravatar() | ||||||
|  | 	return user, err | ||||||
|  | } | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ import ( | |||||||
| type Model struct { | type Model struct { | ||||||
| 	model.ModelBase | 	model.ModelBase | ||||||
| 	Name       string `json:"name" gorm:"column:name" filter:"name,string"` | 	Name       string `json:"name" gorm:"column:name" filter:"name,string"` | ||||||
| 	Nickname   string `json:"nickname" gorm:"column:nickname" filter:"nickname,string"` |  | ||||||
| 	Email      string `json:"email" gorm:"column:email" filter:"email,email"` | 	Email      string `json:"email" gorm:"column:email" filter:"email,email"` | ||||||
| 	IsDisabled bool   `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"` | 	IsDisabled bool   `json:"is_disabled" gorm:"column:is_disabled" filter:"is_disabled,boolean"` | ||||||
| 	IsSystem   bool   `json:"is_system,omitempty" gorm:"column:is_system" filter:"is_system,boolean"` | 	IsSystem   bool   `json:"is_system,omitempty" gorm:"column:is_system" filter:"is_system,boolean"` | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ var ( | |||||||
| 	ErrDatabaseUnavailable    = eris.New("database-unavailable") | 	ErrDatabaseUnavailable    = eris.New("database-unavailable") | ||||||
| 	ErrDuplicateEmailUser     = eris.New("email-already-exists") | 	ErrDuplicateEmailUser     = eris.New("email-already-exists") | ||||||
| 	ErrInvalidLogin           = eris.New("invalid-login-credentials") | 	ErrInvalidLogin           = eris.New("invalid-login-credentials") | ||||||
|  | 	ErrInvalidAuthType        = eris.New("invalid-auth-type") | ||||||
| 	ErrUserDisabled           = eris.New("user-disabled") | 	ErrUserDisabled           = eris.New("user-disabled") | ||||||
| 	ErrSystemUserReadonly     = eris.New("cannot-save-system-users") | 	ErrSystemUserReadonly     = eris.New("cannot-save-system-users") | ||||||
| 	ErrValidationFailed       = eris.New("request-failed-validation") | 	ErrValidationFailed       = eris.New("request-failed-validation") | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ type Filter struct { | |||||||
| 	Value    []string `json:"value"` | 	Value    []string `json:"value"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // FilterMapValue ... | // FilterMapValue is the structure of a filter map value | ||||||
| type FilterMapValue struct { | type FilterMapValue struct { | ||||||
| 	Type   string | 	Type   string | ||||||
| 	Field  string | 	Field  string | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ type Sort struct { | |||||||
| 	Direction string `json:"direction"` | 	Direction string `json:"direction"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // GetSort ... | // GetSort is the sort array | ||||||
| func (p *PageInfo) GetSort(def Sort) []Sort { | func (p *PageInfo) GetSort(def Sort) []Sort { | ||||||
| 	if p.Sort == nil { | 	if p.Sort == nil { | ||||||
| 		return []Sort{def} | 		return []Sort{def} | ||||||
|   | |||||||
| @@ -18,7 +18,7 @@ func TestPageInfoGetSort(t *testing.T) { | |||||||
| 		Direction: "asc", | 		Direction: "asc", | ||||||
| 	} | 	} | ||||||
| 	defined := Sort{ | 	defined := Sort{ | ||||||
| 		Field:     "nickname", | 		Field:     "email", | ||||||
| 		Direction: "desc", | 		Direction: "desc", | ||||||
| 	} | 	} | ||||||
| 	// default | 	// default | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ func TestGetFilterSchema(t *testing.T) { | |||||||
| 		ID          uint      `json:"id" gorm:"column:user_id" filter:"id,number"` | 		ID          uint      `json:"id" gorm:"column:user_id" filter:"id,number"` | ||||||
| 		Created     time.Time `json:"created" gorm:"column:user_created_date" filter:"created,date"` | 		Created     time.Time `json:"created" gorm:"column:user_created_date" filter:"created,date"` | ||||||
| 		Name        string    `json:"name" gorm:"column:user_name" filter:"name,string"` | 		Name        string    `json:"name" gorm:"column:user_name" filter:"name,string"` | ||||||
| 		NickName    string    `json:"nickname" gorm:"column:user_nickname" filter:"nickname"` |  | ||||||
| 		IsDisabled  string    `json:"is_disabled" gorm:"column:user_is_disabled" filter:"is_disabled,bool"` | 		IsDisabled  string    `json:"is_disabled" gorm:"column:user_is_disabled" filter:"is_disabled,bool"` | ||||||
| 		Permissions string    `json:"permissions" gorm:"column:user_permissions" filter:"permissions,regex"` | 		Permissions string    `json:"permissions" gorm:"column:user_permissions" filter:"permissions,regex"` | ||||||
| 		History     string    `json:"history" gorm:"column:user_history" filter:"history,regex,(id|name)"` | 		History     string    `json:"history" gorm:"column:user_history" filter:"history,regex,(id|name)"` | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| 	"version": "3.0.0", | 	"version": "3.0.0", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| 	"scripts": { | 	"scripts": { | ||||||
| 		"dev": "vite", | 		"dev": "vite --host", | ||||||
| 		"build": "tsc && vite build", | 		"build": "tsc && vite build", | ||||||
| 		"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | 		"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", | ||||||
| 		"lint:ci": "yarn lint --watchAll=false", | 		"lint:ci": "yarn lint --watchAll=false", | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ export async function getSSEToken( | |||||||
| ): Promise<TokenResponse> { | ): Promise<TokenResponse> { | ||||||
| 	const { result } = await api.post( | 	const { result } = await api.post( | ||||||
| 		{ | 		{ | ||||||
| 			url: "/tokens/sse", | 			url: "/auth/sse", | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 		abortController, | ||||||
| 	); | 	); | ||||||
|   | |||||||
| @@ -15,7 +15,7 @@ export async function getToken( | |||||||
| ): Promise<TokenResponse> { | ): Promise<TokenResponse> { | ||||||
| 	const { result } = await api.post( | 	const { result } = await api.post( | ||||||
| 		{ | 		{ | ||||||
| 			url: "/tokens", | 			url: "/auth", | ||||||
| 			data: payload, | 			data: payload, | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 		abortController, | ||||||
|   | |||||||
| @@ -4,9 +4,9 @@ import { TokenResponse } from "./responseTypes"; | |||||||
| export async function refreshToken( | export async function refreshToken( | ||||||
| 	abortController?: AbortController, | 	abortController?: AbortController, | ||||||
| ): Promise<TokenResponse> { | ): Promise<TokenResponse> { | ||||||
| 	const { result } = await api.get( | 	const { result } = await api.post( | ||||||
| 		{ | 		{ | ||||||
| 			url: "/tokens", | 			url: "/auth/refresh", | ||||||
| 		}, | 		}, | ||||||
| 		abortController, | 		abortController, | ||||||
| 	); | 	); | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import { ReactNode, createContext, useContext, useState } from "react"; | import { createContext, ReactNode, useContext, useState } from "react"; | ||||||
|  |  | ||||||
| import { useQueryClient } from "@tanstack/react-query"; | import { useQueryClient } from "@tanstack/react-query"; | ||||||
| import { useIntervalWhen } from "rooks"; | import { useIntervalWhen } from "rooks"; | ||||||
| @@ -9,7 +9,7 @@ import AuthStore from "src/modules/AuthStore"; | |||||||
| // Context | // Context | ||||||
| export interface AuthContextType { | export interface AuthContextType { | ||||||
| 	authenticated: boolean; | 	authenticated: boolean; | ||||||
| 	login: (username: string, password: string) => Promise<void>; | 	login: (type: string, username: string, password: string) => Promise<void>; | ||||||
| 	logout: () => void; | 	logout: () => void; | ||||||
| 	token?: string; | 	token?: string; | ||||||
| } | } | ||||||
| @@ -36,8 +36,7 @@ function AuthProvider({ | |||||||
| 		setAuthenticated(true); | 		setAuthenticated(true); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const login = async (identity: string, secret: string) => { | 	const login = async (type: string, identity: string, secret: string) => { | ||||||
| 		const type = "password"; |  | ||||||
| 		const response = await getToken({ payload: { type, identity, secret } }); | 		const response = await getToken({ payload: { type, identity, secret } }); | ||||||
| 		handleTokenUpdate(response); | 		handleTokenUpdate(response); | ||||||
| 	}; | 	}; | ||||||
|   | |||||||
| @@ -5,16 +5,16 @@ import { | |||||||
| 	FormLabel, | 	FormLabel, | ||||||
| 	Input, | 	Input, | ||||||
| 	Modal, | 	Modal, | ||||||
| 	ModalOverlay, |  | ||||||
| 	ModalContent, |  | ||||||
| 	ModalHeader, |  | ||||||
| 	ModalCloseButton, |  | ||||||
| 	ModalBody, | 	ModalBody, | ||||||
|  | 	ModalCloseButton, | ||||||
|  | 	ModalContent, | ||||||
| 	ModalFooter, | 	ModalFooter, | ||||||
|  | 	ModalHeader, | ||||||
|  | 	ModalOverlay, | ||||||
| 	Stack, | 	Stack, | ||||||
| 	useToast, | 	useToast, | ||||||
| } from "@chakra-ui/react"; | } from "@chakra-ui/react"; | ||||||
| import { Formik, Form, Field } from "formik"; | import { Field, Form, Formik } from "formik"; | ||||||
|  |  | ||||||
| import { setAuth } from "src/api/npm"; | import { setAuth } from "src/api/npm"; | ||||||
| import { PrettyButton } from "src/components"; | import { PrettyButton } from "src/components"; | ||||||
| @@ -43,7 +43,7 @@ function ChangePasswordModal({ isOpen, onClose }: ChangePasswordModalProps) { | |||||||
|  |  | ||||||
| 		try { | 		try { | ||||||
| 			await setAuth("me", { | 			await setAuth("me", { | ||||||
| 				type: "password", | 				type: "local", | ||||||
| 				secret: payload.password, | 				secret: payload.password, | ||||||
| 				currentSecret: payload.current, | 				currentSecret: payload.current, | ||||||
| 			}); | 			}); | ||||||
|   | |||||||
| @@ -5,16 +5,16 @@ import { | |||||||
| 	FormLabel, | 	FormLabel, | ||||||
| 	Input, | 	Input, | ||||||
| 	Modal, | 	Modal, | ||||||
| 	ModalOverlay, |  | ||||||
| 	ModalContent, |  | ||||||
| 	ModalHeader, |  | ||||||
| 	ModalCloseButton, |  | ||||||
| 	ModalBody, | 	ModalBody, | ||||||
|  | 	ModalCloseButton, | ||||||
|  | 	ModalContent, | ||||||
| 	ModalFooter, | 	ModalFooter, | ||||||
|  | 	ModalHeader, | ||||||
|  | 	ModalOverlay, | ||||||
| 	Stack, | 	Stack, | ||||||
| 	useToast, | 	useToast, | ||||||
| } from "@chakra-ui/react"; | } from "@chakra-ui/react"; | ||||||
| import { Formik, Form, Field } from "formik"; | import { Field, Form, Formik } from "formik"; | ||||||
|  |  | ||||||
| import { setAuth } from "src/api/npm"; | import { setAuth } from "src/api/npm"; | ||||||
| import { PrettyButton } from "src/components"; | import { PrettyButton } from "src/components"; | ||||||
| @@ -44,7 +44,7 @@ function SetPasswordModal({ userId, isOpen, onClose }: SetPasswordModalProps) { | |||||||
|  |  | ||||||
| 		try { | 		try { | ||||||
| 			await setAuth(userId, { | 			await setAuth(userId, { | ||||||
| 				type: "password", | 				type: "local", | ||||||
| 				secret: payload.password, | 				secret: payload.password, | ||||||
| 			}); | 			}); | ||||||
| 			onClose(); | 			onClose(); | ||||||
|   | |||||||
| @@ -7,22 +7,22 @@ import { | |||||||
| 	FormLabel, | 	FormLabel, | ||||||
| 	Input, | 	Input, | ||||||
| 	Modal, | 	Modal, | ||||||
| 	ModalOverlay, |  | ||||||
| 	ModalContent, |  | ||||||
| 	ModalHeader, |  | ||||||
| 	ModalCloseButton, |  | ||||||
| 	ModalBody, | 	ModalBody, | ||||||
|  | 	ModalCloseButton, | ||||||
|  | 	ModalContent, | ||||||
| 	ModalFooter, | 	ModalFooter, | ||||||
|  | 	ModalHeader, | ||||||
|  | 	ModalOverlay, | ||||||
| 	Stack, | 	Stack, | ||||||
| 	Tab, | 	Tab, | ||||||
| 	Tabs, |  | ||||||
| 	TabList, | 	TabList, | ||||||
| 	TabPanel, | 	TabPanel, | ||||||
| 	TabPanels, | 	TabPanels, | ||||||
|  | 	Tabs, | ||||||
| 	useToast, | 	useToast, | ||||||
| } from "@chakra-ui/react"; | } from "@chakra-ui/react"; | ||||||
| import { useQueryClient } from "@tanstack/react-query"; | import { useQueryClient } from "@tanstack/react-query"; | ||||||
| import { Formik, Form, Field } from "formik"; | import { Field, Form, Formik } from "formik"; | ||||||
|  |  | ||||||
| import { createUser } from "src/api/npm"; | import { createUser } from "src/api/npm"; | ||||||
| import { | import { | ||||||
| @@ -56,7 +56,7 @@ function UserCreateModal({ isOpen, onClose }: UserCreateModalProps) { | |||||||
| 			...{ | 			...{ | ||||||
| 				isDisabled: false, | 				isDisabled: false, | ||||||
| 				auth: { | 				auth: { | ||||||
| 					type: "password", | 					type: "local", | ||||||
| 					secret: values.password, | 					secret: values.password, | ||||||
| 				}, | 				}, | ||||||
| 				capabilities, | 				capabilities, | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| import { useEffect, useRef } from "react"; | import { useEffect, useRef } from "react"; | ||||||
|  |  | ||||||
| import { | import { | ||||||
|  | 	Box, | ||||||
| 	Center, | 	Center, | ||||||
| 	Flex, | 	Flex, | ||||||
| 	Box, |  | ||||||
| 	FormControl, | 	FormControl, | ||||||
| 	FormErrorMessage, | 	FormErrorMessage, | ||||||
| 	FormLabel, | 	FormLabel, | ||||||
| @@ -12,7 +12,7 @@ import { | |||||||
| 	useColorModeValue, | 	useColorModeValue, | ||||||
| 	useToast, | 	useToast, | ||||||
| } from "@chakra-ui/react"; | } from "@chakra-ui/react"; | ||||||
| import { Formik, Form, Field } from "formik"; | import { Field, Form, Formik } from "formik"; | ||||||
|  |  | ||||||
| import { LocalePicker, PrettyButton, ThemeSwitcher } from "src/components"; | import { LocalePicker, PrettyButton, ThemeSwitcher } from "src/components"; | ||||||
| import { useAuthState } from "src/context"; | import { useAuthState } from "src/context"; | ||||||
| @@ -40,7 +40,7 @@ function Login() { | |||||||
| 		}; | 		}; | ||||||
|  |  | ||||||
| 		try { | 		try { | ||||||
| 			await login(values.email, values.password); | 			await login("local", values.email, values.password); | ||||||
| 		} catch (err) { | 		} catch (err) { | ||||||
| 			if (err instanceof Error) { | 			if (err instanceof Error) { | ||||||
| 				showErr(err.message); | 				showErr(err.message); | ||||||
|   | |||||||
| @@ -7,14 +7,14 @@ import { | |||||||
| 	FormControl, | 	FormControl, | ||||||
| 	FormErrorMessage, | 	FormErrorMessage, | ||||||
| 	FormLabel, | 	FormLabel, | ||||||
| 	Stack, |  | ||||||
| 	Heading, | 	Heading, | ||||||
| 	Input, | 	Input, | ||||||
|  | 	Stack, | ||||||
| 	useColorModeValue, | 	useColorModeValue, | ||||||
| 	useToast, | 	useToast, | ||||||
| } from "@chakra-ui/react"; | } from "@chakra-ui/react"; | ||||||
| import { useQueryClient } from "@tanstack/react-query"; | import { useQueryClient } from "@tanstack/react-query"; | ||||||
| import { Formik, Form, Field } from "formik"; | import { Field, Form, Formik } from "formik"; | ||||||
|  |  | ||||||
| import { createUser } from "src/api/npm"; | import { createUser } from "src/api/npm"; | ||||||
| // import logo from "src/assets/logo-256.png"; | // import logo from "src/assets/logo-256.png"; | ||||||
| @@ -42,7 +42,7 @@ function Setup() { | |||||||
| 			...{ | 			...{ | ||||||
| 				isDisabled: false, | 				isDisabled: false, | ||||||
| 				auth: { | 				auth: { | ||||||
| 					type: "password", | 					type: "local", | ||||||
| 					secret: values.password, | 					secret: values.password, | ||||||
| 				}, | 				}, | ||||||
| 				capabilities: ["full-admin"], | 				capabilities: ["full-admin"], | ||||||
| @@ -65,7 +65,7 @@ function Setup() { | |||||||
| 			const response = await createUser(payload); | 			const response = await createUser(payload); | ||||||
| 			if (response && typeof response.id !== "undefined" && response.id) { | 			if (response && typeof response.id !== "undefined" && response.id) { | ||||||
| 				try { | 				try { | ||||||
| 					await login(response.email, password); | 					await login("local", response.email, password); | ||||||
| 					// Trigger a Health change | 					// Trigger a Health change | ||||||
| 					await queryClient.refetchQueries({ queryKey: ["health"] }); | 					await queryClient.refetchQueries({ queryKey: ["health"] }); | ||||||
| 					// window.location.reload(); | 					// window.location.reload(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user