From 39bb677e657ad02c120a500c85e41b6e16aa7750 Mon Sep 17 00:00:00 2001 From: Alexander Medvednikov Date: Mon, 18 May 2026 20:14:59 +0300 Subject: [PATCH] github login --- gitly.v | 6 ++ repo/repo.v | 12 ++- repo/repo_routes.v | 33 +++++-- static/assets/version | 2 +- static/css/gitly.scss | 206 ++++++++++++++++++++++++++++++++++++++++ templates/login.html | 2 +- templates/new.html | 27 +++++- templates/register.html | 4 + translations/en.tr | 50 +++++++++- translations/ru.tr | 50 +++++++++- user/user_routes.v | 3 + 11 files changed, 377 insertions(+), 18 deletions(-) diff --git a/gitly.v b/gitly.v index 7a84b39..aef7e70 100644 --- a/gitly.v +++ b/gitly.v @@ -328,6 +328,12 @@ fn (mut app App) create_tables() ! { sql app.db { create table ApiToken }! + sql app.db { + create table Org + }! + sql app.db { + create table OrgMember + }! } fn (mut app App) migrate_tables() ! { diff --git a/repo/repo.v b/repo/repo.v index 087b43f..e3d323b 100644 --- a/repo/repo.v +++ b/repo/repo.v @@ -132,9 +132,15 @@ fn (app App) find_repo_by_name_and_user_id(repo_name string, user_id int) ?Repo } fn (app App) find_repo_by_name_and_username(repo_name string, username string) ?Repo { - user := app.get_user_by_username(username) or { return none } - - return app.find_repo_by_name_and_user_id(repo_name, user.id) + repos := sql app.db { + select from Repo where name == repo_name && user_name == username && is_deleted == false limit 1 + } or { return none } + if repos.len == 0 { + return none + } + mut repo := repos.first() + repo.lang_stats = app.find_repo_lang_stats(repo.id) + return repo } fn (mut app App) get_count_user_repos(user_id int) int { diff --git a/repo/repo_routes.v b/repo/repo_routes.v index cd4fe58..5911bff 100644 --- a/repo/repo_routes.v +++ b/repo/repo_routes.v @@ -215,6 +215,8 @@ pub fn (mut app App) new() veb.Result { if !ctx.logged_in { return ctx.redirect_to_login() } + orgs := app.find_orgs_for_user(ctx.user.id) + selected_owner := ctx.query['owner'] or { ctx.user.username } return $veb.html() } @@ -227,7 +229,22 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str if !ctx.logged_in { return ctx.redirect_to_login() } - if !ctx.is_admin() && app.get_count_user_repos(ctx.user.id) >= max_user_repos { + owner := ctx.form['owner'] or { ctx.user.username } + mut owner_name := ctx.user.username + mut owner_org_id := 0 + if owner != ctx.user.username { + org := app.get_org_by_name(owner) or { + ctx.error('Unknown owner "${owner}"') + return app.new(mut ctx) + } + if !app.is_org_member(org.id, ctx.user.id) { + ctx.error('You are not a member of "${owner}"') + return app.new(mut ctx) + } + owner_name = org.name + owner_org_id = org.id + } + if owner_org_id == 0 && !ctx.is_admin() && app.get_count_user_repos(ctx.user.id) >= max_user_repos { ctx.error('You have reached the limit for the number of repositories') return app.new(mut ctx) } @@ -236,7 +253,7 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str return app.new(mut ctx) } eprintln(1) - if _ := app.find_repo_by_name_and_username(name, ctx.user.username) { + if _ := app.find_repo_by_name_and_username(name, owner_name) { ctx.error('A repository with the name "${name}" already exists') return app.new(mut ctx) } @@ -266,7 +283,11 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str } } println('OK') - repo_path := os.join_path(app.config.repo_storage_path, ctx.user.username, name) + owner_dir := os.join_path(app.config.repo_storage_path, owner_name) + if !os.exists(owner_dir) { + os.mkdir(owner_dir) or { app.info('failed to create owner dir ${owner_dir}: ${err}') } + } + repo_path := os.join_path(owner_dir, name) id := app.get_max_repo_id() + 1 mut new_repo := &Repo{ name: name @@ -275,7 +296,7 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str git_dir: repo_path user_id: ctx.user.id primary_branch: 'master' - user_name: ctx.user.username + user_name: owner_name clone_url: valid_clone_url is_public: is_public } @@ -297,7 +318,7 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str clone_job_repo := *new_repo spawn clone_repo(clone_job_repo, app.config, import_issues, ctx.user.id) } - new_repo2 := app.find_repo_by_name_and_user_id(new_repo.name, ctx.user.id) or { + new_repo2 := app.find_repo_by_name_and_username(new_repo.name, owner_name) or { app.info('Repo was not inserted') return ctx.redirect('/new') } @@ -328,7 +349,7 @@ pub fn (mut app App) handle_new_repo(mut ctx Context, name string, clone_url str if !has_first_repo_activity { app.add_activity(ctx.user.id, 'first_repo') or { app.info(err.str()) } } - return ctx.redirect('/${ctx.user.username}/${new_repo.name}') + return ctx.redirect('/${owner_name}/${new_repo.name}') } fn bg_fetch_files_info(repo_ Repo, branch string, path string, conf config.Config) { diff --git a/static/assets/version b/static/assets/version index 68ffd24..974adde 100644 --- a/static/assets/version +++ b/static/assets/version @@ -1 +1 @@ -1adb1cd \ No newline at end of file +0f1fc08 \ No newline at end of file diff --git a/static/css/gitly.scss b/static/css/gitly.scss index c4c164b..fe4d608 100644 --- a/static/css/gitly.scss +++ b/static/css/gitly.scss @@ -407,6 +407,22 @@ form { } } +.github-auth { + display: inline-block; + margin-top: 15px; + padding: 8px 14px; + background-color: #24292f; + color: $white !important; + border-radius: $small-radius; + text-decoration: none; + font-weight: 500; + transition: background-color 0.07s; +} + +.github-auth:hover { + background-color: #1a7be9; +} + .block { margin: auto; max-width: 700px; @@ -900,6 +916,47 @@ form { } } +.new-repo__owner-row { + display: flex; + align-items: flex-end; + gap: 8px; +} + +.new-repo__field--owner { + flex: 0 0 auto; + min-width: 180px; +} + +.new-repo__field--name { + flex: 1 1 auto; +} + +.new-repo__owner-select { + box-sizing: border-box; + width: 100%; + padding: 7px 10px; + font-size: 14px; + border: 1px solid $gray; + border-radius: $small-radius; + background-color: $white; + + &:focus { + border-color: #4392eb; + outline: none; + } +} + +.new-repo__owner-slash { + font-size: 20px; + color: $gray-dark; + padding-bottom: 4px; +} + +.new-repo__name-input--standalone { + border: 1px solid $gray; + border-radius: $small-radius; +} + .new-repo__owner { background-color: $gray-light; color: $gray-dark; @@ -1000,6 +1057,155 @@ form { } } +.new-org { + max-width: 640px; +} + +.new-org__header { + margin: 24px 0 24px; + text-align: center; + + h1 { + font-size: 28px; + margin: 0; + padding: 0; + } +} + +.new-org__subtitle { + margin: 0 0 4px; + color: $gray-dark; + font-size: 13px; + font-family: monospace; +} + +.new-org__form { + display: flex; + flex-direction: column; + gap: 22px; +} + +.new-org__field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.new-org__field--inline { + gap: 0; +} + +.new-org__label { + font-weight: 600; + font-size: 14px; + margin: 0; + padding: 0; + color: $black; +} + +.new-org__req { + color: #cf222e; + margin-left: 2px; +} + +.new-org__input { + box-sizing: border-box; + width: 100%; + padding: 8px 10px; + font-size: 14px; + border: 1px solid $gray; + border-radius: $small-radius; + background-color: #f6f8fa; + + &:focus { + border-color: #4392eb; + background-color: $white; + outline: none; + } +} + +.new-org__hint { + margin: 0; + color: $gray-dark; + font-size: 12px; +} + +.new-org__url-slot { + font-weight: 600; + color: $black; +} + +.new-org__option { + display: flex; + gap: 10px; + padding: 4px 0; + cursor: pointer; + align-items: flex-start; + + input[type="radio"] { + margin-top: 4px; + } +} + +.new-org__option-body { + display: flex; + flex-direction: column; + gap: 2px; +} + +.new-org__option-title { + font-weight: 600; + font-size: 14px; + color: $black; +} + +.new-org__option-desc { + color: $gray-dark; + font-size: 13px; + line-height: 1.4; +} + +.new-org__checkbox { + display: inline-flex; + align-items: flex-start; + gap: 8px; + cursor: pointer; + font-size: 14px; + line-height: 1.5; + + input[type="checkbox"] { + margin: 4px 0 0; + } +} + +.new-org__actions { + padding-top: 6px; + display: flex; + justify-content: stretch; +} + +.new-org__submit { + width: 100% !important; + padding: 10px 18px; + background-color: #aed8b8; + color: $white; + border: 1px solid rgba(31, 136, 61, 0.4); + font-weight: 500; + cursor: not-allowed; + + &:not(:disabled) { + background-color: #1f883d; + border-color: rgba(31, 136, 61, 0.6); + cursor: pointer; + } + + &:not(:disabled):hover { + background-color: #1a7f37; + border-color: rgba(27, 31, 36, 0.15); + color: $white; + } +} + .new-discussion { max-width: 720px; } diff --git a/templates/login.html b/templates/login.html index 744d17e..960c0b9 100644 --- a/templates/login.html +++ b/templates/login.html @@ -28,7 +28,7 @@ @if app.settings.oauth_client_id != '' - Login via GitHub + %login_via_github @end diff --git a/templates/new.html b/templates/new.html index 2677a6a..3d8985f 100644 --- a/templates/new.html +++ b/templates/new.html @@ -17,11 +17,28 @@ @end
-
- -
- @app.config.hostname/@ctx.user.username/ - +
+
+ + +
+
/
+
+ +
diff --git a/templates/register.html b/templates/register.html index 1f2e099..95bdff6 100644 --- a/templates/register.html +++ b/templates/register.html @@ -46,6 +46,10 @@
+ + @if app.settings.oauth_client_id != '' + %register_via_github + @end @js '/js/register.js' diff --git a/translations/en.tr b/translations/en.tr index 21555b8..1255e61 100644 --- a/translations/en.tr +++ b/translations/en.tr @@ -198,7 +198,10 @@ new_repo_title Create a new repository ----- new_repo_name -Name +Repository name +----- +new_repo_owner +Owner ----- new_repo_description Description @@ -233,6 +236,45 @@ You choose who can see and commit to this repository. new_repo_create Create repository ----- +new_org_subtitle +Tell us about your organization +----- +new_org_title +Set up your organization +----- +new_org_name +Organization name +----- +new_org_name_hint +This will be the name of your account on Gitly. +----- +new_org_url_hint +Your URL will be: +----- +new_org_contact_email +Contact email +----- +new_org_belongs_to +This organization belongs to: +----- +new_org_personal +My personal account +----- +new_org_personal_example +I.e., +----- +new_org_business +A business or institution +----- +new_org_business_example +For example: Acme, Inc., Example Institute, American Red Cross +----- +new_org_accept_terms +I hereby accept the Terms of Service. +----- +new_org_next +Next +----- commits_count commit|commits|commits ----- @@ -989,3 +1031,9 @@ First issue user_activity_on on ----- +login_via_github +Login via GitHub +----- +register_via_github +Register via GitHub +----- diff --git a/translations/ru.tr b/translations/ru.tr index 4982b57..35fd5c5 100644 --- a/translations/ru.tr +++ b/translations/ru.tr @@ -198,7 +198,10 @@ new_repo_title Создать новый репозиторий KEKW ----- new_repo_name -Название +Название репозитория +----- +new_repo_owner +Владелец ----- new_repo_description Описание @@ -233,6 +236,45 @@ new_repo_private_desc new_repo_create Создать репозиторий ----- +new_org_subtitle +Расскажите о вашей организации +----- +new_org_title +Создание организации +----- +new_org_name +Название организации +----- +new_org_name_hint +Это будет именем вашего аккаунта в Gitly. +----- +new_org_url_hint +URL будет: +----- +new_org_contact_email +Контактный email +----- +new_org_belongs_to +Эта организация принадлежит: +----- +new_org_personal +Моему личному аккаунту +----- +new_org_personal_example +Например, +----- +new_org_business +Компании или учреждению +----- +new_org_business_example +Например: ООО «Ромашка», Example Institute, Красный Крест +----- +new_org_accept_terms +Я принимаю Условия использования. +----- +new_org_next +Далее +----- commits_count коммит|коммита|коммитов ----- @@ -989,3 +1031,9 @@ user_activity_first_issue user_activity_on от ----- +login_via_github +Войти через GitHub +----- +register_via_github +Регистрация через GitHub +----- diff --git a/user/user_routes.v b/user/user_routes.v index 49acaf7..0519ff9 100644 --- a/user/user_routes.v +++ b/user/user_routes.v @@ -190,6 +190,9 @@ pub fn (mut app App) register(mut ctx Context) veb.Result { if ctx.logged_in { return ctx.redirect('/${ctx.user.username}') } + csrf := rand.string(30) + ctx.set_cookie(name: 'csrf', value: csrf) + user_count := app.get_users_count() or { 0 } no_users := user_count == 0 -- 2.39.5