Merge pull request #104 from olblak/config/02

BoardElections && CSS improvements
This commit is contained in:
R. Tyler Croy 2017-05-12 08:23:41 -07:00 committed by GitHub
commit fa149f7287
23 changed files with 519 additions and 156 deletions

View File

@ -5,25 +5,31 @@ LABEL \
Project="https://github.com/jenkins-infra/account-app" \
Maintainer="infra@lists.jenkins-ci.org"
ENV ELECTION_LOGDIR=/var/log/accountapp/elections
ENV CIRCUIT_BREAKER_FILE=/etc/accountapp/circuitBreaker.txt
ENV SMTP_SERVER=localhost
ENV JIRA_URL=https://issues.jenkins-ci.org
ENV APP_URL=http://accounts.jenkins.io/
EXPOSE 8080
ENV CIRCUIT_BREAKER_FILE /etc/accountapp/circuitBreaker.txt
# /home/jetty/.app is apparently needed by Stapler for some weird reason. O_O
RUN \
mkdir -p /home/jetty/.app &&\
mkdir -p /etc/accountapp
RUN mkdir -p /home/jetty/.app &&\
mkdir -p /etc/accountapp &&\
mkdir -p $ELECTION_LOGDIR
COPY config.properties.example /etc/accountapp/config.properties.example
COPY circuitBreaker.txt /etc/accountapp/circuitBreaker.txt
COPY entrypoint.sh /entrypoint.sh
RUN \
chmod 0755 /entrypoint.sh &&\
chown -R jetty:root /etc/accountapp
COPY build/libs/accountapp*.war /var/lib/jetty/webapps/ROOT.war
RUN chmod 0755 /entrypoint.sh &&\
chown -R jetty:root /etc/accountapp &&\
chown -R jetty:root /var/lib/jetty &&\
chown -R jetty:root $ELECTION_LOGDIR
USER jetty
ENTRYPOINT /entrypoint.sh

2
Jenkinsfile vendored
View File

@ -11,7 +11,7 @@ node('docker') {
stage('Build') {
timestamps {
checkout scm
docker.image('java:8').inside {
docker.image('openjdk:8-jdk').inside {
sh './gradlew --no-daemon --info war'
archiveArtifacts artifacts: 'build/libs/*.war', fingerprint: true
}

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
.PHONY: build clean run
current_dir := $(shell pwd)
current_user := $(shell id -u)
build:
docker run --rm -v $(current_dir):/opt/accountapp/ -u $(current_user):$(current_user) -w /opt/accountapp --entrypoint ./gradlew openjdk:8-jdk --no-daemon --info war
docker-compose build run
clean:
docker run --rm -v $(current_dir):/opt/accountapp/ -u $(current_user):$(current_user) -w /opt/accountapp --entrypoint ./gradlew openjdk:8-jdk clean
docker-compose rm -f run
run:
docker-compose up run

View File

@ -5,7 +5,7 @@
First, set up a tunnel to Jenkins LDAP server. Run the following command and
keep the terminal open:
ssh -L 9389:localhost:389 ldap.jenkins.io
ssh -4 -L 9389:localhost:389 ldap.jenkins.io
Create `config.properties` in the same directory as `pom.xml`. See the
`Parameters` class for the details, but it should look something like the
@ -37,17 +37,20 @@ _Require ssh tunnel to an ldap server and an WAR archive_
* Create the file ```.env``` used by docker-compose to load configuration
.env example
```
LDAP_URL=server=ldap://localhost:9389/
LDAP_PASSWORD=<insert your ldap password>
APP_URL=http://localhost:8080/
ELECTION_CANDIDATES=alice,bob
ELECTION_CLOSE=2038/01/19
ELECTION_OPEN=1970/01/01
JIRA_USERNAME=<insert your jira username>
JIRA_PASSWORD=<insert your jira password>
JIRA_URL=https://issues.jenkins-ci.org
SMTP_SERVER=localhost
RECAPTCHA_PRIVATE_KEY=recaptcha_private_key
RECAPTCHA_PUBLIC_KEY=recaptcha_public_key
APP_URL=http://localhost:8080/
LDAP_URL=server=ldap://localhost:9389/
LDAP_PASSWORD=<insert your ldap password>
LDAP_MANAGER_DN=cn=admin,dc=jenkins-ci,dc=org
LDAP_NEW_USER_BASE_DN=ou=people,dc=jenkins-ci,dc=org
RECAPTCHA_PRIVATE_KEY=recaptcha_private_key
RECAPTCHA_PUBLIC_KEY=recaptcha_public_key
SMTP_SERVER=localhost
```
* Run docker-compose
```docker-compose up --build accountapp```
@ -72,6 +75,10 @@ we may want to use environment variable.
```
* APP_URL
* CIRCUIT_BREAKER_FILE
* ELECTION_CANDIDATES coma separated list of candidates
* ELECTION_CLOSE date election will close. yyyy/MM/dd
* ELECTION_OPEN date election will open. yyyy/MM/dd
* ELECTION_LOGDIR
* JIRA_PASSWORD
* JIRA_URL
* JIRA_USERNAME
@ -83,3 +90,9 @@ we may want to use environment variable.
* RECAPTCHA_PRIVATE_KEY
* SMTP_SERVER
```
## Makefile
``` make build```: Build build/libs/accountapp-2.5.war and docker image
``` make run ```: Run docker container
``` make clean ```: Clean build environment

View File

@ -24,7 +24,7 @@ dependencies {
compile('org.kohsuke.stapler:stapler-jelly:[1.203,2.0)')
compile 'org.kohsuke.stapler:stapler-openid-server:[1.0,2.0)'
compile 'org.jvnet.hudson:commons-jelly-tags-define:1.0.1-hudson-20071021'
compile 'commons-jelly:commons-jelly-tags-define:1.0'
compile 'javax.mail:mail:[1.4,2.0)'
compile 'javax.activation:activation:1.1.1'
@ -33,6 +33,13 @@ dependencies {
exclude module: 'javamail'
}
compile 'com.esotericsoftware.yamlbeans:yamlbeans:1.11'
compile 'org.webjars:webjars-servlet-2.x:1.5'
compile 'org.webjars:jquery:3.2.0'
compile 'org.webjars:jquery-ui:1.12.1'
compile 'org.webjars.bower:fontawesome:4.7.0'
testCompile 'junit:junit:[4.8.1,5.0)'
}

View File

@ -8,15 +8,24 @@ managerPassword=LDAP_PASSWORD
newUserBaseDN=LDAP_NEW_USER_BASE_DN
# Host which accountapp can use for sending out password reset and other emails
# Optional: Default value set to localhost
smtpServer=SMTP_SERVER
recaptchaPublicKey=RECAPTCHA_PUBLIC_KEY
recaptchaPrivateKey=RECAPTCHA_PRIVATE_KEY
# Optional: Default value set to http://accounts.jenkins.io/
url=APP_URL
# Create this file on the host machine in order to temporarily disable account
# creation
# Optional: Default value set to /etc/accountapp/circuitBreaker.txt
circuitBreakerFile=CIRCUIT_BREAKER_FILE
electionCandidates=ELECTION_CANDIDATES
electionClose=ELECTION_CLOSE
electionOpen= ELECTION_OPEN
# Optional: Default value set to /var/log/accountapp/elections
electionLogDir=ELECTION_LOGDIR
# vim: ft=conf

View File

@ -1,6 +1,6 @@
version: '3'
services:
accountapp:
run:
build: .
image: accountapp:latest
env_file: .env

View File

@ -18,6 +18,18 @@ init_config_properties() {
: "${LDAP_NEW_USER_BASE_DN:? Require ldap new user base DN}"
: "${CIRCUIT_BREAKER_FILE:? Require circuitBreaker file}"
# Elections configurations
: "${ELECTION_CANDIDATES:? Required coma separated list of candidates}"
: "${ELECTION_CLOSE:? Required date election will close. yyyy/MM/dd}"
: "${ELECTION_OPEN:? date election will open. yyyy/MM/dd }"
: "${ELECTION_LOGDIR:? Require election log directory }"
#Directory to store collected votes. assume this path is well persisted/backup
if [ ! -d "${ELECTION_LOGDIR}" ]; then
mkdir -p "${ELECTION_LOGDIR}"
chown jetty: "$ELECTION_LOGDIR"
fi
cp /etc/accountapp/config.properties.example /etc/accountapp/config.properties
@ -31,6 +43,10 @@ init_config_properties() {
sed -i "s#LDAP_MANAGER_DN#$LDAP_MANAGER_DN#" /etc/accountapp/config.properties
sed -i "s#LDAP_NEW_USER_BASE_DN#$LDAP_NEW_USER_BASE_DN#" /etc/accountapp/config.properties
sed -i "s#CIRCUIT_BREAKER_FILE#$CIRCUIT_BREAKER_FILE#" /etc/accountapp/config.properties
sed -i "s#ELECTION_CANDIDATES#$ELECTION_CANDIDATES#" /etc/accountapp/config.properties
sed -i "s#ELECTION_OPEN#$ELECTION_OPEN#" /etc/accountapp/config.properties
sed -i "s#ELECTION_CLOSE#$ELECTION_CLOSE#" /etc/accountapp/config.properties
sed -i "s#ELECTION_LOGDIR#$ELECTION_LOGDIR#" /etc/accountapp/config.properties
}
if [ ! -f /etc/accountapp/config.properties ]; then

View File

@ -16,6 +16,7 @@ import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.config.ConfigurationLoader;
import javax.annotation.CheckForNull;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonReader;
@ -51,6 +52,7 @@ import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.rmi.RemoteException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
@ -72,8 +74,7 @@ import static javax.naming.directory.DirContext.REMOVE_ATTRIBUTE;
import static javax.naming.directory.DirContext.REPLACE_ATTRIBUTE;
import static javax.naming.directory.SearchControls.SUBTREE_SCOPE;
import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.jenkinsci.account.LdapAbuse.REGISTRATION_DATE;
import static org.jenkinsci.account.LdapAbuse.SENIOR_STATUS;
import static org.jenkinsci.account.LdapAbuse.*;
/**
* Root of the account application.
@ -94,17 +95,22 @@ public class Application {
// not exposing this to UI
/*package*/ final CircuitBreaker circuitBreaker;
public Application(Parameters params) throws IOException {
private BoardElection boardElection;
public Application(Parameters params) throws Exception {
this.params = params;
this.openid = new JenkinsOpenIDServer(this);
this.circuitBreaker = new CircuitBreaker(params);
if (params.electionCandidates() != null) {
this.boardElection = new BoardElection(this, params);
}
}
public Application(Properties config) throws IOException {
public Application(Properties config) throws Exception {
this(ConfigurationLoader.from(config).as(Parameters.class));
}
public Application(File config) throws IOException {
public Application(File config) throws Exception {
this(ConfigurationLoader.from(config).as(Parameters.class));
}
@ -548,7 +554,7 @@ public class Application {
}
// to limit the redirect to this application, require that the from URL starts from '/'
if (from==null || !from.startsWith("/")) from="/myself/";
if (from==null || !from.startsWith("/")) from="/";
return HttpResponses.redirectTo(from);
}
@ -573,12 +579,12 @@ public class Application {
}
public boolean isLoggedIn() {
return current() !=null;
return Myself.current() !=null;
}
public boolean isAdmin() {
Myself myself = current();
return myself!=null && myself.isAdmin();
Myself myself = Myself.current();
return myself !=null && myself.isAdmin();
}
/**
@ -586,22 +592,38 @@ public class Application {
* send the user to the login page.
*/
public Myself getMyself() {
Myself myself = current();
if (myself==null) {
// needs to login
StaplerRequest req = Stapler.getCurrentRequest();
StringBuilder from = new StringBuilder(req.getRequestURI());
if (req.getQueryString()!=null)
from.append('?').append(req.getQueryString());
try {
throw HttpResponses.redirectViaContextPath("login?from="+ URLEncoder.encode(from.toString(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
Myself myself = Myself.current();
if (myself ==null) {
needToLogin();
}
return myself;
}
private void needToLogin() {
// needs to login
StaplerRequest req = Stapler.getCurrentRequest();
StringBuilder from = new StringBuilder(req.getRequestURI());
if (req.getQueryString()!=null)
from.append('?').append(req.getQueryString());
try {
throw HttpResponses.redirectViaContextPath("login?from="+ URLEncoder.encode(from.toString(),"UTF-8"));
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
public @CheckForNull BoardElection getBoardElection() {
return boardElection;
}
public @CheckForNull BoardElection getElection() {
Myself myself = Myself.current();
if (myself ==null) {
needToLogin();
}
return boardElection;
}
/**
* This is a test endpoint to make sure the reverse proxy forwarding is working.
*/
@ -609,10 +631,6 @@ public class Application {
return HttpResponses.plainText(header);
}
private Myself current() {
return (Myself) Stapler.getCurrentRequest().getSession().getAttribute(Myself.class.getName());
}
public AdminUI getAdmin() {
return getMyself().isAdmin() ? new AdminUI(this) : null;
}

View File

@ -0,0 +1,93 @@
package org.jenkinsci.account;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.naming.NamingException;
import java.io.FileWriter;
import java.io.FileReader;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import com.esotericsoftware.yamlbeans.YamlReader;
import com.esotericsoftware.yamlbeans.YamlWriter;
/**
* @author <a href="mailto:nicolas.deloof@gmail.com">Nicolas De Loof</a>
*/
public class BoardElection {
private final Date open;
private final Date close;
private final Application app;
private final String[] candidates;
private final String log;
public BoardElection(Application application, Parameters params) throws Exception {
this.app = application;
final SimpleDateFormat format = new SimpleDateFormat("yyyy/MM/dd");
open = format.parse(params.electionOpen());
close = format.parse(params.electionClose());
candidates = params.electionCandidates().split(",");
log = params.electionLogDir() + "/" + params.electionClose().replaceAll("/","") + ".yaml";
File f = new File(log);
f.createNewFile();
}
@RequirePOST
public HttpResponse doVote(@QueryParameter String vote) throws NamingException, IOException {
final Myself user = Myself.current();
if (user == null) {
throw new UserError("Need to be authenticated");
}
if (!isOpen()) {
throw new UserError("Election is closed");
}
List<String> selected = new ArrayList<>();
// TODO check user is "old enough"
for (String id : vote.split(",")) {
selected.add(candidates[Integer.parseInt(id)]);
}
// TODO build a nicer yaml structure document
YamlReader reader = new YamlReader(new FileReader(log));
Object object = new Object();
object = reader.read();
if (null == object){
String init_content = "election_close: " + close;
YamlReader init = new YamlReader(init_content);
object = init.read();
}
Map map = (Map)object;
map.put(user.userId,StringUtils.join(selected, ","));
YamlWriter writer = new YamlWriter(new FileWriter(log, false));
writer.write(map);
writer.close();
// TODO store
return new HttpRedirect("done");
}
public String[] getCandidates() {
return candidates;
}
public boolean isOpen() {
final long now = System.currentTimeMillis();
return open != null && close != null && open.getTime() <= now && close.getTime() >= now;
}
}

View File

@ -3,6 +3,7 @@ package org.jenkinsci.account;
import org.kohsuke.stapler.HttpRedirect;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.Stapler;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
@ -13,7 +14,8 @@ import javax.naming.ldap.LdapContext;
import java.util.Set;
import java.util.logging.Logger;
import static org.jenkinsci.account.LdapAbuse.*;
import static org.jenkinsci.account.LdapAbuse.GITHUB_ID;
import static org.jenkinsci.account.LdapAbuse.SSH_KEYS;
/**
* Represents the current user logged in and operations on it.
@ -29,16 +31,30 @@ public class Myself {
private final Set<String> groups;
public Myself(Application parent, String dn, Attributes attributes, Set<String> groups) throws NamingException {
this(parent, dn,
getAttribute(attributes,"givenName"),
getAttribute(attributes,"sn"),
getAttribute(attributes,"mail"),
getAttribute(attributes,"cn"),
getAttribute(attributes, GITHUB_ID),
getAttribute(attributes, SSH_KEYS),
groups);
}
public Myself(Application parent, String dn, String firstName, String lastName, String email, String userId, String githubId, String sshKeys, Set<String> groups) {
this.parent = parent;
this.dn = dn;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.userId = userId;
this.githubId = githubId;
this.sshKeys = sshKeys;
this.groups = groups;
}
firstName = getAttribute(attributes,"givenName");
lastName = getAttribute(attributes,"sn");
email = getAttribute(attributes,"mail");
userId = getAttribute(attributes,"cn");
githubId = getAttribute(attributes, GITHUB_ID);
sshKeys = getAttribute(attributes, SSH_KEYS);
public static Myself current() {
return (Myself) Stapler.getCurrentRequest().getSession().getAttribute(Myself.class.getName());
}
/**
@ -48,7 +64,7 @@ public class Myself {
return groups.contains("admins");
}
private String getAttribute(Attributes attributes, String name) throws NamingException {
private static String getAttribute(Attributes attributes, String name) throws NamingException {
Attribute att = attributes.get(name);
return att!=null ? (String) att.get() : null;
}

View File

@ -1,5 +1,7 @@
package org.jenkinsci.account;
import java.util.Date;
/**
* Configuration of the application that needs to be set outside the application.
*
@ -32,4 +34,12 @@ public interface Parameters {
* File that activates a circuit breaker, a temporary shutdown of a sign-up service.
*/
String circuitBreakerFile();
String electionCandidates();
String electionLogDir();
String electionOpen();
String electionClose();
}

View File

@ -1,22 +1,39 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Administration">
<div class="row">
<div class="col-sm-4">
<p>
Confirm creating the following user
</p>
<form method="post" action="doSignup">
<h5>User ID</h5>
<input type="text" name="userid" value="${request.getParameter('userId')}" class="text" />
<div class="form-group">
<label>User ID</label>
<input type="text" name="userid" value="${request.getParameter('userId')}" class="form-control text" placeholder="Userid" />
</div>
<h5>First Name</h5>
<input type="text" name="firstName" value="${request.getParameter('firstName')}" class="text"/>
<div class="form-group">
<label>First Name</label>
<input type="text" name="firstName" value="${request.getParameter('firstName')}" class="form-control text" placeholder="First Name" />
</div>
<h5>Last Name</h5>
<input type="text" name="lastName" value="${request.getParameter('lastName')}" class="text"/>
<div class="form-group">
<label>Last Name</label>
<input type="text" name="lastName" value="${request.getParameter('lastName')}" class="form-control text" placeholder="Last Name"/>
</div>
<h5>E-mail</h5>
<input type="text" name="email" value="${request.getParameter('email')}" class="text"/>
<div class="form-group">
<label>E-mail</label>
<input type="email" name="email" value="${request.getParameter('email')}" class="form-control text" placeholder="Last Name"/>
</div>
<button type="submit" class="btn btn-default">Sign Up</button>
<input type="submit" style="margin-top:2em; display:block"/>
</form>
</div>
<div class="col-sm-8">
</div>
</div>
</t:layout>
</j:jelly>

View File

@ -1,31 +1,18 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Account self-service app">
<div style="width: 100%;">
<p>
You can create/manage your user account that you use for accessing
<a href="http://wiki.jenkins-ci.org/" target="_top">Wiki</a> and <a href="http://issues.jenkins-ci.org/" target="_top">JIRA</a>,
</p>
</div>
<style>
#account-menu H1 {
margin: 1rem;
}
</style>
<p>
You can create/manage your user account that you use for accessing
<a href="http://wiki.jenkins-ci.org/" target="_top">Wiki</a> and <a href="http://issues.jenkins-ci.org/" target="_top">JIRA</a>,
</p>
<div id="account-menu">
<h1><a href="signup">Create a new account</a></h1>
<h1><a href="passwordReset">Reset the password</a></h1>
<h1><a href="myself/">Update your profile</a></h1>
<j:if test="${it.isLoggedIn()}">
<h1><a href="logout">Logout</a></h1>
</j:if>
<j:if test="${it.isAdmin()}">
<h1><a href="admin/">Administration</a></h1>
<j:if test="${not it.isLoggedIn()}">
<script type="text/javascript">
window.location.href = "login"
</script>
<h1><a href="login">Login</a></h1>
<h1><a href="signup">Create a new account</a></h1>
<h1><a href="passwordReset">Reset the password</a></h1>
</j:if>
</div>
</t:layout>

View File

@ -1,25 +1,35 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Login">
<h1>Login</h1>
<div class="row">
<div class="col-sm-6">
<h1 class="text-center">Login</h1>
<form method="post" action="doLogin">
<h5>User ID</h5>
<input type="text" name="userid" class="text" id="userid"/>
<div class="form-group">
<label class="sr-only" for="userid">Login</label>
<input type="text" name="userid" class="form-control text" id="userid" placeholder="Userid"/>
</div>
<h5>Password</h5>
<input type="password" name="password" class="text"/>
<div class="form-group">
<label class="sr-only" for="login_password">Password</label>
<input type="password" id="login_password" name="password" placeholder="Password" class="form-control text"/>
</div>
<input type="hidden" name="from" value="${request.getParameter('from')}"/>
<input type="submit" style="margin-top:2em; display:block"/>
<button type="submit" class="btn btn-default btn-lg btn-block">Login</button>
</form>
(<a href="signup">Want to sign up?</a> or <a href="passwordReset">forgot the password?</a>)
<small><a href="signup">Sign up?</a> - <a href="passwordReset">Forgot password</a></small>
<script>
window.onload = function() {
document.getElementById('userid').focus();
}
</script>
</div>
<div class="col-sm-6">
</div>
</div>
</t:layout>
</j:jelly>

View File

@ -1,21 +1,30 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Reset your password">
<h1>Reset your password</h1>
<t:layout title="Reset password">
<div class="row">
<div class="col-sm-6">
<h1>Reset password</h1>
<form method="post" action="doPasswordReset">
<h5>User ID</h5>
<input type="text" name="id" class="text"/>
<div class="form-group">
<label class="sr-only">Email</label>
<input type="text" name="id" class="form-control text" placeholder="UserId or Email"/>
</div>
<p class="description">
You can also specify your e-mail address that you've registered with us.
(Except for those accounts that are migrated from java.net, whose e-mail address is set to ID@java.net,
and not your real e-mail address.)
</p>
<input type="submit" value="Send me a new password via e-mail" style="margin-top:2em; display:block"/>
<button type="submit" class="btn btn-default btn-lg btn-block">Reset password</button>
<p>
If you can't figure this out, contact us to get your account recovered.
</p>
<small>If you can't figure this out, contact us to get your account recovered.</small>
</form>
</div>
<div class="col-sm-6">
</div>
</div>
</t:layout>
</j:jelly>

View File

@ -1,38 +1,56 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Sign up">
<h1>Sign up</h1>
<div class="row">
<div class="col-sm-6 block-center">
<h1 class="text-center" >Sign up</h1>
<form method="post" action="doSignup">
<h5>User ID</h5>
<input type="text" name="userid" class="text"/>
<p class="description">
Only alphabets, numbers, and '_' is allowed.
</p>
<div class="form-group">
<label class="sr-only" for="userid">User ID</label>
<input type="text" name="userid" id="userid" class="form-control text" placeholder="Userid"/>
<span id="helpBlock" class="help-block text-center">
Only alphabets, numbers, and '_' is allowed.
</span>
</div>
<h5>First Name</h5>
<input type="text" name="firstName" class="text"/>
<div class="form-group">
<label class="sr-only" for="firstname">First Name</label>
<input type="text" name="firstName" id="firstname" class="form-control text" placeholder="First Name"/>
</div>
<h5>Last Name</h5>
<input type="text" name="lastName" class="text"/>
<div class="form-group">
<label class="sr-only" for="lastname" >Last Name</label>
<input type="text" id='lastname' name="lastName" class="form-control text" placeholder="Last Name"/>
</div>
<h5>E-mail</h5>
<input type="text" name="email" class="text"/>
<div class="form-group">
<label class="sr-only" for="email">E-mail</label>
<input type="email" name="email" id="email" class="form-control text" placeholder="Email"/>
</div>
<h5>What do you use Jenkins for?</h5>
<input type="text" name="usedFor" class="text"/>
<input id="hp" type="text" name="hp"/>
<div class="form-group">
<label class="sr-only" for="usedFor">Usage</label>
<input type="text" name="usedFor" id="usedfor" class="form-control text" placeholder="What do you use Jenkins for?"/>
</div>
<input id="hp" type="text" name="hp"/>
<script>
<![CDATA[
document.getElementById("hp").style.display = "none";
]]>
<![CDATA[document.getElementById("hp").style.display = "none";]]>
</script>
<j:if test="${it.captchaPublicKey()!=null}">
<h5>Captcha</h5>
<label class="sr-only" for="captcha">Captcha</label>
<script src="https://www.google.com/recaptcha/api.js" async="true" defer="true"></script>
<div class="g-recaptcha" data-sitekey="${it.captchaPublicKey()}"></div>
<div class="g-recaptcha" id="captcha" data-sitekey="${it.captchaPublicKey()}"></div>
</j:if>
<br/>
<input type="submit" style="margin-top:2em; display:block"/>
<button type="submit" class="btn btn-default btn-lg btn-block">Sign Up</button>
</form>
</div>
<div class="col-sm-6">
</div>
</div>
</t:layout>
</j:jelly>

View File

@ -0,0 +1,19 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Board Election">
<h1>Candidates</h1>
<ul>
<j:forEach var="c" items="${it.candidates}">
<li><img src="${c.avatar}"/> ${c.firstName} ${c.lastName} (<a href="https://wiki.jenkins-ci.org/display/~${c.userId}">${c.userId}</a>)</li>
</j:forEach>
</ul>
<form method="post" action="addCandidate">
<h4>Add candidates by entering user ID</h4>
<input type="text" name="userId" placeholed="userId"/>
<button type="submit">Add</button>
</form>
</t:layout>
</j:jelly>

View File

@ -0,0 +1,5 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Board Election">
<h1>Your vote has been recorded, thanks!</h1>
</t:layout>
</j:jelly>

View File

@ -0,0 +1,49 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Board Election">
<j:choose>
<j:when test="${it.open}">
<h1>Board Election</h1>
<p class="help-block"> Use drag and drop to order candidates by your preference for <a href="https://wiki.jenkins-ci.org/display/JENKINS/Board+Election+Process">Board election</a>. </p>
<script>
$( function() {
$( "#sortable" ).sortable();
$( "#sortable" ).disableSelection();
} );
function vote() {
var vote = $.map( $("#sortable > li"), function (element) {
return element.id;
}).join(",");
$("#vote_form > input").val(vote);
$("#vote_form").submit();
}
</script>
<form method="post" action="vote" id="vote_form" >
<ul id="sortable" class="list-group">
<j:forEach var="c" indexVar="i" items="${it.candidates}" >
<li id="${i}" class="list-group-item text-center text-capitalize" >
<span class="pull-left fa fa-arrows-v" aria-hidden="true"></span>
${c}
<span onclick="this.parentElement.remove()" class="pull-right fa fa-times"></span>
</li>
</j:forEach>
</ul>
<input type="hidden" name="vote"/>
</form>
<button onclick="vote()" class="btn btn-default btn-lg pull-center center-block">Vote</button>
</j:when>
<j:otherwise>
<h1>Board election is closed.</h1>
</j:otherwise>
</j:choose>
</t:layout>
</j:jelly>

View File

@ -1,45 +1,60 @@
<j:jelly xmlns:j="jelly:core" xmlns:t="/org/jenkinsci/account/taglib">
<t:layout title="Your Profile">
<div class="row">
<div class="col-sm-6">
<h1>Your Profile</h1>
<form method="post" action="update">
<h5>User ID</h5>
<input type="text" readonly="true" value="${it.userId}" class="text" disabled="true"/>
<div class="form-group">
<label>User ID</label>
<input type="text" readonly="true" value="${it.userId}" class="form-control text" disabled="true"/>
</div>
<h5>First Name</h5>
<input type="text" name="firstName" value="${it.firstName}" class="text"/>
<div class="form-group">
<label>First Name</label>
<input type="text" name="firstName" value="${it.firstName}" class="form-control text"/>
</div>
<h5>Last Name</h5>
<input type="text" name="lastName" value="${it.lastName}" class="text"/>
<div class="form-group">
<label>Last Name</label>
<input type="text" name="lastName" value="${it.lastName}" class="form-control text"/>
</div>
<h5>E-mail</h5>
<input type="text" name="email" value="${it.email}" class="text"/>
<div class="form-group">
<label>E-mail</label>
<input type="text" name="email" value="${it.email}" class="form-control text"/>
</div>
<h5>GitHub ID</h5>
<input type="text" name="githubId" value="${it.githubId}" class="text"/>
<div class="form-group">
<label>GitHub ID</label>
<input type="text" name="githubId" value="${it.githubId}" class="form-control text"/>
</div>
<h5>SSH Public Keys</h5>
<textarea name="sshKeys" style="width:80%; height:10em;">${it.sshKeys}</textarea>
<div class="form-group">
<label>SSH Public Keys</label>
<textarea class="form-control" rows="3">${it.sshKeys}</textarea>
</div>
<div style="height:2em"></div>
<fieldset style="width:25em">
<legend>Change Password</legend>
<p class="description">
To update your password, please type your current password as well as new one for security.
Leave this empty to keep the current password.
</p>
<legend>Change Password</legend>
<p class="description">
To update your password, please type your current password as well as new one for security.
Leave this empty to keep the current password.
</p>
<h5>Current Password</h5>
<input type="password" name="password" value="" class="text"/>
<label class="sr-only">Current Password</label>
<input type="password" name="password" value="" class="form-control text" placeholder="Current Password"/>
<h5>New Password</h5>
<input type="password" name="newPassword1" class="text"/>
<label class="sr-only">New Password</label>
<input type="password" name="newPassword1" class="form-control text" placeholder="New Password"/>
<h5>Confirm New Password</h5>
<input type="password" name="newPassword2" class="text"/>
</fieldset>
<label class="sr-only">Confirm New Password</label>
<input type="password" name="newPassword2" class="form-control text" placeholder="Confirm new password"/>
<input type="submit" style="margin-top:2em; display:block"/>
<button type="submit" class="btn btn-default btn-lg btn-block">Update</button>
</form>
</div>
<div class="col-sm-6">
</div>
</div>
</t:layout>
</j:jelly>

View File

@ -4,7 +4,7 @@
</st:documentation>
<st:contentType value="text/html;charset=UTF-8" />
<html>
<html style="height: 100%;">
<head>
<title>
${attrs.title} | Jenkins
@ -33,6 +33,7 @@
<meta content='${attrs.title}' property='og:title'/>
<meta content='article' property='og:type'/>
<meta content='Jenkins Continuous Delivery for every team' property='og:description'/>
<link href='/webjars/fontawesome/4.7.0/css/font-awesome.min.css' media='screen' rel='stylesheet'/>
<link href='https://jenkins.io/assets/bower/bootstrap/css/bootstrap.min.css' media='screen' rel='stylesheet'/>
<link href='https://jenkins.io/assets/bower/tether/css/tether.min.css' media='screen' rel='stylesheet'/>
<link href='https://jenkins.io/css/font-icons.css' media='screen' rel='stylesheet'/>
@ -40,10 +41,10 @@
<!-- Non-obtrusive CSS styles -->
<link href='https://jenkins.io/assets/bower/ionicons/css/ionicons.min.css' media='screen' rel='stylesheet'/>
<link href='https://jenkins.io/css/footer.css' media='screen' rel='stylesheet'/>
<link href='https://jenkins.io/css/font-awesome.min.css' media='screen' rel='stylesheet'/>
<script src="/webjars/jquery/3.2.0/jquery.js"></script>
<script src="/webjars/jquery-ui/1.12.1/jquery-ui.js"></script>
</head>
<body>
<script src='https://jenkins.io/assets/bower/jquery/jquery.js'></script>
<body style="height: 100%;">
<!-- starting partial toptoolbar.html.haml -->
<nav class='navbar navbar-toggleable-md navbar-inverse top bg-inverse fixed-top' id='ji-toolbar'>
<div class='container'>
@ -183,12 +184,32 @@ Download
<!-- ending partial toptoolbar.html.haml -->
<div class='container'>
<div class='row'>
<div class="col-md-12">
<d:invokeBody />
</div>
<div class='container' style="height: 60%;overflow: auto;">
<div class='row'>
<div class="col-sm-3 col-md-3">
<div class='sidebar-nav tour'>
<j:if test="${app.isLoggedIn() or it.isLoggedIn()}">
<h4>Account</h4>
<ul>
<li><a href="/" class="active">Home</a></li>
<j:if test="${it.isAdmin() or app.isAdmin()}">
<li><a href="/admin">Administration</a></li>
</j:if>
<li><a href="/election">Board Election</a></li>
<li><a href="/myself">Profile</a></li>
<li><a href="/logout">Logout</a></li>
</ul>
</j:if>
</div>
</div>
<div class="col-md-9 col-md-offset-3">
<div class="main">
<d:invokeBody />
</div>
</div>
</div>
</div>

View File

@ -17,6 +17,16 @@
<url-pattern>/*</url-pattern>
</servlet-mapping>
<!--Webjars Servlet-->
<servlet>
<servlet-name>WebjarsServlet</servlet-name>
<servlet-class>org.webjars.servlet.WebjarsServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>WebjarsServlet</servlet-name>
<url-pattern>/webjars/*</url-pattern>
</servlet-mapping>
<listener>
<listener-class>org.jenkinsci.account.WebAppMain</listener-class>
</listener>