mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 05:52:25 -05:00 
			
		
		
		
	
				commit
				
					
						995e61bb07
					
				
			
		
					 24 changed files with 1247 additions and 135 deletions
				
			
		
							
								
								
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -3,3 +3,6 @@ | |||
| 
 | ||||
| # exclude built documentation, since readthedocs will build it for us anyway | ||||
| /docs/_build | ||||
| 
 | ||||
| # exclude coverage report | ||||
| cp.out | ||||
|  |  | |||
							
								
								
									
										208
									
								
								PROGRESS.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								PROGRESS.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,208 @@ | |||
| # Progress | ||||
| 
 | ||||
| * [ ] Client-To-Server (Client REST API) | ||||
|   * [ ] Token and sign-in | ||||
|     * [x] /api/v1/apps POST                                 (Create an application) | ||||
|     * [ ] /api/v1/apps/verify_credentials GET               (Verify an application works) | ||||
|     * [x] /oauth/authorize GET                              (Show authorize page to user) | ||||
|     * [x] /oauth/authorize POST                             (Get an oauth access code for an app/user) | ||||
|     * [ ] /oauth/token POST                                 (Obtain a user-level access token) | ||||
|     * [ ] /oauth/revoke POST                                (Revoke a user-level access token) | ||||
|     * [x] /auth/sign_in GET                                 (Show form for user signin) | ||||
|     * [x] /auth/sign_in POST                                (Validate username and password and sign user in) | ||||
|   * [ ] Accounts | ||||
|     * [ ] /api/v1/accounts POST                             (Register a new account) | ||||
|     * [ ] /api/v1/accounts/verify_credentials GET           (Verify account credentials with a user token) | ||||
|     * [ ] /api/v1/accounts/update_credentials PATCH         (Update user's display name/preferences) | ||||
|     * [ ] /api/v1/accounts/:id GET                          (Get account information) | ||||
|     * [ ] /api/v1/accounts/:id/statuses GET                 (Get an account's statuses) | ||||
|     * [ ] /api/v1/accounts/:id/followers GET                (Get an account's followers) | ||||
|     * [ ] /api/v1/accounts/:id/following GET                (Get an account's following) | ||||
|     * [ ] /api/v1/accounts/:id/featured_tags GET            (Get an account's featured tags) | ||||
|     * [ ] /api/v1/accounts/:id/lists GET                    (Get lists containing this account) | ||||
|     * [ ] /api/v1/accounts/:id/identity_proofs GET          (Get identity proofs for this account) | ||||
|     * [ ] /api/v1/accounts/:id/follow POST                  (Follow this account) | ||||
|     * [ ] /api/v1/accounts/:id/unfollow POST                (Unfollow this account) | ||||
|     * [ ] /api/v1/accounts/:id/block POST                   (Block this account) | ||||
|     * [ ] /api/v1/accounts/:id/unblock POST                 (Unblock this account) | ||||
|     * [ ] /api/v1/accounts/:id/mute POST                    (Mute this account) | ||||
|     * [ ] /api/v1/accounts/:id/unmute POST                  (Unmute this account) | ||||
|     * [ ] /api/v1/accounts/:id/pin POST                     (Feature this account on profile) | ||||
|     * [ ] /api/v1/accounts/:id/unpin POST                   (Remove this account from profile) | ||||
|     * [ ] /api/v1/accounts/:id/note POST                    (Make a personal note about this account) | ||||
|     * [ ] /api/v1/accounts/relationships GET                (Check relationships with accounts) | ||||
|     * [ ] /api/v1/accounts/search GET                       (Search for an account) | ||||
|   * [ ] Bookmarks | ||||
|     * [ ] /api/v1/bookmarks GET                             (See bookmarked statuses) | ||||
|   * [ ] Favourites | ||||
|     * [ ] /api/v1/favourites GET                            (See faved statuses) | ||||
|   * [ ] Mutes | ||||
|     * [ ] /api/v1/mutes GET                                 (See list of muted accounts) | ||||
|   * [ ] Blocks | ||||
|     * [ ] /api/v1/blocks GET                                (See list of blocked accounts) | ||||
|   * [ ] Domain Blocks | ||||
|     * [ ] /api/v1/domain_blocks GET                         (See list of domain blocks) | ||||
|     * [ ] /api/v1/domain_blocks POST                        (Create a domain block) | ||||
|     * [ ] /api/v1/domain_blocks DELETE                      (Remove a domain block) | ||||
|   * [ ] Filters | ||||
|     * [ ] /api/v1/filters GET                               (Get list of filters) | ||||
|     * [ ] /api/v1/filters/:id GET                           (View a filter) | ||||
|     * [ ] /api/v1/filters POST                              (Create a filter) | ||||
|     * [ ] /api/v1/filters/:id PUT                           (Update a filter) | ||||
|     * [ ] /api/v1/filters/:id DELETE                        (Remove a filter) | ||||
|   * [ ] Reports | ||||
|     * [ ] /api/v1/reports POST                              (File a report) | ||||
|   * [ ] Follow Requests | ||||
|     * [ ] /api/v1/follow_requests GET                       (View pending follow requests) | ||||
|     * [ ] /api/v1/follow_requests/:id/authorize POST        (Accept a follow request) | ||||
|     * [ ] /api/v1/follow_requests/:id/reject POST           (Reject a follow request) | ||||
|   * [ ] Endorsements | ||||
|     * [ ] /api/v1/endorsements GET                          (View existing endorsements) | ||||
|   * [ ] Featured Tags | ||||
|     * [ ] /api/v1/featured_tags GET                         (View featured tags) | ||||
|     * [ ] /api/v1/featured_tags POST                        (Feature a tag) | ||||
|     * [ ] /api/v1/featured_tags/:id DELETE                  (Unfeature a tag) | ||||
|     * [ ] /api/v1/featured_tags/suggestions GET             (See most used tags) | ||||
|   * [ ] Preferences | ||||
|     * [ ] /api/v1/preferences GET                           (Get user preferences) | ||||
|   * [ ] Suggestions | ||||
|     * [ ] /api/v1/suggestions GET                           (Get suggested accounts to follow) | ||||
|     * [ ] /api/v1/suggestions/:account_id DELETE            (Delete a suggestion) | ||||
|   * [ ] Statuses | ||||
|     * [ ] /api/v1/statuses POST                             (Create a new status) | ||||
|     * [ ] /api/v1/statuses/:id GET                          (View an existing status) | ||||
|     * [ ] /api/v1/statuses/:id DELETE                       (Delete a status) | ||||
|     * [ ] /api/v1/statuses/:id/context GET                  (View statuses above and below status ID) | ||||
|     * [ ] /api/v1/statuses/:id/reblogged_by GET             (See who has reblogged a status) | ||||
|     * [ ] /api/v1/statuses/:id/favourited_by GET            (See who has faved a status) | ||||
|     * [ ] /api/v1/statuses/:id/favourite POST               (Fave a status) | ||||
|     * [ ] /api/v1/statuses/:id/favourite POST               (Unfave a status) | ||||
|     * [ ] /api/v1/statuses/:id/reblog POST                  (Reblog a status) | ||||
|     * [ ] /api/v1/statuses/:id/unreblog POST                (Undo a reblog) | ||||
|     * [ ] /api/v1/statuses/:id/bookmark POST                (Bookmark a status) | ||||
|     * [ ] /api/v1/statuses/:id/unbookmark POST              (Undo a bookmark) | ||||
|     * [ ] /api/v1/statuses/:id/mute POST                    (Mute notifications on a status) | ||||
|     * [ ] /api/v1/statuses/:id/unmute POST                  (Unmute notifications on a status) | ||||
|     * [ ] /api/v1/statuses/:id/pin POST                     (Pin a status to profile) | ||||
|     * [ ] /api/v1/statuses/:id/unpin POST                   (Unpin a status from profile) | ||||
|   * [ ] Media | ||||
|     * [ ] /api/v1/media POST                                (Upload a media attachment) | ||||
|     * [ ] /api/v1/media/:id GET                             (Get a media attachment) | ||||
|     * [ ] /api/v1/media/:id PUT                             (Update an attachment) | ||||
|   * [ ] Polls | ||||
|     * [ ] /api/v1/polls/:id GET                             (Show a poll) | ||||
|     * [ ] /api/v1/polls/:id/votes POST                      (Vote on a poll) | ||||
|   * [ ] Scheduled Statuses | ||||
|     * [ ] /api/v1/scheduled_statuses GET                    (View scheduled statuses) | ||||
|     * [ ] /api/v1/scheduled_statuses/:id GET                (View a scheduled status) | ||||
|     * [ ] /api/v1/scheduled_statuses/:id PUT                (Schedule a status) | ||||
|     * [ ] /api/v1/scheduled_statuses/:id DELETE             (Cancel a scheduled status) | ||||
|   * [ ] Timelines | ||||
|     * [ ] /api/v1/timelines/public GET                      (See the public/federated timeline) | ||||
|     * [ ] /api/v1/timelines/tag/:hashtag GET                (Get public statuses that use hashtag) | ||||
|     * [ ] /api/v1/timelines/home GET                        (View statuses from followed users) | ||||
|     * [ ] /api/v1/timelines/list/:list_id GET               (Get statuses in given list) | ||||
|   * [ ] Conversations | ||||
|     * [ ] /api/v1/conversations GET                         (Get a list of direct message convos) | ||||
|     * [ ] /api/v1/conversations/:id DELETE                  (Delete a direct message convo) | ||||
|     * [ ] /api/v1/conversations/:id POST                    (Mark a conversation as read) | ||||
|   * [ ] Lists | ||||
|     * [ ] /api/v1/lists GET                                 (Show a list of lists) | ||||
|     * [ ] /api/v1/lists/:id GET                             (Show a single list) | ||||
|     * [ ] /api/v1/lists POST                                (Create a new list) | ||||
|     * [ ] /api/v1/lists/:id PUT                             (Update a list) | ||||
|     * [ ] /api/v1/lists/:id DELETE                          (Delete a list) | ||||
|     * [ ] /api/v1/lists/:id/accounts GET                    (View which accounts are in a list) | ||||
|     * [ ] /api/v1/lists/:id/accounts POST                   (Add accounts to a list) | ||||
|     * [ ] /api/v1/lists/:id/accounts DELETE                 (Remove accounts from a list) | ||||
|   * [ ] Markers | ||||
|     * [ ] /api/v1/markers GET                               (Get saved timeline position) | ||||
|     * [ ] /api/v1/markers POST                              (Save timeline position) | ||||
|   * [ ] Streaming | ||||
|     * [ ] /api/v1/streaming WEBSOCKETS                      (Stream live events to user via websockets) | ||||
|   * [ ] Notifications | ||||
|     * [ ] /api/v1/notifications GET                         (Get list of notifications) | ||||
|     * [ ] /api/v1/notifications/:id GET                     (Get a single notification) | ||||
|     * [ ] /api/v1/notifications/clear POST                  (Clear all notifications) | ||||
|     * [ ] /api/v1/notifications/:id POST                    (Clear a single notification) | ||||
|   * [ ] Push | ||||
|     * [ ] /api/v1/push/subscription POST                    (Subscribe to push notifications) | ||||
|     * [ ] /api/v1/push/subscription GET                     (Get current subscription) | ||||
|     * [ ] /api/v1/push/subscription PUT                     (Change notification types) | ||||
|     * [ ] /api/v1/push/subscription DELETE                  (Delete current subscription) | ||||
|   * [ ] Search | ||||
|     * [ ] /api/v2/search GET                                (Get search query results) | ||||
|   * [ ] Instance | ||||
|     * [ ] /api/v1/instance GET                              (Get instance information) | ||||
|     * [ ] /api/v1/instance PATCH                            (Update instance information) | ||||
|     * [ ] /api/v1/instance/peers GET                        (Get list of federated servers) | ||||
|     * [ ] /api/v1/instance/activity GET                     (Instance activity over the last 3 months, binned weekly.) | ||||
|   * [ ] Trends | ||||
|     * [ ] /api/v1/trends GET                                (Get a list of trending tags for the last week) | ||||
|   * [ ] Directory | ||||
|     * [ ] /api/v1/directory GET                             (Show profiles this server is aware of.) | ||||
|   * [ ] Custom Emojis | ||||
|     * [ ] /api/v1/custom_emojis GET                         (Show this server's custom emoji) | ||||
|   * [ ] Admin | ||||
|     * [ ] /api/v1/admin/accounts GET                        (View accounts filtered by criteria) | ||||
|     * [ ] /api/v1/admin/accounts/:id GET                    (View admin level info about an account) | ||||
|     * [ ] /api/v1/admin/accounts/:id/action POST            (Perform an admin action on account) | ||||
|     * [ ] /api/v1/admin/accounts/:id/approve POST           (Approve pending account) | ||||
|     * [ ] /api/v1/admin/accounts/:id/reject POST            (Deny pending account) | ||||
|     * [ ] /api/v1/admin/accounts/:id/enable POST            (Reenable a disabled account) | ||||
|     * [ ] /api/v1/admin/accounts/:id/unsilence POST         (Unsilence a silenced account) | ||||
|     * [ ] /api/v1/admin/accounts/:id/unsuspend POST         (Unsuspend a suspended account) | ||||
|     * [ ] /api/v1/admin/reports GET                         (View all reports) | ||||
|     * [ ] /api/v1/admin/reports/:id GET                     (View a single report) | ||||
|     * [ ] /api/v1/admin/reports/:id/assign_to_self POST     (Assign a report to the current admin account) | ||||
|     * [ ] /api/v1/admin/reports/:id/unassign POST           (Unassign a report) | ||||
|     * [ ] /api/v1/admin/reports/:id/resolve POST            (Mark a report as resolved) | ||||
|     * [ ] /api/v1/admin/reports/:id/reopen POST             (Reopen a closed report) | ||||
|   * [ ] Announcements | ||||
|     * [ ] /api/v1/announcements GET                         (Show all current announcements) | ||||
|     * [ ] /api/v1/announcements/:id/dismiss POST            (Mark an announcement as read) | ||||
|     * [ ] /api/v1/announcements/:id/reactions/:name PUT     (Add a reaction to an announcement) | ||||
|     * [ ] /api/v1/announcements/:id/reactions/:name DELETE  (Remove a reaction from an announcement) | ||||
|   * [ ] Proofs | ||||
|     * [ ] /api/proofs GET                                   (View identity proofs) | ||||
|   * [ ] Oembed | ||||
|     * [ ] /api/oembed GET                                   (Get oembed metadata for a status URL) | ||||
| * [ ] Server-To-Server (Federation protocol) | ||||
|   * [ ] Mechanism to trigger side effects from client AP | ||||
|   * [ ] Federation modes | ||||
|     * [ ] 'Slow' federation | ||||
|       * [ ] Reputation scoring system for instances | ||||
|     * [ ] 'Greedy' federation | ||||
|     * [ ] No federation (insulate this instance from the Fediverse) | ||||
|       * [ ] Allowlist | ||||
| * [ ] Storage | ||||
|   * [x] Internal/statuses/preferences etc | ||||
|     * [x] Postgres interface | ||||
|   * [ ] Media storage | ||||
|     * [ ] Local storage interface | ||||
|     * [ ] S3 storage interface | ||||
| * [ ] Cache | ||||
|   * [ ] In-memory cache | ||||
| * [ ] Security features | ||||
|   * [ ] Authorization middleware | ||||
|   * [ ] Rate limiting middleware | ||||
|   * [ ] Scope middleware | ||||
|   * [ ] Permissions/acl middleware for admins+moderators | ||||
| * [ ] Documentation | ||||
|   * [ ] Swagger API documentation | ||||
|   * [ ] ReadTheDocs.io documentation | ||||
|   * [ ] Deployment documentation | ||||
|   * [ ] App creation guide | ||||
| * [ ] Tooling | ||||
|   * [ ] Database migration tool | ||||
|   * [ ] Admin CLI tool | ||||
| * [ ] Build | ||||
|   * [ ] Docker containerization | ||||
|     * [ ] Dockerfile | ||||
|     * [ ] docker-compose.yml | ||||
| * [ ] Tests | ||||
|   * [ ] Unit/integration | ||||
|     * [ ] 25% coverage | ||||
|     * [ ] 50% coverage | ||||
|     * [ ] 90%+ coverage | ||||
|   * [ ] Benchmarking | ||||
|  | @ -1,6 +1,6 @@ | |||
| # GoToSocial | ||||
| 
 | ||||
| <img src="https://img.shields.io/liberapay/patrons/dumpsterqueer.svg?logo=liberapay"> <img src="https://img.shields.io/liberapay/receives/dumpsterqueer.svg?logo=liberapay"> | ||||
|   | ||||
| 
 | ||||
| Federated social media software. | ||||
| 
 | ||||
|  | @ -30,9 +30,13 @@ Among other things: | |||
| * Easily-configurable character limit. | ||||
| * Groups and group posting. | ||||
| 
 | ||||
| ## Implementation Status | ||||
| 
 | ||||
| For an up-to-date view on progress made towards a v1.0.0 release, see [here](./PROGRESS.md). | ||||
| 
 | ||||
| ## Contact | ||||
| 
 | ||||
| For questions and comments, you can reach out to Tobi on the Fediverse <a rel="me" href="https://ondergrond.org/@dumpsterqueer">here</a> or mail admin@gotosocial.org. | ||||
| For questions and comments, you can reach out to Tobi on the Fediverse [here](https://ondergrond.org/@dumpsterqueer) or mail admin@gotosocial.org. | ||||
| 
 | ||||
| ## Sponsorship | ||||
| 
 | ||||
|  |  | |||
|  | @ -95,6 +95,14 @@ func main() { | |||
| 				Value:   "postgres", | ||||
| 				EnvVars: []string{envNames.DbDatabase}, | ||||
| 			}, | ||||
| 
 | ||||
| 			// TEMPLATE FLAGS | ||||
| 			&cli.StringFlag{ | ||||
| 				Name:    flagNames.TemplateBaseDir, | ||||
| 				Usage:   "Basedir for html templating files for rendering pages and composing emails", | ||||
| 				Value:   "./web/template/", | ||||
| 				EnvVars: []string{envNames.TemplateBaseDir}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		Commands: []*cli.Command{ | ||||
| 			{ | ||||
|  | @ -137,12 +145,12 @@ func main() { | |||
| func runAction(c *cli.Context, a action.GTSAction) error { | ||||
| 
 | ||||
| 	// create a new *config.Config based on the config path provided... | ||||
| 	conf, err := config.New(c.String(config.GetFlagNames().ConfigPath)) | ||||
| 	conf, err := config.FromFile(c.String(config.GetFlagNames().ConfigPath)) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error creating config: %s", err) | ||||
| 	} | ||||
| 	// ... and the flags set on the *cli.Context by urfave | ||||
| 	conf.ParseFlags(c) | ||||
| 	conf.ParseCLIFlags(c) | ||||
| 
 | ||||
| 	// create a logger with the log level, formatting, and output splitter already set | ||||
| 	log, err := log.New(conf.LogLevel) | ||||
|  |  | |||
|  | @ -60,3 +60,10 @@ db: | |||
|   # Examples: ["mydb","postgres","gotosocial"] | ||||
|   # Default: "postgres" | ||||
|   database: "postgres" | ||||
| 
 | ||||
| # Config pertaining to templating of web pages/email notifications and the like | ||||
| template: | ||||
|   # String. Directory from which gotosocial will attempt to load html templates (.tmpl files). | ||||
|   # Examples: ["/some/absolute/path/", "./relative/path/", "../../some/weird/path/"] | ||||
|   # Default: "./web/template/" | ||||
|   baseDir: "./web/template/" | ||||
|  |  | |||
							
								
								
									
										5
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										5
									
								
								go.mod
									
										
									
									
									
								
							|  | @ -3,13 +3,14 @@ module github.com/gotosocial/gotosocial | |||
| go 1.16 | ||||
| 
 | ||||
| require ( | ||||
| 	github.com/gin-contrib/sessions v0.0.3 | ||||
| 	github.com/gin-gonic/gin v1.6.3 | ||||
| 	github.com/go-fed/activity v1.0.0 | ||||
| 	github.com/go-pg/pg/extra/pgdebug v0.2.0 | ||||
| 	github.com/go-pg/pg/v10 v10.8.0 | ||||
| 	github.com/golang/mock v1.4.4 // indirect | ||||
| 	github.com/google/uuid v1.2.0 // indirect | ||||
| 	github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57 | ||||
| 	github.com/google/uuid v1.2.0 | ||||
| 	github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3 | ||||
| 	github.com/onsi/ginkgo v1.15.0 // indirect | ||||
| 	github.com/onsi/gomega v1.10.5 // indirect | ||||
| 	github.com/sirupsen/logrus v1.8.0 | ||||
|  |  | |||
							
								
								
									
										31
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										31
									
								
								go.sum
									
										
									
									
									
								
							|  | @ -4,6 +4,9 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= | |||
| github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= | ||||
| github.com/andybalholm/brotli v1.0.0 h1:7UCwP93aiSfvWpapti8g88vVVGp2qqtGyePsSuDafo4= | ||||
| github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= | ||||
| github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= | ||||
| github.com/bradfitz/gomemcache v0.0.0-20190329173943-551aad21a668/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= | ||||
| github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI= | ||||
| github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= | ||||
| github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= | ||||
| github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= | ||||
|  | @ -24,10 +27,14 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo | |||
| github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= | ||||
| github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= | ||||
| github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= | ||||
| github.com/gin-contrib/sessions v0.0.3 h1:PoBXki+44XdJdlgDqDrY5nDVe3Wk7wDV/UCOuLP6fBI= | ||||
| github.com/gin-contrib/sessions v0.0.3/go.mod h1:8C/J6cad3Il1mWYYgtw0w+hqasmpvy25mPkXdOgeB9I= | ||||
| github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= | ||||
| github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= | ||||
| github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= | ||||
| github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= | ||||
| github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= | ||||
| github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= | ||||
| github.com/go-fed/activity v1.0.0 h1:j7w3auHZnVCjUcgA1mE+UqSOjFBhvW2Z2res3vNol+o= | ||||
| github.com/go-fed/activity v1.0.0/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q= | ||||
| github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5 h1:WLvFZqoXnuVTBKA6U/1FnEHNQ0Rq0QM0rGhY8Tx6R1g= | ||||
|  | @ -41,8 +48,10 @@ github.com/go-pg/zerochecker v0.2.0 h1:pp7f72c3DobMWOb2ErtZsnrPaSvHd2W4o9//8HtF4 | |||
| github.com/go-pg/zerochecker v0.2.0/go.mod h1:NJZ4wKL0NmTtz0GKCoJ8kym6Xn/EQzXRl2OnAe7MmDo= | ||||
| github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= | ||||
| github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= | ||||
| github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= | ||||
| github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= | ||||
| github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= | ||||
| github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= | ||||
| github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= | ||||
| github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= | ||||
| github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= | ||||
|  | @ -68,6 +77,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD | |||
| github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | ||||
| github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= | ||||
| github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= | ||||
| github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= | ||||
| github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= | ||||
| github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
| github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||
|  | @ -84,20 +94,29 @@ github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= | |||
| github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||
| github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= | ||||
| github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= | ||||
| github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= | ||||
| github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= | ||||
| github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= | ||||
| github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= | ||||
| github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= | ||||
| github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU= | ||||
| github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= | ||||
| github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= | ||||
| github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= | ||||
| github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57 h1:+zKsBEkg1cbz7zJDms1KMU9vJBeBAlElS1SbK/x0Rvc= | ||||
| github.com/gotosocial/oauth2/v4 v4.2.1-0.20210315164102-1f7842217e57/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= | ||||
| github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3 h1:CKRz5d7mRum+UMR88Ue33tCYcej14WjUsB59C02DDqY= | ||||
| github.com/gotosocial/oauth2/v4 v4.2.1-0.20210318133800-45d321d259b3/go.mod h1:zl5kwHf/atRUrY5yOyDnk49Us1Ygs0BzdW4jKAgoiP8= | ||||
| github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= | ||||
| github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= | ||||
| github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= | ||||
| 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/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||
| github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= | ||||
| github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= | ||||
| github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= | ||||
| github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= | ||||
| github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= | ||||
| github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= | ||||
| github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= | ||||
| github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I= | ||||
| github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= | ||||
|  | @ -107,13 +126,16 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn | |||
| github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= | ||||
| github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= | ||||
| github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= | ||||
| github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= | ||||
| github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= | ||||
| github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= | ||||
| github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g= | ||||
| github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= | ||||
| github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= | ||||
| github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= | ||||
| github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= | ||||
| github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= | ||||
| github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= | ||||
| github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= | ||||
| github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= | ||||
|  | @ -137,6 +159,8 @@ github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7 | |||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||||
| github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= | ||||
| github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438 h1:jnz/4VenymvySjE+Ez511s0pqVzkUOmr1fwCVytNNWk= | ||||
| github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= | ||||
| github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= | ||||
| github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= | ||||
|  | @ -277,6 +301,7 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h | |||
| golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
| golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
|  | @ -339,6 +364,8 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8 | |||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= | ||||
| gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= | ||||
| gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
|  |  | |||
|  | @ -19,16 +19,19 @@ | |||
| package api | ||||
| 
 | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 
 | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-contrib/sessions/memstore" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/gotosocial/gotosocial/internal/config" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| ) | ||||
| 
 | ||||
| type Server interface { | ||||
| 	AttachHTTPHandler(method string, path string, handler http.HandlerFunc) | ||||
| 	AttachGinHandler(method string, path string, handler gin.HandlerFunc) | ||||
| 	AttachHandler(method string, path string, handler gin.HandlerFunc) | ||||
| 	// AttachMiddleware(handler gin.HandlerFunc) | ||||
| 	GetAPIGroup() *gin.RouterGroup | ||||
| 	Start() | ||||
|  | @ -60,16 +63,22 @@ func (s *server) Stop() { | |||
| 	// todo: shut down gracefully | ||||
| } | ||||
| 
 | ||||
| func (s *server) AttachHTTPHandler(method string, path string, handler http.HandlerFunc) { | ||||
| 	s.engine.Handle(method, path, gin.WrapH(handler)) | ||||
| } | ||||
| 
 | ||||
| func (s *server) AttachGinHandler(method string, path string, handler gin.HandlerFunc) { | ||||
| func (s *server) AttachHandler(method string, path string, handler gin.HandlerFunc) { | ||||
| 	if method == "ANY" { | ||||
| 		s.engine.Any(path, handler) | ||||
| 	} else { | ||||
| 		s.engine.Handle(method, path, handler) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func New(config *config.Config, logger *logrus.Logger) Server { | ||||
| 	engine := gin.New() | ||||
| 	store := memstore.NewStore([]byte("authentication-key"), []byte("encryption-keyencryption-key----")) | ||||
| 	engine.Use(sessions.Sessions("gotosocial-session", store)) | ||||
| 	cwd, _ := os.Getwd() | ||||
| 	tmPath := filepath.Join(cwd, fmt.Sprintf("%s*", config.TemplateConfig.BaseDir)) | ||||
| 	logger.Debugf("loading templates from %s", tmPath) | ||||
| 	engine.LoadHTMLGlob(tmPath) | ||||
| 	return &server{ | ||||
| 		APIGroup: engine.Group("/api").Group("/v1"), | ||||
| 		logger:   logger, | ||||
|  |  | |||
|  | @ -30,22 +30,35 @@ type Config struct { | |||
| 	LogLevel        string          `yaml:"logLevel"` | ||||
| 	ApplicationName string          `yaml:"applicationName"` | ||||
| 	DBConfig        *DBConfig       `yaml:"db"` | ||||
| 	TemplateConfig  *TemplateConfig `yaml:"template"` | ||||
| } | ||||
| 
 | ||||
| // New returns a new config, or an error if something goes amiss. | ||||
| // The path parameter is optional, for loading a configuration json from the given path. | ||||
| func New(path string) (*Config, error) { | ||||
| 	config := &Config{ | ||||
| 		DBConfig: &DBConfig{}, | ||||
| 	} | ||||
| 	if path != "" { | ||||
| 		var err error | ||||
| 		if config, err = loadFromFile(path); err != nil { | ||||
| // FromFile returns a new config from a file, or an error if something goes amiss. | ||||
| func FromFile(path string) (*Config, error) { | ||||
| 	c, err := loadFromFile(path) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("error creating config: %s", err) | ||||
| 	} | ||||
| 	return c, nil | ||||
| } | ||||
| 
 | ||||
| 	return config, nil | ||||
| // Default returns a new config with default values. | ||||
| // Not yet implemented. | ||||
| func Default() *Config { | ||||
| 	// TODO: find a way of doing this without code repetition, because having to | ||||
| 	// repeat all values here and elsewhere is annoying and gonna be prone to mistakes. | ||||
| 	return &Config{ | ||||
| 		DBConfig:       &DBConfig{}, | ||||
| 		TemplateConfig: &TemplateConfig{}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Empty just returns an empty config | ||||
| func Empty() *Config { | ||||
| 	return &Config{ | ||||
| 		DBConfig:       &DBConfig{}, | ||||
| 		TemplateConfig: &TemplateConfig{}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // loadFromFile takes a path to a yaml file and attempts to load a Config object from it | ||||
|  | @ -63,8 +76,8 @@ func loadFromFile(path string) (*Config, error) { | |||
| 	return config, nil | ||||
| } | ||||
| 
 | ||||
| // ParseFlags sets flags on the config using the provided Flags object | ||||
| func (c *Config) ParseFlags(f KeyedFlags) { | ||||
| // ParseCLIFlags sets flags on the config using the provided Flags object | ||||
| func (c *Config) ParseCLIFlags(f KeyedFlags) { | ||||
| 	fn := GetFlagNames() | ||||
| 
 | ||||
| 	// For all of these flags, we only want to set them on the config if: | ||||
|  | @ -108,6 +121,11 @@ func (c *Config) ParseFlags(f KeyedFlags) { | |||
| 	if c.DBConfig.Database == "" || f.IsSet(fn.DbDatabase) { | ||||
| 		c.DBConfig.Database = f.String(fn.DbDatabase) | ||||
| 	} | ||||
| 
 | ||||
| 	// template flags | ||||
| 	if c.TemplateConfig.BaseDir == "" || f.IsSet(fn.TemplateBaseDir) { | ||||
| 		c.TemplateConfig.BaseDir = f.String(fn.TemplateBaseDir) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // KeyedFlags is a wrapper for any type that can store keyed flags and give them back. | ||||
|  | @ -130,6 +148,7 @@ type Flags struct { | |||
| 	DbUser          string | ||||
| 	DbPassword      string | ||||
| 	DbDatabase      string | ||||
| 	TemplateBaseDir string | ||||
| } | ||||
| 
 | ||||
| // GetFlagNames returns a struct containing the names of the various flags used for | ||||
|  | @ -145,6 +164,7 @@ func GetFlagNames() Flags { | |||
| 		DbUser:          "db-user", | ||||
| 		DbPassword:      "db-password", | ||||
| 		DbDatabase:      "db-database", | ||||
| 		TemplateBaseDir: "template-basedir", | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -161,5 +181,6 @@ func GetEnvNames() Flags { | |||
| 		DbUser:          "GTS_DB_USER", | ||||
| 		DbPassword:      "GTS_DB_PASSWORD", | ||||
| 		DbDatabase:      "GTS_DB_DATABASE", | ||||
| 		TemplateBaseDir: "GTS_TEMPLATE_BASEDIR", | ||||
| 	} | ||||
| } | ||||
|  |  | |||
							
								
								
									
										25
									
								
								internal/config/template.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								internal/config/template.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,25 @@ | |||
| /* | ||||
|    GoToSocial | ||||
|    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||
| 
 | ||||
|    This program is free software: you can redistribute it and/or modify | ||||
|    it under the terms of the GNU Affero General Public License as published by | ||||
|    the Free Software Foundation, either version 3 of the License, or | ||||
|    (at your option) any later version. | ||||
| 
 | ||||
|    This program is distributed in the hope that it will be useful, | ||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|    GNU Affero General Public License for more details. | ||||
| 
 | ||||
|    You should have received a copy of the GNU Affero General Public License | ||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package config | ||||
| 
 | ||||
| // TemplateConfig pertains to templating of web pages/email notifications and the like | ||||
| type TemplateConfig struct { | ||||
| 	// Directory from which gotosocial will attempt to load html templates (.tmpl files). | ||||
| 	BaseDir string `yaml:"baseDir"` | ||||
| } | ||||
|  | @ -26,60 +26,130 @@ import ( | |||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // Account represents a GoToSocial user account | ||||
| // Account represents either a local or a remote fediverse account, gotosocial or otherwise (mastodon, pleroma, etc) | ||||
| type Account struct { | ||||
| 	/* | ||||
| 		BASIC INFO | ||||
| 	*/ | ||||
| 
 | ||||
| 	// id of this account in the local database; the end-user will never need to know this, it's strictly internal | ||||
| 	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` | ||||
| 	// Username of the account, should just be a string of [a-z0-9_]. Can be added to domain to create the full username in the form ``[username]@[domain]`` eg., ``user_96@example.org`` | ||||
| 	Username string `pg:",notnull,unique:userdomain"` // username and domain should be unique *with* each other | ||||
| 	// Domain of the account, will be empty if this is a local account, otherwise something like ``example.org`` or ``mastodon.social``. Should be unique with username. | ||||
| 	Domain string `pg:",unique:userdomain"` // username and domain | ||||
| 
 | ||||
| 	/* | ||||
| 		ACCOUNT METADATA | ||||
| 	*/ | ||||
| 
 | ||||
| 	// Avatar image for this account | ||||
| 	Avatar | ||||
| 	// Header image for this account | ||||
| 	Header | ||||
| 	URI                   string | ||||
| 	URL                   string | ||||
| 	ID                    string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` | ||||
| 	Username              string | ||||
| 	Domain                string | ||||
| 	Secret                string | ||||
| 	PrivateKey            string | ||||
| 	PublicKey             string | ||||
| 	RemoteURL             string | ||||
| 	CreatedAt             time.Time `pg:"type:timestamp,notnull"` | ||||
| 	UpdatedAt             time.Time `pg:"type:timestamp,notnull"` | ||||
| 	Note                  string | ||||
| 	// DisplayName for this account. Can be empty, then just the Username will be used for display purposes. | ||||
| 	DisplayName string | ||||
| 	SubscriptionExpiresAt time.Time `pg:"type:timestamp"` | ||||
| 	Locked                bool | ||||
| 	LastWebfingeredAt     time.Time `pg:"type:timestamp"` | ||||
| 	InboxURL              string | ||||
| 	OutboxURL             string | ||||
| 	SharedInboxURL        string | ||||
| 	FollowersURL          string | ||||
| 	Protocol              int | ||||
| 	Memorial              bool | ||||
| 	MovedToAccountID      int | ||||
| 	FeaturedCollectionURL string | ||||
| 	// a key/value map of fields that this account has added to their profile | ||||
| 	Fields map[string]string | ||||
| 	ActorType             string | ||||
| 	// A note that this account has on their profile (ie., the account's bio/description of themselves) | ||||
| 	Note string | ||||
| 	// Is this a memorial account, ie., has the user passed away? | ||||
| 	Memorial bool | ||||
| 	// This account has moved this account id in the database | ||||
| 	MovedToAccountID int | ||||
| 	// When was this account created? | ||||
| 	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||
| 	// When was this account last updated? | ||||
| 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||
| 	// When should this account function until | ||||
| 	SubscriptionExpiresAt time.Time `pg:"type:timestamp"` | ||||
| 
 | ||||
| 	/* | ||||
| 		PRIVACY SETTINGS | ||||
| 	*/ | ||||
| 
 | ||||
| 	// Does this account need an approval for new followers? | ||||
| 	Locked bool | ||||
| 	// Should this account be shown in the instance's profile directory? | ||||
| 	Discoverable bool | ||||
| 
 | ||||
| 	/* | ||||
| 		ACTIVITYPUB THINGS | ||||
| 	*/ | ||||
| 
 | ||||
| 	// What is the activitypub URI for this account discovered by webfinger? | ||||
| 	URI string `pg:",unique"` | ||||
| 	// At which URL can we see the user account in a web browser? | ||||
| 	URL string `pg:",unique"` | ||||
| 	// RemoteURL where this account is located. Will be empty if this is a local account. | ||||
| 	RemoteURL string `pg:",unique"` | ||||
| 	// Last time this account was located using the webfinger API. | ||||
| 	LastWebfingeredAt time.Time `pg:"type:timestamp"` | ||||
| 	// Address of this account's activitypub inbox, for sending activity to | ||||
| 	InboxURL string `pg:",unique"` | ||||
| 	// Address of this account's activitypub outbox | ||||
| 	OutboxURL string `pg:",unique"` | ||||
| 	// Don't support shared inbox right now so this is just a stub for a future implementation | ||||
| 	SharedInboxURL string `pg:",unique"` | ||||
| 	// URL for getting the followers list of this account | ||||
| 	FollowersURL string `pg:",unique"` | ||||
| 	// URL for getting the featured collection list of this account | ||||
| 	FeaturedCollectionURL string `pg:",unique"` | ||||
| 	// What type of activitypub actor is this account? | ||||
| 	ActorType string | ||||
| 	// This account is associated with x account id | ||||
| 	AlsoKnownAs string | ||||
| 	SilencedAt            time.Time `pg:"type:timestamp"` | ||||
| 	SuspendedAt           time.Time `pg:"type:timestamp"` | ||||
| 	TrustLevel            int | ||||
| 	HideCollections       bool | ||||
| 
 | ||||
| 	/* | ||||
| 		CRYPTO FIELDS | ||||
| 	*/ | ||||
| 
 | ||||
| 	Secret string | ||||
| 	// Privatekey for validating activitypub requests, will obviously only be defined for local accounts | ||||
| 	PrivateKey string | ||||
| 	// Publickey for encoding activitypub requests, will be defined for both local and remote accounts | ||||
| 	PublicKey string | ||||
| 
 | ||||
| 	/* | ||||
| 		ADMIN FIELDS | ||||
| 	*/ | ||||
| 
 | ||||
| 	// When was this account set to have all its media shown as sensitive? | ||||
| 	SensitizedAt time.Time `pg:"type:timestamp"` | ||||
| 	// When was this account silenced (eg., statuses only visible to followers, not public)? | ||||
| 	SilencedAt time.Time `pg:"type:timestamp"` | ||||
| 	// When was this account suspended (eg., don't allow it to log in/post, don't accept media/posts from this account) | ||||
| 	SuspendedAt time.Time `pg:"type:timestamp"` | ||||
| 	// How much do we trust this account 🤔 | ||||
| 	TrustLevel int | ||||
| 	// Should we hide this account's collections? | ||||
| 	HideCollections bool | ||||
| 	// id of the user that suspended this account through an admin action | ||||
| 	SuspensionOrigin int | ||||
| } | ||||
| 
 | ||||
| // Avatar represents the avatar for the account for display purposes | ||||
| type Avatar struct { | ||||
| 	// File name of the avatar on local storage | ||||
| 	AvatarFileName string | ||||
| 	// Gif? png? jpeg? | ||||
| 	AvatarContentType string | ||||
| 	AvatarFileSize    int | ||||
| 	AvatarUpdatedAt   *time.Time `pg:"type:timestamp"` | ||||
| 	// Where can we retrieve the avatar? | ||||
| 	AvatarRemoteURL            *url.URL `pg:"type:text"` | ||||
| 	AvatarStorageSchemaVersion int | ||||
| } | ||||
| 
 | ||||
| // Header represents the header of the account for display purposes | ||||
| type Header struct { | ||||
| 	// File name of the header on local storage | ||||
| 	HeaderFileName string | ||||
| 	// Gif? png? jpeg? | ||||
| 	HeaderContentType string | ||||
| 	HeaderFileSize    int | ||||
| 	HeaderUpdatedAt   *time.Time `pg:"type:timestamp"` | ||||
| 	// Where can we retrieve the header? | ||||
| 	HeaderRemoteURL            *url.URL `pg:"type:text"` | ||||
| 	HeaderStorageSchemaVersion int | ||||
| } | ||||
|  |  | |||
							
								
								
									
										30
									
								
								internal/gtsmodel/application.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								internal/gtsmodel/application.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| /* | ||||
|    GoToSocial | ||||
|    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||
| 
 | ||||
|    This program is free software: you can redistribute it and/or modify | ||||
|    it under the terms of the GNU Affero General Public License as published by | ||||
|    the Free Software Foundation, either version 3 of the License, or | ||||
|    (at your option) any later version. | ||||
| 
 | ||||
|    This program is distributed in the hope that it will be useful, | ||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|    GNU Affero General Public License for more details. | ||||
| 
 | ||||
|    You should have received a copy of the GNU Affero General Public License | ||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package gtsmodel | ||||
| 
 | ||||
| type Application struct { | ||||
| 	ID           string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` | ||||
| 	Name         string | ||||
| 	Website      string | ||||
| 	RedirectURI  string `json:"redirect_uri"` | ||||
| 	ClientID     string `json:"client_id"` | ||||
| 	ClientSecret string `json:"client_secret"` | ||||
| 	Scopes       string `json:"scopes"` | ||||
| 	VapidKey     string `json:"vapid_key"` | ||||
| } | ||||
|  | @ -22,11 +22,11 @@ import "time" | |||
| 
 | ||||
| type Status struct { | ||||
| 	ID             string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` | ||||
| 	URI            string | ||||
| 	URL            string | ||||
| 	URI            string `pg:",unique"` | ||||
| 	URL            string `pg:",unique"` | ||||
| 	Content        string | ||||
| 	CreatedAt      time.Time `pg:"type:timestamp,notnull"` | ||||
| 	UpdatedAt      time.Time `pg:"type:timestamp,notnull"` | ||||
| 	CreatedAt      time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||
| 	UpdatedAt      time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||
| 	Local          bool | ||||
| 	AccountID      string | ||||
| 	InReplyToID    string | ||||
|  |  | |||
|  | @ -23,43 +23,98 @@ import ( | |||
| 	"time" | ||||
| ) | ||||
| 
 | ||||
| // User represents an actual human user of gotosocial. Note, this is a LOCAL gotosocial user, not a remote account. | ||||
| // To cross reference this local user with their account (which can be local or remote), use the AccountID field. | ||||
| type User struct { | ||||
| 	ID                     string    `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` | ||||
| 	Email                  string    `pg:",notnull"` | ||||
| 	CreatedAt              time.Time `pg:"type:timestamp,notnull"` | ||||
| 	UpdatedAt              time.Time `pg:"type:timestamp,notnull"` | ||||
| 	/* | ||||
| 		BASIC INFO | ||||
| 	*/ | ||||
| 
 | ||||
| 	// id of this user in the local database; the end-user will never need to know this, it's strictly internal | ||||
| 	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` | ||||
| 	// confirmed email address for this user, this should be unique -- only one email address registered per instance, multiple users per email are not supported | ||||
| 	Email string `pg:",notnull,unique"` | ||||
| 	// The id of the local gtsmodel.Account entry for this user, if it exists (unconfirmed users don't have an account yet) | ||||
| 	AccountID string `pg:"default:'',notnull,unique"` | ||||
| 	// The encrypted password of this user, generated using https://pkg.go.dev/golang.org/x/crypto/bcrypt#GenerateFromPassword. A salt is included so we're safe against 🌈 tables | ||||
| 	EncryptedPassword string `pg:",notnull"` | ||||
| 	ResetPasswordToken     string | ||||
| 	ResetPasswordSentAt    time.Time `pg:"type:timestamp"` | ||||
| 	SignInCount            int | ||||
| 
 | ||||
| 	/* | ||||
| 		USER METADATA | ||||
| 	*/ | ||||
| 
 | ||||
| 	// When was this user created? | ||||
| 	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||
| 	// From what IP was this user created? | ||||
| 	SignUpIP net.IP | ||||
| 	// When was this user updated (eg., password changed, email address changed)? | ||||
| 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||
| 	// When did this user sign in for their current session? | ||||
| 	CurrentSignInAt time.Time `pg:"type:timestamp"` | ||||
| 	LastSignInAt           time.Time `pg:"type:timestamp"` | ||||
| 	// What's the most recent IP of this user | ||||
| 	CurrentSignInIP net.IP | ||||
| 	// When did this user last sign in? | ||||
| 	LastSignInAt time.Time `pg:"type:timestamp"` | ||||
| 	// What's the previous IP of this user? | ||||
| 	LastSignInIP net.IP | ||||
| 	Admin                  bool | ||||
| 	ConfirmationToken      string | ||||
| 	ConfirmedAt            time.Time `pg:"type:timestamp"` | ||||
| 	ConfirmationSentAt     time.Time `pg:"type:timestamp"` | ||||
| 	UnconfirmedEmail       string | ||||
| 	// How many times has this user signed in? | ||||
| 	SignInCount int | ||||
| 	// id of the user who invited this user (who let this guy in?) | ||||
| 	InviteID string | ||||
| 	// What languages does this user want to see? | ||||
| 	ChosenLanguages []string | ||||
| 	// What languages does this user not want to see? | ||||
| 	FilteredLanguages []string | ||||
| 	// In what timezone/locale is this user located? | ||||
| 	Locale string | ||||
| 	// Which application id created this user? See gtsmodel.Application | ||||
| 	CreatedByApplicationID string | ||||
| 	// When did we last contact this user | ||||
| 	LastEmailedAt time.Time `pg:"type:timestamp"` | ||||
| 
 | ||||
| 	/* | ||||
| 		USER CONFIRMATION | ||||
| 	*/ | ||||
| 
 | ||||
| 	// What confirmation token did we send this user/what are we expecting back? | ||||
| 	ConfirmationToken string | ||||
| 	// When did the user confirm their email address | ||||
| 	ConfirmedAt time.Time `pg:"type:timestamp"` | ||||
| 	// When did we send email confirmation to this user? | ||||
| 	ConfirmationSentAt time.Time `pg:"type:timestamp"` | ||||
| 	// Email address that hasn't yet been confirmed | ||||
| 	UnconfirmedEmail string | ||||
| 
 | ||||
| 	/* | ||||
| 		ACL FLAGS | ||||
| 	*/ | ||||
| 
 | ||||
| 	// Is this user a moderator? | ||||
| 	Moderator bool | ||||
| 	// Is this user an admin? | ||||
| 	Admin bool | ||||
| 	// Is this user disabled from posting? | ||||
| 	Disabled bool | ||||
| 	// Has this user been approved by a moderator? | ||||
| 	Approved bool | ||||
| 
 | ||||
| 	/* | ||||
| 		USER SECURITY | ||||
| 	*/ | ||||
| 
 | ||||
| 	// The generated token that the user can use to reset their password | ||||
| 	ResetPasswordToken string | ||||
| 	// When did we email the user their reset-password email? | ||||
| 	ResetPasswordSentAt time.Time `pg:"type:timestamp"` | ||||
| 
 | ||||
| 	EncryptedOTPSecret     string | ||||
| 	EncryptedOTPSecretIv   string | ||||
| 	EncryptedOTPSecretSalt string | ||||
| 	ConsumedTimestamp      int | ||||
| 	OTPRequiredForLogin    bool | ||||
| 	LastEmailedAt          time.Time `pg:"type:timestamp"` | ||||
| 	OTPBackupCodes         []string | ||||
| 	FilteredLanguages      []string | ||||
| 	AccountID              string `pg:",notnull"` | ||||
| 	Disabled               bool | ||||
| 	Moderator              bool | ||||
| 	InviteID               string | ||||
| 	ConsumedTimestamp      int | ||||
| 	RememberToken          string | ||||
| 	ChosenLanguages        []string | ||||
| 	CreatedByApplicationID string | ||||
| 	Approved               bool | ||||
| 	SignInToken            string | ||||
| 	SignInTokenSentAt      time.Time `pg:"type:timestamp"` | ||||
| 	WebauthnID             string | ||||
| 	SignUpIP               net.IP | ||||
| } | ||||
|  |  | |||
							
								
								
									
										9
									
								
								internal/oauth/html.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								internal/oauth/html.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| package oauth | ||||
| 
 | ||||
| const ( | ||||
| 	signInHTML = ` | ||||
| ` | ||||
| 
 | ||||
| 	authorizeHTML = ` | ||||
| ` | ||||
| ) | ||||
|  | @ -19,9 +19,17 @@ | |||
| package oauth | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 
 | ||||
| 	"github.com/gin-contrib/sessions" | ||||
| 	"github.com/gin-gonic/gin" | ||||
| 	"github.com/go-pg/pg/v10" | ||||
| 	"github.com/google/uuid" | ||||
| 	"github.com/gotosocial/gotosocial/internal/api" | ||||
| 	"github.com/gotosocial/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/gotosocial/gotosocial/pkg/mastotypes" | ||||
| 	"github.com/gotosocial/oauth2/v4" | ||||
| 	"github.com/gotosocial/oauth2/v4/errors" | ||||
| 	"github.com/gotosocial/oauth2/v4/manage" | ||||
|  | @ -37,12 +45,43 @@ type API struct { | |||
| 	log     *logrus.Logger | ||||
| } | ||||
| 
 | ||||
| type login struct { | ||||
| 	Email    string `form:"username"` | ||||
| 	Password string `form:"password"` | ||||
| } | ||||
| 
 | ||||
| type code struct { | ||||
| 	Code string `form:"code"` | ||||
| } | ||||
| 
 | ||||
| func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.Logger) *API { | ||||
| 	manager := manage.NewDefaultManager() | ||||
| 	manager.MapTokenStorage(ts) | ||||
| 	manager.MapClientStorage(cs) | ||||
| 	manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) | ||||
| 	sc := &server.Config{ | ||||
| 		TokenType: "Bearer", | ||||
| 		// Must follow the spec. | ||||
| 		AllowGetAccessRequest: false, | ||||
| 		// Support only the non-implicit flow. | ||||
| 		AllowedResponseTypes: []oauth2.ResponseType{oauth2.Code}, | ||||
| 		// Allow: | ||||
| 		// - Authorization Code (for first & third parties) | ||||
| 		// - Refreshing Tokens | ||||
| 		// | ||||
| 		// Deny: | ||||
| 		// - Resource owner secrets (password grant) | ||||
| 		// - Client secrets | ||||
| 		AllowedGrantTypes: []oauth2.GrantType{ | ||||
| 			oauth2.AuthorizationCode, | ||||
| 			oauth2.Refreshing, | ||||
| 		}, | ||||
| 		AllowedCodeChallengeMethods: []oauth2.CodeChallengeMethod{ | ||||
| 			oauth2.CodeChallengePlain, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	srv := server.NewDefaultServer(manager) | ||||
| 	srv := server.NewServer(sc, manager) | ||||
| 	srv.SetInternalErrorHandler(func(err error) *errors.Response { | ||||
| 		log.Errorf("internal oauth error: %s", err) | ||||
| 		return nil | ||||
|  | @ -52,15 +91,29 @@ func New(ts oauth2.TokenStore, cs oauth2.ClientStore, conn *pg.DB, log *logrus.L | |||
| 		log.Errorf("internal response error: %s", re.Error) | ||||
| 	}) | ||||
| 
 | ||||
| 	return &API{ | ||||
| 	api := &API{ | ||||
| 		manager: manager, | ||||
| 		server:  srv, | ||||
| 		conn:    conn, | ||||
| 		log:     log, | ||||
| 	} | ||||
| 
 | ||||
| 	api.server.SetUserAuthorizationHandler(api.UserAuthorizationHandler) | ||||
| 	api.server.SetClientInfoHandler(server.ClientFormHandler) | ||||
| 	return api | ||||
| } | ||||
| 
 | ||||
| func (a *API) AddRoutes(s api.Server) error { | ||||
| 	s.AttachHandler(http.MethodPost, "/api/v1/apps", a.AppsPOSTHandler) | ||||
| 
 | ||||
| 	s.AttachHandler(http.MethodGet, "/auth/sign_in", a.SignInGETHandler) | ||||
| 	s.AttachHandler(http.MethodPost, "/auth/sign_in", a.SignInPOSTHandler) | ||||
| 
 | ||||
| 	s.AttachHandler(http.MethodPost, "/oauth/token", a.TokenPOSTHandler) | ||||
| 
 | ||||
| 	s.AttachHandler(http.MethodGet, "/oauth/authorize", a.AuthorizeGETHandler) | ||||
| 	s.AttachHandler(http.MethodPost, "/oauth/authorize", a.AuthorizePOSTHandler) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  | @ -68,28 +121,326 @@ func incorrectPassword() (string, error) { | |||
| 	return "", errors.New("password/email combination was incorrect") | ||||
| } | ||||
| 
 | ||||
| func (a *API) PasswordAuthorizationHandler(email string, password string) (userid string, err error) { | ||||
| /* | ||||
| 	MAIN HANDLERS -- serve these through a server/router | ||||
| */ | ||||
| 
 | ||||
| // AppsPOSTHandler should be served at https://example.org/api/v1/apps | ||||
| // It is equivalent to: https://docs.joinmastodon.org/methods/apps/ | ||||
| func (a *API) AppsPOSTHandler(c *gin.Context) { | ||||
| 	l := a.log.WithField("func", "AppsPOSTHandler") | ||||
| 	l.Trace("entering AppsPOSTHandler") | ||||
| 
 | ||||
| 	form := &mastotypes.ApplicationPOSTRequest{} | ||||
| 	if err := c.ShouldBind(form); err != nil { | ||||
| 		c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// permitted length for most fields | ||||
| 	permittedLength := 64 | ||||
| 	// redirect can be a bit bigger because we probably need to encode data in the redirect uri | ||||
| 	permittedRedirect := 256 | ||||
| 
 | ||||
| 	// check lengths of fields before proceeding so the user can't spam huge entries into the database | ||||
| 	if len(form.ClientName) > permittedLength { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", permittedLength)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.Website) > permittedLength { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", permittedLength)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.RedirectURIs) > permittedRedirect { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", permittedRedirect)}) | ||||
| 		return | ||||
| 	} | ||||
| 	if len(form.Scopes) > permittedLength { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", permittedLength)}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// set default 'read' for scopes if it's not set | ||||
| 	var scopes string | ||||
| 	if form.Scopes == "" { | ||||
| 		scopes = "read" | ||||
| 	} else { | ||||
| 		scopes = form.Scopes | ||||
| 	} | ||||
| 
 | ||||
| 	// generate new IDs for this application and its associated client | ||||
| 	clientID := uuid.NewString() | ||||
| 	clientSecret := uuid.NewString() | ||||
| 	vapidKey := uuid.NewString() | ||||
| 
 | ||||
| 	// generate the application to put in the database | ||||
| 	app := >smodel.Application{ | ||||
| 		Name:         form.ClientName, | ||||
| 		Website:      form.Website, | ||||
| 		RedirectURI:  form.RedirectURIs, | ||||
| 		ClientID:     clientID, | ||||
| 		ClientSecret: clientSecret, | ||||
| 		Scopes:       scopes, | ||||
| 		VapidKey:     vapidKey, | ||||
| 	} | ||||
| 
 | ||||
| 	// chuck it in the db | ||||
| 	if _, err := a.conn.Model(app).Insert(); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// now we need to model an oauth client from the application that the oauth library can use | ||||
| 	oc := &oauthClient{ | ||||
| 		ID:     clientID, | ||||
| 		Secret: clientSecret, | ||||
| 		Domain: form.RedirectURIs, | ||||
| 		UserID: "", // This client isn't yet associated with a specific user,  it's just an app client right now | ||||
| 	} | ||||
| 
 | ||||
| 	// chuck it in the db | ||||
| 	if _, err := a.conn.Model(oc).Insert(); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// done, return the new app information per the spec here: https://docs.joinmastodon.org/methods/apps/ | ||||
| 	c.JSON(http.StatusOK, app) | ||||
| } | ||||
| 
 | ||||
| // SignInGETHandler should be served at https://example.org/auth/sign_in. | ||||
| // The idea is to present a sign in page to the user, where they can enter their username and password. | ||||
| // The form will then POST to the sign in page, which will be handled by SignInPOSTHandler | ||||
| func (a *API) SignInGETHandler(c *gin.Context) { | ||||
| 	a.log.WithField("func", "SignInGETHandler").Trace("serving sign in html") | ||||
| 	c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{}) | ||||
| } | ||||
| 
 | ||||
| // SignInPOSTHandler should be served at https://example.org/auth/sign_in. | ||||
| // The idea is to present a sign in page to the user, where they can enter their username and password. | ||||
| // The handler will then redirect to the auth handler served at /auth | ||||
| func (a *API) SignInPOSTHandler(c *gin.Context) { | ||||
| 	l := a.log.WithField("func", "SignInPOSTHandler") | ||||
| 	s := sessions.Default(c) | ||||
| 	form := &login{} | ||||
| 	if err := c.ShouldBind(form); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 	l.Tracef("parsed form: %+v", form) | ||||
| 
 | ||||
| 	userid, err := a.ValidatePassword(form.Email, form.Password) | ||||
| 	if err != nil { | ||||
| 		c.String(http.StatusForbidden, err.Error()) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	s.Set("username", userid) | ||||
| 	if err := s.Save(); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	l.Trace("redirecting to auth page") | ||||
| 	c.Redirect(http.StatusFound, "/oauth/authorize") | ||||
| } | ||||
| 
 | ||||
| // TokenPOSTHandler should be served as a POST at https://example.org/oauth/token | ||||
| // The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs. | ||||
| // See https://docs.joinmastodon.org/methods/apps/oauth/#obtain-a-token | ||||
| func (a *API) TokenPOSTHandler(c *gin.Context) { | ||||
| 	l := a.log.WithField("func", "TokenHandler") | ||||
| 	l.Trace("entered token handler, will now go to server.HandleTokenRequest") | ||||
| 	if err := a.server.HandleTokenRequest(c.Writer, c.Request); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize | ||||
| // The idea here is to present an oauth authorize page to the user, with a button | ||||
| // that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user | ||||
| func (a *API) AuthorizeGETHandler(c *gin.Context) { | ||||
| 	l := a.log.WithField("func", "AuthorizeGETHandler") | ||||
| 	s := sessions.Default(c) | ||||
| 
 | ||||
| 	// Username will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow | ||||
| 	// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page. | ||||
| 	v := s.Get("username") | ||||
| 	if username, ok := v.(string); !ok || username == "" { | ||||
| 		l.Trace("username was empty, parsing form then redirecting to sign in page") | ||||
| 
 | ||||
| 		// first make sure they've filled out the authorize form with the required values | ||||
| 		form := &mastotypes.OAuthAuthorize{} | ||||
| 		if err := c.ShouldBind(form); err != nil { | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 		l.Tracef("parsed form: %+v", form) | ||||
| 
 | ||||
| 		// these fields are *required* so check 'em | ||||
| 		if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" { | ||||
| 			c.JSON(http.StatusBadRequest, gin.H{"error": "missing one of: response_type, client_id or redirect_uri"}) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// save these values from the form so we can use them elsewhere in the session | ||||
| 		s.Set("force_login", form.ForceLogin) | ||||
| 		s.Set("response_type", form.ResponseType) | ||||
| 		s.Set("client_id", form.ClientID) | ||||
| 		s.Set("redirect_uri", form.RedirectURI) | ||||
| 		s.Set("scope", form.Scope) | ||||
| 		if err := s.Save(); err != nil { | ||||
| 			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 			return | ||||
| 		} | ||||
| 
 | ||||
| 		// send them to the sign in page so we can tell who they are | ||||
| 		c.Redirect(http.StatusFound, "/auth/sign_in") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// Check if we have a code already. If we do, it means the user used urn:ietf:wg:oauth:2.0:oob as their redirect URI | ||||
| 	// and were sent here, which means they just want the code displayed so they can use it out of band. | ||||
| 	code := &code{} | ||||
| 	if err := c.Bind(code); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// the authorize template will either: | ||||
| 	// 1. Display the code to the user if they're already authorized and were redirected here because they selected urn:ietf:wg:oauth:2.0:oob. | ||||
| 	// 2. Display a form where they can get some information about the app that's trying to authorize, and approve it, which will then go to AuthorizePOSTHandler | ||||
| 	l.Trace("serving authorize html") | ||||
| 	c.HTML(http.StatusOK, "authorize.tmpl", gin.H{ | ||||
| 		"code": code.Code, | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| // AuthorizePOSTHandler should be served as POST at https://example.org/oauth/authorize | ||||
| // The idea here is to present an oauth authorize page to the user, with a button | ||||
| // that they have to click to accept. See here: https://docs.joinmastodon.org/methods/apps/oauth/#authorize-a-user | ||||
| func (a *API) AuthorizePOSTHandler(c *gin.Context) { | ||||
| 	l := a.log.WithField("func", "AuthorizePOSTHandler") | ||||
| 	s := sessions.Default(c) | ||||
| 
 | ||||
| 	v := s.Get("username") | ||||
| 	if username, ok := v.(string); !ok || username == "" { | ||||
| 		c.JSON(http.StatusUnauthorized, gin.H{"error": "you are not signed in"}) | ||||
| 	} | ||||
| 
 | ||||
| 	values := url.Values{} | ||||
| 
 | ||||
| 	if v, ok := s.Get("force_login").(string); !ok { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "session missing force_login"}) | ||||
| 		return | ||||
| 	} else { | ||||
| 		values.Add("force_login", v) | ||||
| 	} | ||||
| 
 | ||||
| 	if v, ok := s.Get("response_type").(string); !ok { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "session missing response_type"}) | ||||
| 		return | ||||
| 	} else { | ||||
| 		values.Add("response_type", v) | ||||
| 	} | ||||
| 
 | ||||
| 	if v, ok := s.Get("client_id").(string); !ok { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "session missing client_id"}) | ||||
| 		return | ||||
| 	} else { | ||||
| 		values.Add("client_id", v) | ||||
| 	} | ||||
| 
 | ||||
| 	if v, ok := s.Get("redirect_uri").(string); !ok { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "session missing redirect_uri"}) | ||||
| 		return | ||||
| 	} else { | ||||
| 		// todo: explain this little hack | ||||
| 		if v == "urn:ietf:wg:oauth:2.0:oob" { | ||||
| 			v = "http://localhost:8080/oauth/authorize" | ||||
| 		} | ||||
| 		values.Add("redirect_uri", v) | ||||
| 	} | ||||
| 
 | ||||
| 	if v, ok := s.Get("scope").(string); !ok { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "session missing scope"}) | ||||
| 		return | ||||
| 	} else { | ||||
| 		values.Add("scope", v) | ||||
| 	} | ||||
| 
 | ||||
| 	if v, ok := s.Get("username").(string); !ok { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "session missing username"}) | ||||
| 		return | ||||
| 	} else { | ||||
| 		values.Add("username", v) | ||||
| 	} | ||||
| 
 | ||||
| 	c.Request.Form = values | ||||
| 	l.Tracef("values on request set to %+v", c.Request.Form) | ||||
| 
 | ||||
| 	if err := s.Save(); err != nil { | ||||
| 		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	if err := a.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil { | ||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| /* | ||||
| 	SUB-HANDLERS -- don't serve these directly, they should be attached to the oauth2 server | ||||
| */ | ||||
| 
 | ||||
| // PasswordAuthorizationHandler takes a username (in this case, we use an email address) | ||||
| // and a password. The goal is to authenticate the password against the one for that email | ||||
| // address stored in the database. If OK, we return the userid (a uuid) for that user, | ||||
| // so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db. | ||||
| func (a *API) ValidatePassword(email string, password string) (userid string, err error) { | ||||
| 	l := a.log.WithField("func", "PasswordAuthorizationHandler") | ||||
| 
 | ||||
| 	// make sure an email/password was provided and bail if not | ||||
| 	if email == "" || password == "" { | ||||
| 		l.Debug("email or password was not provided") | ||||
| 		return incorrectPassword() | ||||
| 	} | ||||
| 
 | ||||
| 	// first we select the user from the database based on email address, bail if no user found for that email | ||||
| 	gtsUser := >smodel.User{} | ||||
| 	if err := a.conn.Model(gtsUser).Where("email = ?", email).Select(); err != nil { | ||||
| 		a.log.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) | ||||
| 		l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err) | ||||
| 		return incorrectPassword() | ||||
| 	} | ||||
| 
 | ||||
| 	// make sure a password is actually set and bail if not | ||||
| 	if gtsUser.EncryptedPassword == "" { | ||||
| 		a.log.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email) | ||||
| 		l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email) | ||||
| 		return incorrectPassword() | ||||
| 	} | ||||
| 
 | ||||
| 	// compare the provided password with the encrypted one from the db, bail if they don't match | ||||
| 	if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil { | ||||
| 		a.log.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err) | ||||
| 		l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err) | ||||
| 		return incorrectPassword() | ||||
| 	} | ||||
| 
 | ||||
| 	// If we've made it this far the email/password is correct so we need the oauth client-id of the user | ||||
| 	// This is, conveniently, the same as the user ID, so we can just return it. | ||||
| 	// If we've made it this far the email/password is correct, so we can just return the id of the user. | ||||
| 	userid = gtsUser.ID | ||||
| 	l.Tracef("returning (%s, %s)", userid, err) | ||||
| 	return | ||||
| } | ||||
| 
 | ||||
| // UserAuthorizationHandler gets the user's ID from the 'username' field of the request form, | ||||
| // or redirects to the /auth/sign_in page, if this key is not present. | ||||
| func (a *API) UserAuthorizationHandler(w http.ResponseWriter, r *http.Request) (userID string, err error) { | ||||
| 	l := a.log.WithField("func", "UserAuthorizationHandler") | ||||
| 	userID = r.FormValue("username") | ||||
| 	if userID == "" { | ||||
| 		l.Trace("username was empty, redirecting to sign in page") | ||||
| 		http.Redirect(w, r, "/auth/sign_in", http.StatusFound) | ||||
| 		return "", nil | ||||
| 	} | ||||
| 	l.Tracef("returning (%s, %s)", userID, err) | ||||
| 	return userID, err | ||||
| } | ||||
|  |  | |||
							
								
								
									
										130
									
								
								internal/oauth/oauth_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								internal/oauth/oauth_test.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,130 @@ | |||
| package oauth | ||||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/go-pg/pg/v10" | ||||
| 	"github.com/go-pg/pg/v10/orm" | ||||
| 	"github.com/gotosocial/gotosocial/internal/api" | ||||
| 	"github.com/gotosocial/gotosocial/internal/config" | ||||
| 	"github.com/gotosocial/gotosocial/internal/gtsmodel" | ||||
| 	"github.com/gotosocial/oauth2/v4" | ||||
| 	"github.com/sirupsen/logrus" | ||||
| 	"github.com/stretchr/testify/suite" | ||||
| 	"golang.org/x/crypto/bcrypt" | ||||
| ) | ||||
| 
 | ||||
| type OauthTestSuite struct { | ||||
| 	suite.Suite | ||||
| 	tokenStore  oauth2.TokenStore | ||||
| 	clientStore oauth2.ClientStore | ||||
| 	conn        *pg.DB | ||||
| 	testAccount *gtsmodel.Account | ||||
| 	testUser    *gtsmodel.User | ||||
| 	testClient  *oauthClient | ||||
| 	config      *config.Config | ||||
| } | ||||
| 
 | ||||
| const () | ||||
| 
 | ||||
| // SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout | ||||
| func (suite *OauthTestSuite) SetupSuite() { | ||||
| 	encryptedPassword, err := bcrypt.GenerateFromPassword([]byte("test-password"), bcrypt.DefaultCost) | ||||
| 	if err != nil { | ||||
| 		logrus.Panicf("error encrypting user pass: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	suite.testAccount = >smodel.Account{} | ||||
| 	suite.testUser = >smodel.User{ | ||||
| 		EncryptedPassword: string(encryptedPassword), | ||||
| 		Email:             "user@localhost", | ||||
| 		AccountID:         "some-account-id-it-doesn't-matter-really-since-this-user-doesn't-actually-have-an-account!", | ||||
| 	} | ||||
| 	suite.testClient = &oauthClient{ | ||||
| 		ID:     "a-known-client-id", | ||||
| 		Secret: "some-secret", | ||||
| 		Domain: "http://localhost:8080", | ||||
| 	} | ||||
| 
 | ||||
| 	// because go tests are run within the test package directory, we need to fiddle with the templateconfig | ||||
| 	// basedir in a way that we wouldn't normally have to do when running the binary, in order to make | ||||
| 	// the templates actually load | ||||
| 	c := config.Empty() | ||||
| 	c.TemplateConfig.BaseDir = "../../web/template/" | ||||
| 	suite.config = c | ||||
| } | ||||
| 
 | ||||
| // SetupTest creates a postgres connection and creates the oauth_clients table before each test | ||||
| func (suite *OauthTestSuite) SetupTest() { | ||||
| 	suite.conn = pg.Connect(&pg.Options{}) | ||||
| 	if err := suite.conn.Ping(context.Background()); err != nil { | ||||
| 		logrus.Panicf("db connection error: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	models := []interface{}{ | ||||
| 		&oauthClient{}, | ||||
| 		&oauthToken{}, | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Application{}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, m := range models { | ||||
| 		if err := suite.conn.Model(m).CreateTable(&orm.CreateTableOptions{ | ||||
| 			IfNotExists: true, | ||||
| 		}); err != nil { | ||||
| 			logrus.Panicf("db connection error: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	suite.tokenStore = NewPGTokenStore(context.Background(), suite.conn, logrus.New()) | ||||
| 	suite.clientStore = NewPGClientStore(suite.conn) | ||||
| 
 | ||||
| 	if _, err := suite.conn.Model(suite.testUser).Insert(); err != nil { | ||||
| 		logrus.Panicf("could not insert test user into db: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| 	if _, err := suite.conn.Model(suite.testClient).Insert(); err != nil { | ||||
| 		logrus.Panicf("could not insert test client into db: %s", err) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| // TearDownTest drops the oauth_clients table and closes the pg connection after each test | ||||
| func (suite *OauthTestSuite) TearDownTest() { | ||||
| 	models := []interface{}{ | ||||
| 		&oauthClient{}, | ||||
| 		&oauthToken{}, | ||||
| 		>smodel.User{}, | ||||
| 		>smodel.Account{}, | ||||
| 		>smodel.Application{}, | ||||
| 	} | ||||
| 	for _, m := range models { | ||||
| 		if err := suite.conn.Model(m).DropTable(&orm.DropTableOptions{}); err != nil { | ||||
| 			logrus.Panicf("drop table error: %s", err) | ||||
| 		} | ||||
| 	} | ||||
| 	if err := suite.conn.Close(); err != nil { | ||||
| 		logrus.Panicf("error closing db connection: %s", err) | ||||
| 	} | ||||
| 	suite.conn = nil | ||||
| } | ||||
| 
 | ||||
| func (suite *OauthTestSuite) TestAPIInitialize() { | ||||
| 	log := logrus.New() | ||||
| 	log.SetLevel(logrus.TraceLevel) | ||||
| 
 | ||||
| 	r := api.New(suite.config, log) | ||||
| 	api := New(suite.tokenStore, suite.clientStore, suite.conn, log) | ||||
| 	api.AddRoutes(r) | ||||
| 	go r.Start() | ||||
| 	time.Sleep(30 * time.Second) | ||||
| 	// http://localhost:8080/oauth/authorize?client_id=a-known-client-id&response_type=code&redirect_uri=https://example.org | ||||
| 	// http://localhost:8080/oauth/authorize?client_id=a-known-client-id&response_type=code&redirect_uri=urn:ietf:wg:oauth:2.0:oob | ||||
| } | ||||
| 
 | ||||
| func TestOauthTestSuite(t *testing.T) { | ||||
| 	suite.Run(t, new(OauthTestSuite)) | ||||
| } | ||||
|  | @ -20,6 +20,7 @@ package oauth | |||
| 
 | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 
 | ||||
| 	"github.com/go-pg/pg/v10" | ||||
| 	"github.com/gotosocial/oauth2/v4" | ||||
|  | @ -42,7 +43,7 @@ func (pcs *pgClientStore) GetByID(ctx context.Context, clientID string) (oauth2. | |||
| 		ID: clientID, | ||||
| 	} | ||||
| 	if err := pcs.conn.WithContext(ctx).Model(poc).Where("id = ?", poc.ID).Select(); err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, fmt.Errorf("error in clientstore getbyid searching for client %s: %s", clientID, err) | ||||
| 	} | ||||
| 	return models.New(poc.ID, poc.Secret, poc.Domain, poc.UserID), nil | ||||
| } | ||||
|  | @ -55,7 +56,10 @@ func (pcs *pgClientStore) Set(ctx context.Context, id string, cli oauth2.ClientI | |||
| 		UserID: cli.GetUserID(), | ||||
| 	} | ||||
| 	_, err := pcs.conn.WithContext(ctx).Model(poc).OnConflict("(id) DO UPDATE").Insert() | ||||
| 	return err | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error in clientstore set: %s", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (pcs *pgClientStore) Delete(ctx context.Context, id string) error { | ||||
|  | @ -63,7 +67,10 @@ func (pcs *pgClientStore) Delete(ctx context.Context, id string) error { | |||
| 		ID: id, | ||||
| 	} | ||||
| 	_, err := pcs.conn.WithContext(ctx).Model(poc).Where("id = ?", poc.ID).Delete() | ||||
| 	return err | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error in clientstore delete: %s", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| type oauthClient struct { | ||||
|  |  | |||
|  | @ -96,7 +96,6 @@ func (suite *PgClientStoreTestSuite) TestClientSetAndDelete() { | |||
| 	deletedClient, err := cs.GetByID(context.Background(), suite.testClientID) | ||||
| 	suite.Assert().Nil(deletedClient) | ||||
| 	suite.Assert().NotNil(err) | ||||
| 	suite.EqualValues("pg: no rows in result set", err.Error()) | ||||
| } | ||||
| 
 | ||||
| func TestPgClientStoreTestSuite(t *testing.T) { | ||||
|  |  | |||
|  | @ -21,6 +21,7 @@ package oauth | |||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/go-pg/pg/v10" | ||||
|  | @ -98,32 +99,44 @@ func (pts *pgTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) erro | |||
| 		return errors.New("info param was not a models.Token") | ||||
| 	} | ||||
| 	_, err := pts.conn.WithContext(ctx).Model(oauthTokenToPGToken(t)).Insert() | ||||
| 	return err | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error in tokenstore create: %s", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // RemoveByCode deletes a token from the DB based on the Code field | ||||
| func (pts *pgTokenStore) RemoveByCode(ctx context.Context, code string) error { | ||||
| 	_, err := pts.conn.Model(&oauthToken{}).Where("code = ?", code).Delete() | ||||
| 	return err | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error in tokenstore removebycode: %s", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // RemoveByAccess deletes a token from the DB based on the Access field | ||||
| func (pts *pgTokenStore) RemoveByAccess(ctx context.Context, access string) error { | ||||
| 	_, err := pts.conn.Model(&oauthToken{}).Where("access = ?", access).Delete() | ||||
| 	return err | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error in tokenstore removebyaccess: %s", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // RemoveByRefresh deletes a token from the DB based on the Refresh field | ||||
| func (pts *pgTokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { | ||||
| 	_, err := pts.conn.Model(&oauthToken{}).Where("refresh = ?", refresh).Delete() | ||||
| 	return err | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("error in tokenstore removebyrefresh: %s", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // GetByCode selects a token from the DB based on the Code field | ||||
| func (pts *pgTokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) { | ||||
| 	pgt := &oauthToken{} | ||||
| 	if err := pts.conn.Model(pgt).Where("code = ?", code).Select(); err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, fmt.Errorf("error in tokenstore getbycode: %s", err) | ||||
| 	} | ||||
| 	return pgTokenToOauthToken(pgt), nil | ||||
| } | ||||
|  | @ -132,7 +145,7 @@ func (pts *pgTokenStore) GetByCode(ctx context.Context, code string) (oauth2.Tok | |||
| func (pts *pgTokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) { | ||||
| 	pgt := &oauthToken{} | ||||
| 	if err := pts.conn.Model(pgt).Where("access = ?", access).Select(); err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, fmt.Errorf("error in tokenstore getbyaccess: %s", err) | ||||
| 	} | ||||
| 	return pgTokenToOauthToken(pgt), nil | ||||
| } | ||||
|  | @ -141,7 +154,7 @@ func (pts *pgTokenStore) GetByAccess(ctx context.Context, access string) (oauth2 | |||
| func (pts *pgTokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) { | ||||
| 	pgt := &oauthToken{} | ||||
| 	if err := pts.conn.Model(pgt).Where("refresh = ?", refresh).Select(); err != nil { | ||||
| 		return nil, err | ||||
| 		return nil, fmt.Errorf("error in tokenstore getbyrefresh: %s", err) | ||||
| 	} | ||||
| 	return pgTokenToOauthToken(pgt), nil | ||||
| } | ||||
|  | @ -165,15 +178,15 @@ type oauthToken struct { | |||
| 	UserID              string | ||||
| 	RedirectURI         string | ||||
| 	Scope               string | ||||
| 	Code                string `pg:",pk"` | ||||
| 	Code                string `pg:"default:'',pk"` | ||||
| 	CodeChallenge       string | ||||
| 	CodeChallengeMethod string | ||||
| 	CodeCreateAt        time.Time `pg:"type:timestamp"` | ||||
| 	CodeExpiresAt       time.Time `pg:"type:timestamp"` | ||||
| 	Access              string    `pg:",pk"` | ||||
| 	Access              string    `pg:"default:'',pk"` | ||||
| 	AccessCreateAt      time.Time `pg:"type:timestamp"` | ||||
| 	AccessExpiresAt     time.Time `pg:"type:timestamp"` | ||||
| 	Refresh             string    `pg:",pk"` | ||||
| 	Refresh             string    `pg:"default:'',pk"` | ||||
| 	RefreshCreateAt     time.Time `pg:"type:timestamp"` | ||||
| 	RefreshExpiresAt    time.Time `pg:"type:timestamp"` | ||||
| } | ||||
|  |  | |||
|  | @ -18,12 +18,38 @@ | |||
| 
 | ||||
| package mastotypes | ||||
| 
 | ||||
| // Application represents a mastodon-api Application, as defined here: https://docs.joinmastodon.org/entities/application/ | ||||
| // Application represents a mastodon-api Application, as defined here: https://docs.joinmastodon.org/entities/application/. | ||||
| // Primarily, application is used for allowing apps like Tusky etc to connect to Mastodon on behalf of a user. | ||||
| // See https://docs.joinmastodon.org/methods/apps/ | ||||
| type Application struct { | ||||
| 	// The application ID in the db | ||||
| 	ID string `json:"id,omitempty"` | ||||
| 	// The name of your application. | ||||
| 	Name string `json:"name"` | ||||
| 	// The website associated with your application (url) | ||||
| 	Website string `json:"website"` | ||||
| 	Website string `json:"website,omitempty"` | ||||
| 	// Where the user should be redirected after authorization. | ||||
| 	RedirectURI string `json:"redirect_uri,omitempty"` | ||||
| 	// ClientID to use when obtaining an oauth token for this application (ie., in client_id parameter of https://docs.joinmastodon.org/methods/apps/) | ||||
| 	ClientID string `json:"client_id,omitempty"` | ||||
| 	// Client secret to use when obtaining an auth token for this application (ie., in client_secret parameter of https://docs.joinmastodon.org/methods/apps/) | ||||
| 	ClientSecret string `json:"client_secret,omitempty"` | ||||
| 	// Used for Push Streaming API. Returned with POST /api/v1/apps. Equivalent to https://docs.joinmastodon.org/entities/pushsubscription/#server_key | ||||
| 	VapidKey string `json:"vapid_key"` | ||||
| } | ||||
| 
 | ||||
| // ApplicationPOSTRequest represents a POST request to https://example.org/api/v1/apps. | ||||
| // See here: https://docs.joinmastodon.org/methods/apps/ | ||||
| // And here: https://docs.joinmastodon.org/client/token/ | ||||
| type ApplicationPOSTRequest struct { | ||||
| 	// A name for your application | ||||
| 	ClientName string `form:"client_name"` | ||||
| 	// Where the user should be redirected after authorization. | ||||
| 	// To display the authorization code to the user instead of redirecting | ||||
| 	// to a web page, use urn:ietf:wg:oauth:2.0:oob in this parameter. | ||||
| 	RedirectURIs string `form:"redirect_uris"` | ||||
| 	// Space separated list of scopes. If none is provided, defaults to read. | ||||
| 	Scopes string `form:"scopes"` | ||||
| 	// A URL to the homepage of your app | ||||
| 	Website string `form:"website"` | ||||
| } | ||||
|  |  | |||
							
								
								
									
										37
									
								
								pkg/mastotypes/oauth.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								pkg/mastotypes/oauth.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| /* | ||||
|    GoToSocial | ||||
|    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||
| 
 | ||||
|    This program is free software: you can redistribute it and/or modify | ||||
|    it under the terms of the GNU Affero General Public License as published by | ||||
|    the Free Software Foundation, either version 3 of the License, or | ||||
|    (at your option) any later version. | ||||
| 
 | ||||
|    This program is distributed in the hope that it will be useful, | ||||
|    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|    GNU Affero General Public License for more details. | ||||
| 
 | ||||
|    You should have received a copy of the GNU Affero General Public License | ||||
|    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
| */ | ||||
| 
 | ||||
| package mastotypes | ||||
| 
 | ||||
| // OAuthAuthorize represents a request sent to https://example.org/oauth/authorize | ||||
| // See here: https://docs.joinmastodon.org/methods/apps/oauth/ | ||||
| type OAuthAuthorize struct { | ||||
| 	// Forces the user to re-login, which is necessary for authorizing with multiple accounts from the same instance. | ||||
| 	ForceLogin string `form:"force_login,omitempty"` | ||||
| 	// Should be set equal to `code`. | ||||
| 	ResponseType string `form:"response_type"` | ||||
| 	// Client ID, obtained during app registration. | ||||
| 	ClientID string `form:"client_id"` | ||||
| 	// Set a URI to redirect the user to. | ||||
| 	// If this parameter is set to urn:ietf:wg:oauth:2.0:oob then the authorization code will be shown instead. | ||||
| 	// Must match one of the redirect URIs declared during app registration. | ||||
| 	RedirectURI string `form:"redirect_uri"` | ||||
| 	// List of requested OAuth scopes, separated by spaces (or by pluses, if using query parameters). | ||||
| 	// Must be a subset of scopes declared during app registration. If not provided, defaults to read. | ||||
| 	Scope string `form:"scope,omitempty"` | ||||
| } | ||||
							
								
								
									
										44
									
								
								web/template/authorize.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								web/template/authorize.tmpl
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
|   <head> | ||||
|     <meta charset="UTF-8" /> | ||||
|     <title>Auth</title> | ||||
|     <link | ||||
|       rel="stylesheet" | ||||
|       href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" | ||||
|     /> | ||||
|     <script src="//code.jquery.com/jquery-2.2.4.min.js"></script> | ||||
|     <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> | ||||
|   </head> | ||||
| 
 | ||||
| {{if len .code | eq 0 }} | ||||
|   <body> | ||||
|     <div class="container"> | ||||
|       <div class="jumbotron"> | ||||
|         <form action="/oauth/authorize" method="POST"> | ||||
|           <h1>Authorize</h1> | ||||
|           <p>The client would like to perform actions on your behalf.</p> | ||||
|           <p> | ||||
|             <button | ||||
|               type="submit" | ||||
|               class="btn btn-primary btn-lg" | ||||
|               style="width:200px;" | ||||
|             > | ||||
|               Allow | ||||
|             </button> | ||||
|           </p> | ||||
|         </form> | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| {{else}} | ||||
|   <body> | ||||
|     <div class="container"> | ||||
|       <div class="jumbotron"> | ||||
|         {{.code}} | ||||
|       </div> | ||||
|     </div> | ||||
|   </body> | ||||
| {{end}} | ||||
| 
 | ||||
| </html> | ||||
							
								
								
									
										28
									
								
								web/template/sign-in.tmpl
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								web/template/sign-in.tmpl
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <title>Login</title> | ||||
|     <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"> | ||||
|     <script src="//code.jquery.com/jquery-2.2.4.min.js"></script> | ||||
|     <script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script> | ||||
| </head> | ||||
| 
 | ||||
| <body> | ||||
|     <div class="container"> | ||||
|         <h1>Login</h1> | ||||
|         <form action="/auth/sign_in" method="POST"> | ||||
|             <div class="form-group"> | ||||
|                 <label for="email">Email</label> | ||||
|                 <input type="text" class="form-control" name="username" required placeholder="Please enter your email address"> | ||||
|             </div> | ||||
|             <div class="form-group"> | ||||
|                 <label for="password">Password</label> | ||||
|                 <input type="password" class="form-control" name="password" placeholder="Please enter your password"> | ||||
|             </div> | ||||
|             <button type="submit" class="btn btn-success">Login</button> | ||||
|         </form> | ||||
|     </div> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue