docs: wire GitHub auth mode into UI
This commit is contained in:
parent
97f6ada0e0
commit
ef4f4a7cca
34
README.md
34
README.md
|
|
@ -71,6 +71,40 @@ docker-compose up
|
|||
| `SMTP_AUTH_TYPE` | The SMTP authentication type. Possible values: `PLAIN`, `LOGIN`, `NONE` | `NONE` |
|
||||
| `SMTP_ENCRYPTION` | The encryption method. Possible values: `NONE`, `SSL`, `SSLTLS`, `TLS`, `STARTTLS` | `STARTTLS` |
|
||||
| `SMTP_HELO` | Hostname to use for the HELO message. smtp-relay.gmail.com needs this set to anything but `localhost` | `localhost` |
|
||||
|
||||
### GitHub OAuth
|
||||
|
||||
Set `WGUI_AUTH_METHOD=github` to enable GitHub OAuth authentication. When enabled, the local password-based login form is replaced with a GitHub OAuth flow.
|
||||
|
||||
| Variable | Description | Default |
|
||||
|-----------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------|
|
||||
| `WGUI_AUTH_METHOD` | Authentication method. Possible values: `local`, `github` | `local` |
|
||||
| `WGUI_GITHUB_CLIENT_ID` | GitHub OAuth application client ID | N/A (required for github mode) |
|
||||
| `WGUI_GITHUB_CLIENT_SECRET` | GitHub OAuth application client secret | N/A (required for github mode) |
|
||||
| `WGUI_GITHUB_CLIENT_SECRET_FILE` | Optional filepath for the GitHub OAuth client secret. Leave `WGUI_GITHUB_CLIENT_SECRET` blank to take effect | N/A |
|
||||
| `WGUI_GITHUB_REDIRECT_URL` | GitHub OAuth callback URL (must match the callback URL configured in your GitHub App) | N/A (required for github mode) |
|
||||
| `WGUI_GITHUB_ALLOWED_USERS` | Comma-separated list of GitHub usernames allowed to login | N/A (required for github mode) |
|
||||
| `WGUI_GITHUB_ALLOWED_ORGS` | Comma-separated list of GitHub organizations whose members are allowed to login | N/A |
|
||||
| `WGUI_GITHUB_ADMIN_USERS` | Comma-separated list of GitHub usernames that should be granted admin privileges | N/A (required for github mode) |
|
||||
|
||||
#### GitHub App Setup
|
||||
|
||||
1. Create a GitHub App at [GitHub Developer Settings](https://github.com/settings/developers)
|
||||
- Set the callback URL to: `https://your-domain.com/auth/github/callback` (or `/your-base-path/auth/github/callback` if using a subpath)
|
||||
- Enable "Request user authorization (OAuth) during installation"
|
||||
- Set the following permissions:
|
||||
- Account: Read-only (for user email and username)
|
||||
2. Note the Client ID and Client Secret from the app
|
||||
3. Configure the environment variables
|
||||
|
||||
#### Mode Switching
|
||||
|
||||
- Switching from `local` to `github` mode: Existing sessions remain valid until they expire. Users will see the GitHub login button on next login.
|
||||
- Switching from `github` to `local` mode: Existing sessions remain valid until they expire. Users who log in again will use the password form.
|
||||
- First startup after upgrading to a build that includes GitHub OAuth support: Existing sessions from the previous version are invalidated once and users must re-authenticate.
|
||||
|
||||
> **Note:** After upgrading wireguard-ui with GitHub OAuth support, sessions from the previous version will be invalidated. Users will need to re-authenticate.
|
||||
|
||||
### Defaults for server configuration
|
||||
|
||||
These environment variables are used to control the default server settings used when initializing the database.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,258 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ngoduykhanh/wireguard-ui/util"
|
||||
)
|
||||
|
||||
func TestLoginPage_GitHubModeShowsOAuthButton(t *testing.T) {
|
||||
content, err := os.ReadFile("../templates/login.html")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read login.html: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, "auth/github/start") {
|
||||
t.Errorf("expected login page to contain 'auth/github/start' in github mode")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, ".authMethod") {
|
||||
t.Errorf("expected login page to use .authMethod for mode switching")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, "btn-github") && !strings.Contains(body, "github-login") {
|
||||
t.Errorf("expected login page to have GitHub login button")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginPage_LocalModeKeepsPasswordForm(t *testing.T) {
|
||||
content, err := os.ReadFile("../templates/login.html")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read login.html: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, `id="username"`) {
|
||||
t.Errorf("expected login page to contain username input in local mode")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, `id="password"`) {
|
||||
t.Errorf("expected login page to contain password input in local mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginPage_GitHubModeWithNextParam(t *testing.T) {
|
||||
content, err := os.ReadFile("../templates/login.html")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read login.html: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, "{{if .authMethod}}") && !strings.Contains(body, ".authMethod") {
|
||||
t.Errorf("expected login page to use .authMethod to conditionally show OAuth button")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, "auth/github/start") {
|
||||
t.Errorf("expected login page to contain 'auth/github/start' for OAuth flow")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginPage_GitHubModeDoesNotShowPasswordForm(t *testing.T) {
|
||||
content, err := os.ReadFile("../templates/login.html")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read login.html: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
lines := strings.Split(body, "\n")
|
||||
inGitHubBlock := false
|
||||
hasPasswordInGitHubBlock := false
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "{{if eq .authMethod \"github\"}}") {
|
||||
inGitHubBlock = true
|
||||
}
|
||||
if inGitHubBlock && strings.Contains(line, "{{else}}") {
|
||||
inGitHubBlock = false
|
||||
}
|
||||
if inGitHubBlock && strings.Contains(line, `id="password"`) {
|
||||
hasPasswordInGitHubBlock = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasPasswordInGitHubBlock {
|
||||
t.Errorf("expected password input NOT to be in github mode block")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubModeUsersSettingsNotRegistered(t *testing.T) {
|
||||
content, err := os.ReadFile("../main.go")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main.go: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
lines := strings.Split(body, "\n")
|
||||
inGitHubBlock := false
|
||||
hasUsersSettingsInGitHubBlock := false
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "AuthMethodGitHub") {
|
||||
inGitHubBlock = true
|
||||
}
|
||||
if inGitHubBlock && strings.Contains(line, "} else {") {
|
||||
inGitHubBlock = false
|
||||
break
|
||||
}
|
||||
if inGitHubBlock && strings.Contains(line, "users-settings") {
|
||||
hasUsersSettingsInGitHubBlock = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasUsersSettingsInGitHubBlock {
|
||||
t.Errorf("users-settings route should not be registered in github mode block")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubStartHandlerRegistered(t *testing.T) {
|
||||
content, err := os.ReadFile("../main.go")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main.go: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, "auth/github/start") {
|
||||
t.Errorf("main.go should register auth/github/start route for github mode")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, "GitHubStart") {
|
||||
t.Errorf("main.go should use GitHubStart handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubCallbackHandlerRegistered(t *testing.T) {
|
||||
content, err := os.ReadFile("../main.go")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main.go: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, "auth/github/callback") {
|
||||
t.Errorf("main.go should register auth/github/callback route for github mode")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, "GitHubCallback") {
|
||||
t.Errorf("main.go should use GitHubCallback handler")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubModeProfileAPIRegistered(t *testing.T) {
|
||||
content, err := os.ReadFile("../main.go")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read main.go: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
lines := strings.Split(body, "\n")
|
||||
inGitHubBlock := false
|
||||
hasProfileAPIInGitHubBlock := false
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "AuthMethodGitHub") {
|
||||
inGitHubBlock = true
|
||||
}
|
||||
if inGitHubBlock && strings.Contains(line, "} else {") {
|
||||
inGitHubBlock = false
|
||||
break
|
||||
}
|
||||
if inGitHubBlock && strings.Contains(line, `"/api/user/:username"`) {
|
||||
hasProfileAPIInGitHubBlock = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasProfileAPIInGitHubBlock {
|
||||
t.Errorf("github mode should register /api/user/:username for profile page")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilePage_GitHubModeReadOnlyInfo(t *testing.T) {
|
||||
content, err := os.ReadFile("../templates/profile.html")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read profile.html: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, ".authMethod") {
|
||||
t.Errorf("expected profile page to use .authMethod for mode switching")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, "github") {
|
||||
t.Errorf("expected profile page to mention github in github mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfilePage_LocalModeHasPasswordForm(t *testing.T) {
|
||||
content, err := os.ReadFile("../templates/profile.html")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read profile.html: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, `id="password"`) {
|
||||
t.Errorf("expected profile page to contain password input in local mode")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBaseTemplate_GitHubModeHidesUserSettings(t *testing.T) {
|
||||
content, err := os.ReadFile("../templates/base.html")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read base.html: %v", err)
|
||||
}
|
||||
body := string(content)
|
||||
|
||||
if !strings.Contains(body, ".authMethod") {
|
||||
t.Errorf("expected base.html to use .authMethod for mode switching")
|
||||
}
|
||||
|
||||
if !strings.Contains(body, "users-settings") {
|
||||
t.Errorf("expected base.html to contain 'users-settings' link")
|
||||
}
|
||||
|
||||
lines := strings.Split(body, "\n")
|
||||
inAdminBlock := false
|
||||
hasUserSettingsWithoutGithubCheck := false
|
||||
|
||||
for _, line := range lines {
|
||||
if strings.Contains(line, "{{if .baseData.Admin}}") {
|
||||
inAdminBlock = true
|
||||
}
|
||||
if inAdminBlock && strings.Contains(line, "{{end}}") && !strings.Contains(line, "{{else}}") {
|
||||
inAdminBlock = false
|
||||
}
|
||||
if inAdminBlock && strings.Contains(line, "users-settings") && strings.Contains(line, "{{if") && strings.Contains(line, "github") {
|
||||
hasUserSettingsWithoutGithubCheck = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasUserSettingsWithoutGithubCheck {
|
||||
return
|
||||
}
|
||||
t.Errorf("users-settings should be hidden in github mode")
|
||||
}
|
||||
|
||||
func TestUtil_CurrentAuthMethodType(t *testing.T) {
|
||||
if string(util.AuthMethodGitHub) != "github" {
|
||||
t.Errorf("AuthMethodGitHub should be 'github', got '%s'", util.AuthMethodGitHub)
|
||||
}
|
||||
|
||||
if string(util.AuthMethodLocal) != "local" {
|
||||
t.Errorf("AuthMethodLocal should be 'local', got '%s'", util.AuthMethodLocal)
|
||||
}
|
||||
}
|
||||
|
|
@ -148,6 +148,7 @@
|
|||
</a>
|
||||
</li>
|
||||
{{if not .loginDisabled}}
|
||||
{{if and (not (eq .authMethod "github")) .baseData.Admin}}
|
||||
<li class="nav-item">
|
||||
<a href="{{.basePath}}/users-settings" class="nav-link {{if eq .baseData.Active "users-settings" }}active{{end}}">
|
||||
<i class="nav-icon fas fa-cog"></i>
|
||||
|
|
@ -159,6 +160,7 @@
|
|||
</li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<li class="nav-header">工具</li>
|
||||
<li class="nav-item">
|
||||
|
|
|
|||
|
|
@ -31,6 +31,15 @@
|
|||
<div class="card">
|
||||
<div class="card-body login-card-body">
|
||||
<p class="login-box-msg">登录以开始使用</p>
|
||||
{{if eq .authMethod "github"}}
|
||||
<div class="text-center">
|
||||
<p class="mb-4">使用 GitHub 账户登录</p>
|
||||
<a href="{{.basePath}}/auth/github/start" id="btn_github_login" class="btn btn-github btn-block btn-lg">
|
||||
<i class="fab fa-github mr-2"></i> 使用 GitHub 登录
|
||||
</a>
|
||||
<p id="message" class="mt-3"></p>
|
||||
</div>
|
||||
{{else}}
|
||||
<form action="" method="post">
|
||||
<div class="input-group mb-3">
|
||||
<input id="username" type="text" class="form-control" placeholder="用户名">
|
||||
|
|
@ -67,6 +76,7 @@
|
|||
<div class="text-center mb-3">
|
||||
<p id="message"></p>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<!-- /.login-card-body -->
|
||||
</div>
|
||||
|
|
@ -87,10 +97,26 @@
|
|||
if (nextURL && /(?:^\/[a-zA-Z_])|(?:^\/$)/.test(nextURL.trim())) {
|
||||
window.location.href = nextURL;
|
||||
} else {
|
||||
window.location.href = '/{{.basePath}}';
|
||||
}
|
||||
window.location.href = '{{.basePath}}/';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{if eq .authMethod "github"}}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$("#btn_github_login").click(function (e) {
|
||||
e.preventDefault();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const nextURL = urlParams.get('next');
|
||||
let redirectUrl = '{{.basePath}}/auth/github/start';
|
||||
if (nextURL) {
|
||||
redirectUrl += '?next=' + encodeURIComponent(nextURL);
|
||||
}
|
||||
window.location.href = redirectUrl;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{{else}}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('form').on('submit', function(e) {
|
||||
|
|
@ -127,4 +153,5 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -20,17 +20,43 @@
|
|||
<div class="row">
|
||||
<!-- left column -->
|
||||
<div class="col-md-6">
|
||||
{{if eq .authMethod "github"}}
|
||||
<div class="card card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">身份信息</h3>
|
||||
</div>
|
||||
<!-- /.card-header -->
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label class="control-label">用户名</label>
|
||||
<input type="text" class="form-control" value="" id="username" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">显示名称</label>
|
||||
<input type="text" class="form-control" value="" id="display_name" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">认证来源</label>
|
||||
<input type="text" class="form-control" value="GitHub" id="auth_source" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">角色</label>
|
||||
<input type="text" class="form-control" value="" id="role" readonly>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="card card-success">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">更新用户信息</h3>
|
||||
} </div>
|
||||
</div>
|
||||
<!-- /.card-header -->
|
||||
<!-- form start -->
|
||||
<form role="form" id="frm_profile" name="frm_profile">
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="username" class="control-label">用户名</label>
|
||||
} <input type="text" class="form-control" name="username" id="username"
|
||||
<input type="text" class="form-control" name="username" id="username"
|
||||
value="">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
@ -46,6 +72,7 @@
|
|||
</form>
|
||||
</div>
|
||||
<!-- /.card -->
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.row -->
|
||||
|
|
@ -71,6 +98,10 @@
|
|||
$("#username").val(user.username);
|
||||
previous_username = user.username;
|
||||
admin = user.admin;
|
||||
{{if eq .authMethod "github"}}
|
||||
$("#display_name").val(user.display_name || user.username);
|
||||
$("#role").val(user.admin ? '管理员' : '普通用户');
|
||||
{{end}}
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
|
|
@ -78,8 +109,10 @@
|
|||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
{{if not (eq .authMethod "github")}}
|
||||
<script>
|
||||
function updateUserInfo() {
|
||||
const username = $("#username").val();
|
||||
const password = $("#password").val();
|
||||
|
|
@ -93,7 +126,7 @@
|
|||
data: JSON.stringify(data),
|
||||
success: function (data) {
|
||||
toastr.success("已成功更新用户信息");
|
||||
} location.reload();
|
||||
location.reload();
|
||||
},
|
||||
error: function (jqXHR, exception) {
|
||||
const responseJson = jQuery.parseJSON(jqXHR.responseText);
|
||||
|
|
@ -117,7 +150,7 @@
|
|||
messages: {
|
||||
username: {
|
||||
required: "请输入用户名",
|
||||
} }
|
||||
}
|
||||
},
|
||||
errorElement: 'span',
|
||||
errorPlacement: function (error, element) {
|
||||
|
|
@ -133,4 +166,5 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{ end }}
|
||||
|
|
|
|||
Loading…
Reference in New Issue