├── Procfile
├── .idea
├── .gitignore
├── misc.xml
├── dictionaries
│ └── project.xml
├── modules.xml
├── ImpactServer.iml
├── runConfigurations
│ └── Run.xml
└── vcs.xml
├── static
├── google0dac0c1f21963dd8.html
├── google5b4336da37970ce2.html
├── favicon.ico
├── img
│ ├── header.png
│ ├── header.xcf
│ ├── project1.jpg
│ ├── project3.png
│ ├── project4.jpg
│ ├── project5.png
│ ├── welcome.png
│ ├── java-icon.png
│ ├── macmoment.png
│ ├── parallax1.png
│ ├── project2.jpeg
│ ├── project6.jpeg
│ ├── avatar_flash.png
│ ├── avatar_robin.png
│ ├── windows-icon.png
│ ├── windowsmoment.png
│ ├── avatar_cat_woman.png
│ ├── javamoment_linux.png
│ ├── javamoment_mac.png
│ ├── javamoment_win.png
│ ├── pepsi_background.png
│ └── avatar_captain_america.png
├── favicon-128x128.png
├── favicon-16x16.png
├── favicon-196x196.png
├── favicon-256x256.png
├── favicon-32x32.png
├── favicon-96x96.png
├── fonts
│ ├── FontAwesome.otf
│ ├── fontawesome-webfont.eot
│ ├── fontawesome-webfont.ttf
│ ├── roboto
│ │ ├── Roboto-Bold.eot
│ │ ├── Roboto-Bold.ttf
│ │ ├── Roboto-Bold.woff
│ │ ├── Roboto-Light.eot
│ │ ├── Roboto-Light.ttf
│ │ ├── Roboto-Thin.eot
│ │ ├── Roboto-Thin.ttf
│ │ ├── Roboto-Thin.woff
│ │ ├── Roboto-Bold.woff2
│ │ ├── Roboto-Light.woff
│ │ ├── Roboto-Light.woff2
│ │ ├── Roboto-Medium.eot
│ │ ├── Roboto-Medium.ttf
│ │ ├── Roboto-Medium.woff
│ │ ├── Roboto-Regular.eot
│ │ ├── Roboto-Regular.ttf
│ │ ├── Roboto-Thin.woff2
│ │ ├── Roboto-Medium.woff2
│ │ ├── Roboto-Regular.woff
│ │ └── Roboto-Regular.woff2
│ ├── fontawesome-webfont.woff
│ └── fontawesome-webfont.woff2
├── apple-touch-icon-57x57.png
├── apple-touch-icon-60x60.png
├── apple-touch-icon-72x72.png
├── apple-touch-icon-76x76.png
├── apple-touch-icon-114x114.png
├── apple-touch-icon-120x120.png
├── apple-touch-icon-144x144.png
├── apple-touch-icon-152x152.png
├── recaptcha.html
├── min
│ ├── auth.jquery-min.js
│ ├── init-min.js
│ └── style-min.css
├── discord_oauth.html
├── account_explanation.html
├── alternatives.html
├── css
│ └── style.css
├── js
│ └── init.js
└── discord.html
├── .travis.yml
├── src
├── mailgun
│ └── main.go
├── middleware
│ ├── log.go
│ ├── redirect.go
│ ├── cache_test.go
│ ├── rewrite.go
│ ├── cache.go
│ ├── limit.go
│ ├── index.go
│ ├── index_test.go
│ └── auth.go
├── web
│ ├── applepay.go
│ ├── stripe.go
│ ├── changelog.go
│ ├── server.go
│ ├── changelog_test.go
│ ├── recaptcha.go
│ └── releases.go
├── util
│ ├── mediatype
│ │ └── index.go
│ ├── email.go
│ ├── runners.go
│ ├── proxy.go
│ ├── ip.go
│ ├── rsa.go
│ ├── urls_test.go
│ ├── proxy_test.go
│ ├── txt_import.go
│ ├── rsa_test.go
│ ├── urls.go
│ ├── http_test.go
│ └── http.go
├── api
│ ├── v1
│ │ ├── shared_test.go
│ │ ├── dbtest.go
│ │ ├── themes_test.go
│ │ ├── premiumcheck.go
│ │ ├── futureclientintegration.go
│ │ ├── motd.go
│ │ ├── thealtening.go
│ │ ├── impactbotintegration.go
│ │ ├── emailtest.go
│ │ ├── server.go
│ │ ├── minecraft.go
│ │ ├── themes.go
│ │ └── password.go
│ └── server.go
├── database
│ ├── null_uuid.go
│ ├── conn.go
│ ├── listener.go
│ ├── user.go
│ └── schema.go
├── minecraft
│ ├── auth.go
│ └── profile.go
├── jwt
│ ├── discord.go
│ ├── minecraft.go
│ ├── password.go
│ └── jwt.go
├── users
│ ├── features.go
│ ├── user.go
│ ├── editions.go
│ ├── user_info.go
│ └── role.go
├── newWeb
│ └── server.go
├── heroku
│ └── heroku.go
├── s3proxy
│ └── server.go
├── recaptcha
│ └── recaptcha.go
├── cloudflare
│ └── main.go
├── server.go
└── discord
│ └── discord.go
├── .gitignore
├── README.md
└── go.mod
/Procfile:
--------------------------------------------------------------------------------
1 | web: bin/src
2 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Default ignored files
3 | /workspace.xml
4 | /shelf/
5 |
--------------------------------------------------------------------------------
/static/google0dac0c1f21963dd8.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google0dac0c1f21963dd8.html
--------------------------------------------------------------------------------
/static/google5b4336da37970ce2.html:
--------------------------------------------------------------------------------
1 | google-site-verification: google5b4336da37970ce2.html
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/static/img/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/header.png
--------------------------------------------------------------------------------
/static/img/header.xcf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/header.xcf
--------------------------------------------------------------------------------
/static/img/project1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/project1.jpg
--------------------------------------------------------------------------------
/static/img/project3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/project3.png
--------------------------------------------------------------------------------
/static/img/project4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/project4.jpg
--------------------------------------------------------------------------------
/static/img/project5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/project5.png
--------------------------------------------------------------------------------
/static/img/welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/welcome.png
--------------------------------------------------------------------------------
/static/favicon-128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/favicon-128x128.png
--------------------------------------------------------------------------------
/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/favicon-16x16.png
--------------------------------------------------------------------------------
/static/favicon-196x196.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/favicon-196x196.png
--------------------------------------------------------------------------------
/static/favicon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/favicon-256x256.png
--------------------------------------------------------------------------------
/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/favicon-32x32.png
--------------------------------------------------------------------------------
/static/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/favicon-96x96.png
--------------------------------------------------------------------------------
/static/img/java-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/java-icon.png
--------------------------------------------------------------------------------
/static/img/macmoment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/macmoment.png
--------------------------------------------------------------------------------
/static/img/parallax1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/parallax1.png
--------------------------------------------------------------------------------
/static/img/project2.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/project2.jpeg
--------------------------------------------------------------------------------
/static/img/project6.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/project6.jpeg
--------------------------------------------------------------------------------
/static/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/static/img/avatar_flash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/avatar_flash.png
--------------------------------------------------------------------------------
/static/img/avatar_robin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/avatar_robin.png
--------------------------------------------------------------------------------
/static/img/windows-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/windows-icon.png
--------------------------------------------------------------------------------
/static/img/windowsmoment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/windowsmoment.png
--------------------------------------------------------------------------------
/static/img/avatar_cat_woman.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/avatar_cat_woman.png
--------------------------------------------------------------------------------
/static/img/javamoment_linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/javamoment_linux.png
--------------------------------------------------------------------------------
/static/img/javamoment_mac.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/javamoment_mac.png
--------------------------------------------------------------------------------
/static/img/javamoment_win.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/javamoment_win.png
--------------------------------------------------------------------------------
/static/img/pepsi_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/pepsi_background.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-57x57.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-57x57.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-60x60.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-60x60.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-72x72.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-72x72.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-76x76.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-76x76.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-114x114.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-114x114.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-120x120.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-120x120.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-144x144.png
--------------------------------------------------------------------------------
/static/apple-touch-icon-152x152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/apple-touch-icon-152x152.png
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Bold.eot
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Bold.ttf
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Bold.woff
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Light.eot
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Light.ttf
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Thin.eot
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Thin.ttf
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Thin.woff
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.13.x
5 |
6 | script:
7 | - diff -u <(echo -n) <(gofmt -d ./)
8 | - go test -v ./...
9 |
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Bold.woff2
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Light.woff
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Light.woff2
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Medium.eot
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Medium.ttf
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Medium.woff
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Regular.eot
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Regular.ttf
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Thin.woff2
--------------------------------------------------------------------------------
/static/img/avatar_captain_america.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/img/avatar_captain_america.png
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Medium.woff2
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Regular.woff
--------------------------------------------------------------------------------
/static/fonts/roboto/Roboto-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImpactDevelopment/ImpactServer/HEAD/static/fonts/roboto/Roboto-Regular.woff2
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
|
26 | |
28 |

You can now close this window.
88 | 91 | 92 | -------------------------------------------------------------------------------- /src/middleware/index_test.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "testing" 7 | 8 | "github.com/labstack/echo/v4" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestIndexToSlash(t *testing.T) { 13 | // Helper func to setup the test 14 | setup := func(code int) (e *echo.Echo) { 15 | e = echo.New() 16 | e.Pre(RemoveIndexHTML(code)) 17 | e.Any("/*", func(c echo.Context) error { 18 | return c.String(http.StatusOK, "Ok cowboy") 19 | }) 20 | return 21 | } 22 | 23 | // Helper function to run a request and return the response 24 | test := func(s *echo.Echo, url string) *httptest.ResponseRecorder { 25 | req := httptest.NewRequest(http.MethodGet, url, nil) 26 | rec := httptest.NewRecorder() 27 | s.ServeHTTP(rec, req) 28 | return rec 29 | } 30 | 31 | e := setup(http.StatusMovedPermanently) 32 | 33 | // index.html/ should produce 301 34 | rec := test(e, "http://foobar.net/foo/bar/index.html/") 35 | assert.Equal(t, http.StatusMovedPermanently, rec.Code) 36 | assert.Equal(t, "http://foobar.net/foo/bar", rec.Header().Get(echo.HeaderLocation)) 37 | assert.Equal(t, "", rec.Body.String()) 38 | 39 | // index.html should produce 301 40 | rec = test(e, "http://foobar.net/foo/bar/index.html") 41 | assert.Equal(t, http.StatusMovedPermanently, rec.Code) 42 | assert.Equal(t, "http://foobar.net/foo/bar", rec.Header().Get(echo.HeaderLocation)) 43 | assert.Equal(t, "", rec.Body.String()) 44 | 45 | // / should not redirect 46 | rec = test(e, "http://foobar.net/foo/bar/") 47 | assert.Equal(t, http.StatusOK, rec.Code) 48 | assert.Equal(t, "", rec.Header().Get(echo.HeaderLocation)) 49 | assert.Equal(t, "Ok cowboy", rec.Body.String()) 50 | 51 | // neither should bare path 52 | rec = test(e, "http://foobar.net/foo/bar") 53 | assert.Equal(t, http.StatusOK, rec.Code) 54 | assert.Equal(t, "", rec.Header().Get(echo.HeaderLocation)) 55 | assert.Equal(t, "Ok cowboy", rec.Body.String()) 56 | 57 | // root/ 58 | rec = test(e, "http://foobar.net/") 59 | assert.Equal(t, http.StatusOK, rec.Code) 60 | assert.Equal(t, "", rec.Header().Get(echo.HeaderLocation)) 61 | assert.Equal(t, "Ok cowboy", rec.Body.String()) 62 | 63 | // root 64 | rec = test(e, "http://foobar.net") 65 | assert.Equal(t, http.StatusOK, rec.Code) 66 | assert.Equal(t, "", rec.Header().Get(echo.HeaderLocation)) 67 | assert.Equal(t, "Ok cowboy", rec.Body.String()) 68 | 69 | // index.html/ should produce 301 70 | rec = test(e, "http://foobar.net/index.html/") 71 | assert.Equal(t, http.StatusMovedPermanently, rec.Code) 72 | assert.Equal(t, "http://foobar.net", rec.Header().Get(echo.HeaderLocation)) 73 | assert.Equal(t, "", rec.Body.String()) 74 | 75 | // index.html should produce 301 76 | rec = test(e, "http://foobar.net/index.html") 77 | assert.Equal(t, http.StatusMovedPermanently, rec.Code) 78 | assert.Equal(t, "http://foobar.net", rec.Header().Get(echo.HeaderLocation)) 79 | assert.Equal(t, "", rec.Body.String()) 80 | 81 | // Try with a different status code 82 | e = setup(http.StatusFound) 83 | rec = test(e, "http://foobar.net/index.html") 84 | assert.Equal(t, http.StatusFound, rec.Code) 85 | assert.Equal(t, "http://foobar.net", rec.Header().Get(echo.HeaderLocation)) 86 | assert.Equal(t, "", rec.Body.String()) 87 | } 88 | -------------------------------------------------------------------------------- /src/users/user_info.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import "github.com/google/uuid" 4 | 5 | // Public information about the user, can be hidden using `incognito` 6 | // Be sure to update src/users/features.go:publicFeatures() if adding/removing features 7 | type UserInfo struct { 8 | // Icon to display next to this user 9 | Icon string `json:"icon,omitempty"` 10 | // Cape this user should wear 11 | Cape string `json:"cape,omitempty"` 12 | // Color code of the text for nametags. e.g. LIGHT_PURPLE or BLUE 13 | TextColor string `json:"text_color,omitempty"` 14 | // Numeric ARGB color of the nametag background. Empty string for default. e.g. 1358954495 for pepsi's light gray 15 | BackgroundColor string `json:"bg_color,omitempty"` 16 | // Numeric ARGB color of the nametag border. Empty string for default. e.g. -1761673216 for pepsi's red 17 | BorderColor string `json:"border_color,omitempty"` 18 | } 19 | 20 | // NewUserInfo creates a UserInfo based on a User's roles and any special cases that apply to them 21 | func NewUserInfo(user User) *UserInfo { 22 | var info UserInfo 23 | 24 | if user.MinecraftID != nil { 25 | if special, ok := specialCases[*user.MinecraftID]; ok { 26 | if special.info != nil { 27 | info = *special.info 28 | } 29 | } 30 | } 31 | 32 | for _, role := range getRolesSorted(user.Roles) { // go in order from highest priority to least (aka numerically lowest to highest) 33 | role.applyDefaults(&info) 34 | } 35 | 36 | return &info 37 | } 38 | 39 | var specialCases = map[uuid.UUID]roleTemplate{ // TODO this should basically just be a SELECT * FROM customizations; 40 | // catgorl 41 | uuid.MustParse("2c3174fc-0c6b-4cfb-bb2b-0069bf7294d1"): { 42 | info: &UserInfo{ 43 | TextColor: "LIGHT_PURPLE", 44 | }, 45 | }, 46 | // leijurv 47 | uuid.MustParse("51dcd870-d33b-40e9-9fc1-aecdcff96081"): { 48 | info: &UserInfo{ 49 | TextColor: "RED", 50 | Icon: "https://files.impactclient.net/img/texture/speckles128.png", 51 | }, 52 | edition: &Edition{ 53 | Icon: "https://files.impactclient.net/img/texture/speckles128.png", 54 | }, 55 | }, 56 | // liejurv since leijurv is disabled 57 | uuid.MustParse("7b9c005b-011e-42de-bfb4-c0003f5c3a77"): { 58 | info: &UserInfo{ 59 | TextColor: "RED", 60 | Icon: "https://files.impactclient.net/img/texture/speckles128.png", 61 | }, 62 | edition: &Edition{ 63 | Icon: "https://files.impactclient.net/img/texture/speckles128.png", 64 | }, 65 | }, 66 | // triibu popstonia 67 | uuid.MustParse("8e563236-c7f5-4c82-aa27-c95bf3f4c322"): { 68 | info: &UserInfo{ 69 | Icon: "https://files.impactclient.net/img/texture/popstonia.png", 70 | }, 71 | }, 72 | // popstonia (rebane) 73 | uuid.MustParse("342fc44b-1fd1-4272-a4c3-a98a2df98abc"): { 74 | info: &UserInfo{ 75 | Icon: "https://files.impactclient.net/img/texture/popstonia.png", 76 | }, 77 | }, 78 | // HermeticLock 79 | uuid.MustParse("e97ff4c0-48bf-4c98-be34-248fdde2ffd3"): { 80 | info: &UserInfo{ 81 | TextColor: "RED", 82 | Icon: "https://i.imgur.com/aKt1g4H.jpg", 83 | Cape: "https://i.imgur.com/bvhC1Xk.png", 84 | }, 85 | edition: &Edition{ 86 | Icon: "https://i.imgur.com/aKt1g4H.jpg", 87 | }, 88 | }, 89 | // peanut 90 | uuid.MustParse("9d913c0a-3d57-4ce9-8b7d-689973312856"): { 91 | info: &UserInfo{ 92 | TextColor: "ORANGE", 93 | }, 94 | }, 95 | } 96 | -------------------------------------------------------------------------------- /src/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "github.com/ImpactDevelopment/ImpactServer/src/jwt" 6 | "github.com/ImpactDevelopment/ImpactServer/src/users" 7 | "github.com/labstack/echo/v4" 8 | "net/http" 9 | "os" 10 | "regexp" 11 | ) 12 | 13 | const userCtxKey = "user" 14 | 15 | var authBearerRegx = regexp.MustCompile(`^Bearer\s+(\S+)`) 16 | 17 | // GetUser returns the User object attached to the context, presumably by the RequireAuth middleware. 18 | // Otherwise it returns nil. 19 | func GetUser(c echo.Context) (user *users.User) { 20 | // Try to cast to *user.User, ignore if it failed, user probably just isn't set 21 | user, _ = c.Get(userCtxKey).(*users.User) 22 | return 23 | } 24 | 25 | var Auth = func(next echo.HandlerFunc) echo.HandlerFunc { 26 | return func(c echo.Context) error { 27 | if header := c.Request().Header.Get(echo.HeaderAuthorization); header != "" { 28 | if m := authBearerRegx.FindStringSubmatch(header); len(m) == 2 && m[1] != "" { 29 | token := m[1] 30 | 31 | // Verify the JWT 32 | user, err := jwt.Verify(token) 33 | if err != nil { 34 | return echo.NewHTTPError(http.StatusUnauthorized, "invalid token").SetInternal(err) 35 | } 36 | // Set the user context userCtxKey 37 | c.Set(userCtxKey, user) 38 | } 39 | } 40 | return next(c) 41 | } 42 | } 43 | 44 | // RequireAuth requires that the Authorization header be set, correctly formatted, and the token to be valid 45 | // For invalid token, it sends “401 - Unauthorized” response. 46 | // For missing or invalid Authorization header, it sends “400 - Bad Request”. 47 | var RequireAuth = func(next echo.HandlerFunc) echo.HandlerFunc { 48 | return func(c echo.Context) error { 49 | header := c.Request().Header.Get(echo.HeaderAuthorization) 50 | if header == "" { 51 | return echo.NewHTTPError(http.StatusBadRequest, "authentication is required") 52 | } 53 | if m := authBearerRegx.FindStringSubmatch(header); len(m) != 2 || m[1] == "" { 54 | return echo.NewHTTPError(http.StatusBadRequest, "invalid "+echo.HeaderAuthorization+" header") 55 | } 56 | return Auth(func(c echo.Context) error { 57 | // Require Auth to have succeeded 58 | if user := GetUser(c); user == nil { 59 | return echo.NewHTTPError(http.StatusUnauthorized, "no user found") 60 | } 61 | return next(c) 62 | })(c) 63 | } 64 | } 65 | 66 | // RequireRoles returns a middleware that requires the user to have at least one of the provided role IDs. 67 | // This middleware automatically calls RequireAuth 68 | func RequireRole(roles ...string) echo.MiddlewareFunc { 69 | // Figure out how best to print the roles in http errors 70 | var rolesString string 71 | if len(roles) == 1 { 72 | rolesString = fmt.Sprintf("%v", roles[0]) 73 | } else { 74 | rolesString = fmt.Sprintf("%v", roles) 75 | } 76 | 77 | return func(next echo.HandlerFunc) echo.HandlerFunc { 78 | return RequireAuth(func(c echo.Context) error { 79 | user := GetUser(c) 80 | if user == nil { 81 | return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Roles required but no user: %v", rolesString)) 82 | } 83 | 84 | // If any role matches, continue with request 85 | for _, role := range user.Roles { 86 | for _, required := range roles { 87 | if role.ID == required { 88 | return next(c) 89 | } 90 | } 91 | } 92 | 93 | // The user doesn't have any matching roles 94 | return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("Required at least one role from %v", rolesString)) 95 | }) 96 | } 97 | } 98 | 99 | func AuthGetParam() echo.MiddlewareFunc { 100 | return func(next echo.HandlerFunc) echo.HandlerFunc { 101 | return func(c echo.Context) error { 102 | auth := c.QueryParam("auth") + "0" 103 | if auth != os.Getenv("API_AUTH_SECRET") { 104 | return c.JSON(http.StatusForbidden, "auth wrong im sowwy") 105 | } 106 | return next(c) 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/api/v1/minecraft.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "log" 7 | "net/http" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/ImpactDevelopment/ImpactServer/src/database" 12 | 13 | "github.com/ImpactDevelopment/ImpactServer/src/cloudflare" 14 | "github.com/ImpactDevelopment/ImpactServer/src/users" 15 | "github.com/google/uuid" 16 | 17 | "github.com/labstack/echo/v4" 18 | ) 19 | 20 | var userData map[string]users.UserInfo 21 | var userDataNonHashed map[string]users.UserInfo 22 | 23 | var legacyRoles map[string]string 24 | 25 | // API Handler /minecraft/user/info 26 | func getUserInfo(c echo.Context) error { 27 | return c.JSON(http.StatusOK, userData) 28 | } 29 | 30 | // Legacy API handler /minecraft/user/:role/list 31 | func getRoleMembers(c echo.Context) error { 32 | ret := legacyRoles[c.Param("role")] 33 | if ret == "" { 34 | return c.NoContent(http.StatusNotFound) 35 | } 36 | return c.String(http.StatusOK, ret) 37 | } 38 | 39 | func init() { 40 | usersList := database.GetAllUsers() 41 | updatedData(usersList) 42 | updatedLegacyRoles(usersList) 43 | database.CallbackOnUsersTableUpdate(checkDatabaseForUpdatedUsers) 44 | } 45 | 46 | func checkDatabaseForUpdatedUsers() { 47 | usersList := database.GetAllUsers() 48 | if updatedData(usersList) { 49 | log.Println("MC UPDATE: Updated user info") 50 | cloudflare.PurgeURLs([]string{ 51 | "https://api.impactclient.net/v1/minecraft/user/info", 52 | }) 53 | } 54 | if updatedLegacyRoles(usersList) { 55 | log.Println("MC UPDATE: Updated user legacy data") 56 | cloudflare.PurgeURLs([]string{ 57 | "https://api.impactclient.net/v1/minecraft/user/staff/list", 58 | "https://api.impactclient.net/v1/minecraft/user/developer/list", 59 | "https://api.impactclient.net/v1/minecraft/user/pepsi/list", 60 | "https://api.impactclient.net/v1/minecraft/user/premium/list", 61 | }) 62 | } 63 | } 64 | 65 | func updatedData(usersList []users.User) bool { 66 | newUserData, newUnhashed := generateMap(usersList) 67 | // reflect.DeepEqual is slow, especially since this map is big 68 | if userData == nil || !reflect.DeepEqual(newUserData, userData) { 69 | userData = newUserData 70 | userDataNonHashed = newUnhashed 71 | return true 72 | } 73 | return false 74 | } 75 | 76 | func updatedLegacyRoles(usersList []users.User) bool { 77 | newLegacyRoles := generateLegacy(usersList) 78 | // reflect.DeepEqual is slow, especially since this map is big 79 | if legacyRoles == nil || !reflect.DeepEqual(newLegacyRoles, legacyRoles) { 80 | legacyRoles = newLegacyRoles 81 | return true 82 | } 83 | return false 84 | } 85 | 86 | func generateLegacy(usersList []users.User) map[string]string { 87 | m := make(map[string]string) 88 | for role, roleVal := range users.Roles { 89 | if !roleVal.LegacyList { 90 | continue 91 | } 92 | var list strings.Builder 93 | for _, user := range usersList { 94 | if !user.HasRoleWithID(role) { 95 | continue 96 | } 97 | if !user.LegacyEnabled { 98 | continue 99 | } 100 | if minecraftID := user.MinecraftID; minecraftID != nil { 101 | list.WriteString(minecraftID.String() + "\n") 102 | } 103 | } 104 | m[role] = list.String() 105 | } 106 | return m 107 | } 108 | 109 | func generateMap(usersList []users.User) (map[string]users.UserInfo, map[string]users.UserInfo) { 110 | data := make(map[string]users.UserInfo) 111 | unhashed := make(map[string]users.UserInfo) 112 | for _, user := range usersList { 113 | if !user.Incognito && user.MinecraftID != nil && user.UserInfo != nil { 114 | // if a user has cape disabled, they are trying to be incognito. we should send no entry at all. not good enough to send "HASH123":{}. 115 | data[hashUUID(*user.MinecraftID)] = *user.UserInfo 116 | unhashed[user.MinecraftID.String()] = *user.UserInfo 117 | } 118 | } 119 | return data, unhashed 120 | } 121 | 122 | func hashUUID(uuid uuid.UUID) string { 123 | hash := sha256.Sum256([]byte(uuid.String())) 124 | return hex.EncodeToString(hash[:]) 125 | } 126 | -------------------------------------------------------------------------------- /src/database/user.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "github.com/lib/pq" 7 | 8 | "github.com/ImpactDevelopment/ImpactServer/src/users" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | type userRow struct { 13 | id uuid.UUID 14 | email sql.NullString 15 | minecraft NullUUID 16 | discord sql.NullString 17 | passwdHash sql.NullString 18 | stripe sql.NullString 19 | legacyEnabled bool 20 | capeEnabled bool 21 | legacy bool 22 | roleList pq.StringArray 23 | } 24 | 25 | // rowScanner is implemented by sql.Row and sql.Rows 26 | type rowScanner interface { 27 | Scan(dest ...interface{}) error 28 | } 29 | 30 | // scanUsersView takes a sql.Row or sql.Rows and scans it into the user. 31 | // It is assumed the row is has the same column order as `users_view` 32 | func (user *userRow) scanUsersView(row rowScanner) error { 33 | return row.Scan(&user.id, &user.email, &user.minecraft, &user.discord, &user.passwdHash, &user.stripe, &user.capeEnabled, &user.legacyEnabled, &user.legacy, &user.roleList) 34 | } 35 | 36 | // makeUser converts a userRow into a users.User 37 | func (user *userRow) makeUser() users.User { 38 | ret := users.User{ 39 | LegacyEnabled: user.legacyEnabled, 40 | Incognito: !user.capeEnabled, 41 | Legacy: user.legacy, 42 | Roles: user.roles(), 43 | } 44 | if user.email.Valid { 45 | ret.Email = user.email.String 46 | } 47 | if user.minecraft.Valid { 48 | ret.MinecraftID = &user.minecraft.UUID 49 | } 50 | if user.discord.Valid { 51 | ret.DiscordID = user.discord.String 52 | } 53 | if user.passwdHash.Valid { 54 | ret.PasswordHash = user.passwdHash.String 55 | } 56 | if user.stripe.Valid { 57 | ret.StripeID = user.stripe.String 58 | } 59 | ret.UserInfo = users.NewUserInfo(ret) 60 | ret.ID = user.id 61 | return ret 62 | } 63 | 64 | func (user userRow) roles() []users.Role { 65 | var roles []users.Role 66 | for _, roleID := range user.roleList { 67 | if role, ok := users.Roles[roleID]; ok { 68 | roles = append(roles, role) 69 | } else { 70 | fmt.Printf("User %s has unknown role %s\n", user.id, roleID) 71 | } 72 | } 73 | return roles 74 | } 75 | 76 | // GetAllUsers returns... all the users 77 | func GetAllUsers() []users.User { 78 | if DB == nil { 79 | fmt.Println("Database not connected!") 80 | return nil 81 | } 82 | 83 | rows, err := DB.Query(`SELECT * FROM users_view`) 84 | if err != nil { 85 | panic(err) 86 | } 87 | defer rows.Close() 88 | 89 | ret := make([]users.User, 0) 90 | for rows.Next() { 91 | var r userRow 92 | err = r.scanUsersView(rows) 93 | if err != nil { 94 | panic(err) 95 | } 96 | ret = append(ret, r.makeUser()) 97 | } 98 | 99 | err = rows.Err() 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | return ret 105 | } 106 | 107 | // LookupUserByMinecraftID returns the matching user, or nil if not found 108 | func LookupUserByID(id uuid.UUID) *users.User { 109 | return lookupUserByField("user_id", id) 110 | } 111 | 112 | func LookupUserByEmail(email string) *users.User { 113 | return lookupUserByField("email", email) 114 | } 115 | 116 | // LookupUserByMinecraftID returns the matching user, or nil if not found 117 | func LookupUserByMinecraftID(minecraftID uuid.UUID) *users.User { 118 | return lookupUserByField("mc_uuid", minecraftID) 119 | } 120 | 121 | // LookupUserByDiscordID returns the matching user, or nil if not found 122 | func LookupUserByDiscordID(discordID string) *users.User { 123 | return lookupUserByField("discord_id", discordID) 124 | } 125 | 126 | func lookupUserByField(field string, value interface{}) *users.User { 127 | if DB == nil { 128 | fmt.Println("Database not connected!") 129 | return nil 130 | } 131 | 132 | var r userRow 133 | err := r.scanUsersView(DB.QueryRow(`SELECT * FROM users_view WHERE `+field+` = $1`, value)) 134 | if err != nil { 135 | if err == sql.ErrNoRows { 136 | return nil // no match 137 | } 138 | panic(err) // any other error 139 | } 140 | 141 | user := r.makeUser() 142 | return &user 143 | } 144 | -------------------------------------------------------------------------------- /static/account_explanation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |28 | The newer (4.8+) Impact Account system separates enabling premium and listing info (such as capes). It also adds a layer of privacy to showing info. 29 |
30 | Our API shows a SHA hashed version of your Minecraft ID, which prevents somebody from easily getting a list of Impact Account holders. 31 | However, it is still possible for them to check if individual Minecraft accounts have Impact Account features. 32 | They can do this by hashing the Minecraft ID and then looking through the API's list to see if the resultant hash is in the list. 33 | If you don't want this to be possible, you can enable Incognito Mode and you won't be included in the list. 34 | Not being in the list means no one can tell if you have an Impact Account or not, including other Impact users. 35 |
36 | Because listing info and enabling premium now separated, if you enable Incognito Mode you will still get access to other Impact Account features, such as donation perks like Ignite and nightly builds. 37 | These features use Minecraft's authentication system (the same one used by Multiplayer Servers). This means you must launch Impact while online and logged in to the Minecraft account that is linked to your Impact Account. 38 |
39 |
41 | The legacy (pre 4.8) Impact Account system uses plain text lists that list all Minecraft IDs with a given "role" (e.g. the premium role for donators).
42 |
44 | The problem with this is that anyone can easily get a list of Impact Accounts holders. 45 | For example an overzealous moderator might set up a system that bans anyone with an Impact Account automatically. 46 |
47 |48 | An additional issue is that the same list is used for enabling all features, from capes to Ignite, so it's not possible to have an "Incognito Mode" that doesn't disable everything. 49 | Luckily there's little reason to use an Impact version before 4.8, since 4.8 supports 1.12.2, 1.13.2 and 1.14.4. 50 | Therefore we don't recommend having "Legacy mode" enabled unless you understand the privacy concerns and want Impact Account features on Impact versions prior to 4.8. 51 |
52 |54 | This website and all the code that makes it run, including the Impact Account code, is open source. 55 | If you know how to read code why not browse our GitHub repo? 56 |
57 | Even better if you spot something that could be improved, you can report an Issue or even contribute directly by proposing a Pull Request! 58 |
59 |Impact supports Minecraft 1.16.5, 1.15.2, 1.14.4, 1.13.2, 1.12.2, 1.12.1, 1.12, and 1.11.2.
63 |Impact does NOT support cracked/non-premium launchers.
64 | 65 |
71 | Please note, this list is manually compiled. We cannot garentee its acuracy nor can we promise to keep it up to date.
72 |
73 | The projects listed here are third-party projects, not afiliated with Impact in any way. We do not endorse them and cannot garentee they are safe to use.
74 |
| Name | 80 |Cost | 81 |Notes | 82 |Versions | 83 |
|---|---|---|---|
| Future | 86 |One-time purchase | 87 |Future is a premium client with support for the latest versions, popular with players on various anarchy servers. Impact, sponsered by Future™ | 88 |1.14.4, 1.13.2, 1.12.2, 1.8.9 | 89 |
| Aristois | 92 |Free (optional donation perks) | 93 |Aristois is a free (closed-source) mod that aims to be always up-to-date with the latest minecraft versions. It can be installed with support for Fabric, Forge, MultiMC, and more. | 94 |1.8.9 through 1.18.2 | 95 |
25 | Click here to reset your password or copy the following link if that doesn't work: 26 |
27 |28 | %s 29 |` 30 | ) 31 | 32 | func resetPassword(c echo.Context) error { 33 | var body struct { 34 | Email string `json:"email" form:"email" query:"email"` 35 | } 36 | err := c.Bind(&body) 37 | if err != nil { 38 | return err 39 | } 40 | if body.Email == "" { 41 | return echo.NewHTTPError(http.StatusBadRequest, "email is required") 42 | } 43 | 44 | err = recaptcha.Verify(c) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | user := database.LookupUserByEmail(strings.TrimSpace(body.Email)) 50 | if user == nil { 51 | return echo.NewHTTPError(http.StatusBadRequest, "user not found") 52 | } 53 | 54 | token, err := genToken(user.ID) 55 | if err != nil { 56 | return err 57 | } 58 | resetURL := util.GetServerURL() 59 | resetURL.Path = "/forgotpassword.html" 60 | resetURL.RawQuery = url.Values{"token": {token.String()}}.Encode() 61 | 62 | // Send user an email, don't just give anyone a token lol 63 | message := mailgun.MG.NewMessage("Impact